Skip to content

Instantly share code, notes, and snippets.

@katspaugh
Created December 6, 2023 12:36
Show Gist options
  • Save katspaugh/148fc9d5482e8834087a780986610a9f to your computer and use it in GitHub Desktop.
Save katspaugh/148fc9d5482e8834087a780986610a9f to your computer and use it in GitHub Desktop.
<!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">&nbsp;</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>
// 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