mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-27 10:53:50 +02:00
267 lines
6.3 KiB
HTML
267 lines
6.3 KiB
HTML
<!DOCTYPE html>
|
|
|
|
<input type="file">
|
|
|
|
<div id="player"
|
|
style="display: none; flex-direction: column; align-items: center;
|
|
max-width: 1000px; max-height: calc(100svh - 10px);
|
|
margin: 0 auto;">
|
|
<canvas style="background: magenta; flex: 1 1 0%; min-height: 0;"></canvas>
|
|
<div style="display: flex; width: 100%; align-items: center; gap: 8px;">
|
|
<button style="width: 100px;"></button>
|
|
<p id="currentTime" style="font-variant-numeric: tabular-nums;">-</p>
|
|
<input type="range" style="flex: 1 1 0; outline: none;" value="0" min="0" max="1" step="0.000001">
|
|
<p id="duration" style="font-variant-numeric: tabular-nums;">-</p>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="../dist/metamuxer.js"></script>
|
|
|
|
<script type="module">
|
|
const fileInput = document.querySelector('input[type="file"]');
|
|
const currentTimeElement = document.querySelector('#currentTime');
|
|
const durationElement = document.querySelector('#duration');
|
|
const playerDiv = document.querySelector('#player');
|
|
const playButton = playerDiv.querySelector('button');
|
|
const rangeInput = playerDiv.querySelector('input[type="range"]');
|
|
|
|
const file = await new Promise(resolve => {
|
|
fileInput.addEventListener('change', () => {
|
|
resolve(fileInput.files[0]);
|
|
});
|
|
});
|
|
|
|
fileInput.style.display = 'none';
|
|
const audioContext = new AudioContext();
|
|
|
|
const source = new Metamuxer.BlobSource(file);
|
|
|
|
const input = new Metamuxer.Input({
|
|
formats: Metamuxer.ALL_FORMATS,
|
|
source
|
|
});
|
|
|
|
console.log(await input.getMimeType());
|
|
|
|
let videoTrack = await input.getPrimaryVideoTrack();
|
|
let audioTrack = await input.getPrimaryAudioTrack();
|
|
|
|
if (!(await videoTrack?.canDecode())) {
|
|
videoTrack = null;
|
|
}
|
|
if (!(await audioTrack?.canDecode())) {
|
|
audioTrack = null;
|
|
}
|
|
|
|
const canvas = document.querySelector('canvas');
|
|
const context = canvas.getContext('2d');
|
|
let videoRotation = 0;
|
|
|
|
if (!videoTrack) {
|
|
canvas.style.display = 'none';
|
|
} else {
|
|
canvas.width = videoTrack.displayWidth;
|
|
canvas.height = videoTrack.displayHeight;
|
|
}
|
|
|
|
const totalDuration = await input.computeDuration();
|
|
durationElement.textContent = formatSeconds(totalDuration);
|
|
|
|
playerDiv.style.display = 'flex';
|
|
|
|
const videoSink = videoTrack && new Metamuxer.CanvasSink(videoTrack, { poolSize: 2 });
|
|
const audioSink = audioTrack && new Metamuxer.AudioBufferSink(audioTrack);
|
|
|
|
let startTime = null;
|
|
let playing = false;
|
|
let playbackTimeAtStart = 0;
|
|
let videoFrameIterator = null;
|
|
let audioBufferIterator = null;
|
|
let currentFrame = null;
|
|
let nextFrame = null;
|
|
let seeking = false;
|
|
let seekId = 0;
|
|
const queuedAudioNodes = new Set();
|
|
|
|
function getPlaybackTime() {
|
|
if (playing) {
|
|
return audioContext.currentTime - startTime + playbackTimeAtStart;
|
|
} else {
|
|
return playbackTimeAtStart;
|
|
}
|
|
}
|
|
|
|
function play() {
|
|
audioBufferIterator?.return();
|
|
audioBufferIterator = audioSink?.buffers(getPlaybackTime());
|
|
startTime = audioContext.currentTime;
|
|
playing = true;
|
|
runAudioIterator();
|
|
|
|
playButton.textContent = 'Pause';
|
|
}
|
|
|
|
function pause() {
|
|
playbackTimeAtStart = getPlaybackTime();
|
|
playing = false;
|
|
audioBufferIterator?.return();
|
|
audioBufferIterator = null;
|
|
|
|
for (const node of queuedAudioNodes) {
|
|
node.stop();
|
|
}
|
|
|
|
playButton.textContent = 'Play';
|
|
}
|
|
|
|
function togglePlay() {
|
|
if (seeking) {
|
|
return;
|
|
}
|
|
|
|
if (playing) {
|
|
pause();
|
|
} else {
|
|
play();
|
|
}
|
|
}
|
|
|
|
async function seek() {
|
|
if (!videoTrack) {
|
|
return;
|
|
}
|
|
|
|
seeking = true;
|
|
seekId++;
|
|
|
|
await videoFrameIterator?.return();
|
|
|
|
videoFrameIterator = videoSink.canvases(getPlaybackTime());
|
|
|
|
const newCurrentFrame = (await videoFrameIterator.next()).value;
|
|
const newNextFrame = (await videoFrameIterator.next()).value;
|
|
|
|
currentFrame = newCurrentFrame;
|
|
nextFrame = newNextFrame;
|
|
|
|
if (currentFrame) {
|
|
context.drawImage(currentFrame.canvas, 0, 0);
|
|
}
|
|
|
|
seeking = false;
|
|
}
|
|
await seek();
|
|
|
|
async function render() {
|
|
const playbackTime = getPlaybackTime();
|
|
if (playbackTime >= totalDuration) {
|
|
pause();
|
|
playbackTimeAtStart = totalDuration;
|
|
}
|
|
|
|
if (currentFrame) {
|
|
while (nextFrame && nextFrame.timestamp <= playbackTime && !seeking) {
|
|
currentFrame = nextFrame;
|
|
context.drawImage(currentFrame.canvas, 0, 0);
|
|
|
|
const currentSeekId = seekId;
|
|
const newNextFrame = (await videoFrameIterator.next()).value;
|
|
|
|
if (currentSeekId === seekId) {
|
|
nextFrame = newNextFrame;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!movingRangeInput) {
|
|
rangeInput.value = playbackTime / totalDuration;
|
|
currentTimeElement.textContent = formatSeconds(playbackTime);
|
|
}
|
|
|
|
requestAnimationFrame(render);
|
|
}
|
|
|
|
async function runAudioIterator() {
|
|
if (!audioTrack) {
|
|
return;
|
|
}
|
|
|
|
for await (let { buffer, timestamp } of audioBufferIterator) {
|
|
const node = audioContext.createBufferSource();
|
|
node.buffer = buffer;
|
|
node.connect(audioContext.destination);
|
|
|
|
const startTimestamp = startTime + timestamp - playbackTimeAtStart;
|
|
if (startTimestamp >= audioContext.currentTime) {
|
|
node.start(startTimestamp);
|
|
} else {
|
|
node.start(audioContext.currentTime, audioContext.currentTime - startTimestamp);
|
|
}
|
|
|
|
queuedAudioNodes.add(node);
|
|
node.onended = () => {
|
|
queuedAudioNodes.delete(node);
|
|
};
|
|
|
|
if (timestamp - getPlaybackTime() >= 1) {
|
|
await new Promise(resolve => {
|
|
const id = setInterval(() => {
|
|
if (timestamp - getPlaybackTime() < 1) {
|
|
clearInterval(id);
|
|
resolve();
|
|
}
|
|
}, 100);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function formatSeconds(seconds) {
|
|
seconds = Math.round(seconds * 1000) / 1000; // Round to milliseconds
|
|
|
|
const minutes = Math.floor(seconds / 60);
|
|
const remainingSeconds = Math.floor(seconds % 60);
|
|
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}.${Math.floor(1000 * seconds % 1000).toString().padStart(3, '0')}`;
|
|
}
|
|
|
|
playButton.addEventListener('click', togglePlay);
|
|
window.addEventListener('keydown', (e) => {
|
|
if (e.code === 'Space') {
|
|
togglePlay();
|
|
e.preventDefault();
|
|
}
|
|
});
|
|
|
|
let movingRangeInput = false;
|
|
rangeInput.addEventListener('input', () => {
|
|
movingRangeInput = true;
|
|
const time = Number(rangeInput.value) * totalDuration;
|
|
currentTimeElement.textContent = formatSeconds(time);
|
|
});
|
|
rangeInput.addEventListener('change', async () => {
|
|
movingRangeInput = false;
|
|
|
|
if (seeking) {
|
|
return;
|
|
}
|
|
|
|
const wasPlaying = playing;
|
|
|
|
if (wasPlaying) {
|
|
pause();
|
|
}
|
|
|
|
const newTime = Number(rangeInput.value) * totalDuration;
|
|
playbackTimeAtStart = newTime;
|
|
|
|
await seek();
|
|
|
|
if (wasPlaying) {
|
|
play();
|
|
}
|
|
});
|
|
addEventListener('pointerup', () => movingRangeInput = false);
|
|
|
|
render();
|
|
play();
|
|
</script> |