Created
December 6, 2023 12:36
-
-
Save katspaugh/148fc9d5482e8834087a780986610a9f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> | |
<title>Video Annotation System</title> | |
<link href="data:image/gif;" rel="icon" type="image/x-icon" /> | |
<!-- Bootstrap --> | |
<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet"> | |
<link rel="stylesheet" href="../css/style.css" /> | |
<link rel="stylesheet" href="../css/ribbon.css" /> | |
<link rel="screenshot" itemprop="screenshot" href="https://katspaugh.github.io/wavesurfer.js/example/screenshot.png" /> | |
<!-- wavesurfer.js --> | |
<script src="https://unpkg.com/wavesurfer.js/dist/wavesurfer.js"></script> | |
<!-- plugins --> | |
<script src="https://unpkg.com/wavesurfer.js/dist/plugin/wavesurfer.timeline.js"></script> | |
<script src="https://unpkg.com/wavesurfer.js/dist/plugin/wavesurfer.regions.js"></script> | |
<script src="https://unpkg.com/wavesurfer.js/dist/plugin/wavesurfer.minimap.js"></script> | |
<script src="https://unpkg.com/wavesurfer.js/dist/plugin/wavesurfer.cursor.js"></script> | |
<!-- Demo --> | |
<script src="../trivia.js"></script> | |
<script src="main.js"></script> | |
<!-- highlight.js for syntax highlighting in this example --> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.2.0/styles/default.min.css"> | |
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.2.0/highlight.min.js"></script> | |
<script>hljs.highlightAll();</script> | |
</head> | |
<body itemscope itemtype="http://schema.org/WebApplication"> | |
<div class="container"> | |
<div class="header"> | |
<h1 itemprop="name">Video Annotation System</h1> | |
</div> | |
<div id="demo"> | |
<!-- This video comes from NASA Video Gallery https://www.youtube.com/watch?v=Zg7i4q_EX9E --> | |
<video style="display:block; margin: 0 auto;" src="../media/nasa.mp4" type="video/mpeg" width="800"> | |
<!-- Here be the video --> | |
</video> | |
<p id="subtitle" class="text-center text-info"> </p> | |
<div id="wave-timeline"></div> | |
<div id="waveform"> | |
<!-- Here be the waveform --> | |
</div> | |
<div class="row" style="margin: 30px 0"> | |
<div class="col-sm-3"> | |
<p> | |
Click on a region to enter an annotation.<br /> | |
</p> | |
</div> | |
<div class="col-sm-3"> | |
<button class="btn btn-primary btn-block" data-action="play"> | |
<span id="play"> | |
<i class="glyphicon glyphicon-play"></i> | |
Play | |
</span> | |
<span id="pause" style="display: none"> | |
<i class="glyphicon glyphicon-pause"></i> | |
Pause | |
</span> | |
</button> | |
</div> | |
<div class="col-sm-3"> | |
<button class="btn btn-info btn-block" data-action="export" title="Export annotations to JSON"> | |
<i class="glyphicon glyphicon-file"></i> | |
Export | |
</button> | |
</div> | |
</div> | |
</div> | |
<form role="form" name="edit" style="opacity: 0; transition: opacity 300ms linear; margin: 30px 0px;"> | |
<div class="col-sm-3 form-group"> | |
<label for="start">Begin</label> | |
<input class="form-control" id="start" name="start" /> | |
<label for="end">End</label> | |
<input class="form-control" id="end" name="end" /> | |
</div> | |
<div class="col-sm-6 form-group"> | |
<label for="note">Note</label> | |
<textarea id="note" class="form-control" rows="3" name="note"></textarea> | |
</div> | |
<div class="col-sm-3"> | |
<center><b>Region edit</b></center> | |
<button type="submit" class="btn btn-success btn-block">Save region</button> | |
<center><b>or</b></center> | |
<button type="button" class="btn btn-danger btn-block" data-action="delete-region">Delete region</button> | |
</div> | |
</form> | |
</div> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Create an instance | |
var wavesurfer; | |
// Init & load audio file | |
document.addEventListener('DOMContentLoaded', function() { | |
// Init | |
wavesurfer = WaveSurfer.create({ | |
container: document.querySelector('#waveform'), | |
height: 100, | |
pixelRatio: 1, | |
minPxPerSec: 100, | |
scrollParent: true, | |
normalize: true, | |
splitChannels: false, | |
backend: 'MediaElement', | |
plugins: [ | |
WaveSurfer.regions.create(), | |
WaveSurfer.minimap.create({ | |
height: 30, | |
waveColor: '#ddd', | |
progressColor: '#999' | |
}), | |
WaveSurfer.timeline.create({ | |
container: '#wave-timeline' | |
}), | |
WaveSurfer.cursor.create() | |
] | |
}); | |
// Load audio from existing media element | |
let mediaElt = document.querySelector('video'); | |
wavesurfer.on('error', function(e) { | |
console.warn(e); | |
}); | |
wavesurfer.load(mediaElt); | |
wavesurfer.on('ready', function() { | |
wavesurfer.enableDragSelection({ | |
color: randomColor(0.25) | |
}); | |
wavesurfer.util | |
.fetchFile({ | |
responseType: 'json', | |
url: '../media/nasa.json' | |
}) | |
.on('success', function(data) { | |
loadRegions(data); | |
saveRegions(); | |
}); | |
}); | |
wavesurfer.on('region-click', function(region, e) { | |
e.stopPropagation(); | |
// Play on click, loop on shift click | |
e.shiftKey ? region.playLoop() : region.play(); | |
}); | |
wavesurfer.on('region-click', editAnnotation); | |
wavesurfer.on('region-update-end', saveRegions); | |
wavesurfer.on('region-updated', saveRegions); | |
wavesurfer.on('region-removed', saveRegions); | |
wavesurfer.on('region-in', showNote); | |
wavesurfer.on('region-out', hideNote); | |
wavesurfer.on('region-play', function(region) { | |
region.once('out', function() { | |
wavesurfer.play(region.start); | |
wavesurfer.pause(); | |
}); | |
}); | |
/* Toggle play/pause buttons. */ | |
let playButton = document.querySelector('#play'); | |
let pauseButton = document.querySelector('#pause'); | |
wavesurfer.on('play', function() { | |
playButton.style.display = 'none'; | |
pauseButton.style.display = 'block'; | |
}); | |
wavesurfer.on('pause', function() { | |
playButton.style.display = 'block'; | |
pauseButton.style.display = 'none'; | |
}); | |
}); | |
/** | |
* Save annotations to localStorage. | |
*/ | |
function saveRegions() { | |
localStorage.regions = JSON.stringify( | |
Object.keys(wavesurfer.regions.list).map(function(id) { | |
let region = wavesurfer.regions.list[id]; | |
return { | |
start: region.start, | |
end: region.end, | |
attributes: region.attributes, | |
data: region.data | |
}; | |
}) | |
); | |
} | |
/** | |
* Load regions from localStorage. | |
*/ | |
function loadRegions(regions) { | |
regions.forEach(function(region) { | |
region.color = randomColor(0.25); | |
wavesurfer.addRegion(region); | |
}); | |
} | |
/** | |
* Extract regions separated by silence. | |
*/ | |
function extractRegions(peaks, duration) { | |
// Silence params | |
let minValue = 0.0015; | |
let minSeconds = 0.25; | |
let length = peaks.length; | |
let coef = duration / length; | |
let minLen = minSeconds / coef; | |
// Gather silence indeces | |
let silences = []; | |
Array.prototype.forEach.call(peaks, function(val, index) { | |
if (Math.abs(val) <= minValue) { | |
silences.push(index); | |
} | |
}); | |
// Cluster silence values | |
let clusters = []; | |
silences.forEach(function(val, index) { | |
if (clusters.length && val == silences[index - 1] + 1) { | |
clusters[clusters.length - 1].push(val); | |
} else { | |
clusters.push([val]); | |
} | |
}); | |
// Filter silence clusters by minimum length | |
let fClusters = clusters.filter(function(cluster) { | |
return cluster.length >= minLen; | |
}); | |
// Create regions on the edges of silences | |
let regions = fClusters.map(function(cluster, index) { | |
let next = fClusters[index + 1]; | |
return { | |
start: cluster[cluster.length - 1], | |
end: next ? next[0] : length - 1 | |
}; | |
}); | |
// Add an initial region if the audio doesn't start with silence | |
let firstCluster = fClusters[0]; | |
if (firstCluster && firstCluster[0] != 0) { | |
regions.unshift({ | |
start: 0, | |
end: firstCluster[firstCluster.length - 1] | |
}); | |
} | |
// Filter regions by minimum length | |
let fRegions = regions.filter(function(reg) { | |
return reg.end - reg.start >= minLen; | |
}); | |
// Return time-based regions | |
return fRegions.map(function(reg) { | |
return { | |
start: Math.round(reg.start * coef * 100) / 100, | |
end: Math.round(reg.end * coef * 100) / 100 | |
}; | |
}); | |
} | |
/** | |
* Random RGBA color. | |
*/ | |
function randomColor(alpha) { | |
return ( | |
'rgba(' + | |
[ | |
~~(Math.random() * 255), | |
~~(Math.random() * 255), | |
~~(Math.random() * 255), | |
alpha || 1 | |
] + | |
')' | |
); | |
} | |
/** | |
* Edit annotation for a region. | |
*/ | |
function editAnnotation(region) { | |
let form = document.forms.edit; | |
form.style.opacity = 1; | |
(form.elements.start.value = Math.round(region.start * 100) / 100), | |
(form.elements.end.value = Math.round(region.end * 100) / 100); | |
form.elements.note.value = region.data.note || ''; | |
form.onsubmit = function(e) { | |
e.preventDefault(); | |
region.update({ | |
start: form.elements.start.value, | |
end: form.elements.end.value, | |
data: { | |
note: form.elements.note.value | |
} | |
}); | |
form.style.opacity = 0; | |
}; | |
form.onreset = function() { | |
form.style.opacity = 0; | |
form.dataset.region = null; | |
}; | |
form.dataset.region = region.id; | |
} | |
/** | |
* Display annotation. | |
*/ | |
function showNote(region) { | |
if (!showNote.el) { | |
showNote.el = document.querySelector('#subtitle'); | |
} | |
showNote.el.style.color = 'Red'; | |
showNote.el.style.fontSize = 'large'; | |
showNote.el.textContent = region.data.note || '–'; | |
} | |
function hideNote(region) { | |
if (!hideNote.el) { | |
hideNote.el = document.querySelector('#subtitle'); | |
} | |
hideNote.el.style.color = 'Red'; | |
hideNote.el.style.fontSize = 'large'; | |
hideNote.el.textContent = '–'; | |
} | |
/** | |
* Bind controls. | |
*/ | |
window.GLOBAL_ACTIONS['delete-region'] = function() { | |
let form = document.forms.edit; | |
let regionId = form.dataset.region; | |
if (regionId) { | |
wavesurfer.regions.list[regionId].remove(); | |
form.reset(); | |
} | |
}; | |
window.GLOBAL_ACTIONS['export'] = function() { | |
window.open( | |
'data:application/json;charset=utf-8,' + | |
encodeURIComponent(localStorage.regions) | |
); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment