Compare commits

...
40 Commits
Author SHA1 Message Date
Vanilagy d2507a5a72 Fix incorrect decoded audio sample timestamps in Firefox 2025-09-01 16:39:19 +02:00
Vanilagy 69e1383796 Add thing 2025-09-01 16:11:42 +02:00
Vanilagy cdb00a3942 Made media player work properly on touch devices 2025-09-01 15:50:11 +02:00
Vanilagy ee4bfeed90 Fix BBB link 2025-09-01 12:05:35 +02:00
github-actions[bot] ac7e4422a3 Merge main into release for tag v1.12.0 2025-09-01 09:53:28 +00:00
Vanilagy 2373d802c5 Add roprgm 2025-08-28 17:59:45 +02:00
github-actions[bot] a8009d1ac5 Merge main into release for tag v1.11.2 2025-08-27 06:57:17 +00:00
github-actions[bot] 311d141f19 Merge main into release for tag v1.11.1 2025-08-25 17:45:41 +00:00
github-actions[bot] 99e16a0497 Merge main into release for tag v1.11.0 2025-08-24 14:40:07 +00:00
github-actions[bot] 174ae83843 Merge main into release for tag v1.10.0 2025-08-23 14:33:41 +00:00
github-actions[bot] 3e128553f9 Merge main into release for tag v1.9.3 2025-08-20 13:13:31 +00:00
github-actions[bot] 6e9873817c Merge main into release for tag v1.9.2 2025-08-20 12:34:02 +00:00
github-actions[bot] f704b87d03 Merge main into release for tag v1.9.1 2025-08-20 11:39:41 +00:00
Vanilagy 4bd146364e Merge branch 'main' into release 2025-08-19 10:27:03 +02:00
github-actions[bot] dc43b89905 Merge main into release for tag v1.9.0 2025-08-18 20:59:12 +00:00
Vanilagy cd990b837b Merge branch 'release' of https://github.com/Vanilagy/mediabunny into release 2025-08-15 21:17:52 +02:00
Vanilagy cfc8f8a512 Merge branch 'main' into release 2025-08-15 21:17:34 +02:00
github-actions[bot] e5b19ed72f Merge main into release for tag v1.8.0 2025-08-15 12:20:10 +00:00
Vanilagy 33531ef675 Merge branch 'main' into release 2025-08-15 12:01:04 +02:00
Vanilagy b5463d1bf8 Merge branch 'main' into release 2025-08-15 11:37:19 +02:00
github-actions[bot] 7a35928850 Merge main into release for tag v1.7.6 2025-08-14 08:57:29 +00:00
github-actions[bot] 1565a7bb1c Merge main into release for tag v1.7.5 2025-08-13 15:15:17 +00:00
github-actions[bot] 7de3d43149 Merge main into release for tag v1.7.4 2025-08-13 07:59:44 +00:00
github-actions[bot] 30a19fa7f7 Merge main into release for tag v1.7.3 2025-08-12 13:37:09 +00:00
github-actions[bot] 94257205bd Merge main into release for tag v1.7.2 2025-08-12 08:11:27 +00:00
github-actions[bot] 991cfb4a64 Merge main into release for tag v1.7.1 2025-08-11 16:14:58 +00:00
github-actions[bot] a1161a6290 Merge main into release for tag v1.7.0 2025-08-11 01:03:13 +00:00
github-actions[bot] a0fc1becb4 Merge main into release for tag v1.7.0 2025-08-11 01:01:22 +00:00
Vanilagy 508788eb58 Test commit 2025-08-10 15:13:39 +02:00
Vanilagy beea136347 Test commit 2025-08-10 15:13:27 +02:00
Vanilagy 24c7849282 Test commit 2025-08-10 15:12:56 +02:00
Vanilagy 2bf08c3903 Test commit 2025-08-10 15:11:11 +02:00
Vanilagy e130b5d831 Test commit 2025-08-10 15:08:30 +02:00
David P.andGitHub 7191207944 Merge pull request #53 from Vanilagy/main
Release
2025-08-10 14:37:22 +02:00
github-actions[bot] a146e7faee Merge main into release for tag v1.6.2 2025-08-10 02:08:55 +00:00
github-actions[bot] a62071ec92 Merge main into release for tag v1.6.1 2025-08-10 01:51:36 +00:00
github-actions[bot] e225959fbf Merge main into release for tag v1.6.1 2025-08-10 01:48:12 +00:00
github-actions[bot] fe35b1706b Merge main into release for tag v1.6.1 2025-08-10 01:46:41 +00:00
github-actions[bot] 632af85a35 Merge main into release for tag v1.6.0 2025-08-10 01:42:35 +00:00
github-actions[bot] c7dd3332c3 Merge main into release for tag v1.5.0 2025-08-08 16:58:16 +00:00
12 changed files with 122 additions and 48 deletions
+2
View File
@@ -88,6 +88,7 @@ const bundleSizes = [
const sponsors = {
gold: [
{ image: '/sponsors/remotion.png', name: 'Remotion', url: 'https://remotion.dev/' },
{ image: '/sponsors/gling.svg', name: 'Gling AI', url: 'https://www.gling.ai/' },
{ image: '/sponsors/diffusionstudio.png', name: 'Diffusion Studio', url: 'https://diffusion.studio/' },
{ image: '/sponsors/kino.jpg', name: 'Kino', url: 'https://kino.ai/' },
@@ -105,6 +106,7 @@ const sponsors = {
{ image: 'https://avatars.githubusercontent.com/u/91711202', name: 'ihasq', url: 'https://github.com/ihasq' },
{ image: 'https://avatars.githubusercontent.com/u/61233224', name: 'Allwhy', url: 'https://github.com/Allwhy' },
{ image: 'https://avatars.githubusercontent.com/u/97225946', name: '808vita', url: 'https://github.com/808vita' },
{ image: 'https://avatars.githubusercontent.com/u/3709646', name: 'Rodrigo Belfiore', url: 'https://github.com/roprgm' },
],
};
</script>
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

@@ -139,9 +139,9 @@ selectMediaButton.addEventListener('click', () => {
loadUrlButton.addEventListener('click', () => {
const url = prompt(
'Please enter a URL of a media file. Note that it must support cross-origin requests, so have the right'
+ ' CORS headers set.',
'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
'Please enter a URL of a media file. Note that it must be HTTPS and support cross-origin requests, so have the'
+ ' right CORS headers set.',
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
);
if (!url) {
return;
+17 -15
View File
@@ -50,31 +50,33 @@
</div>
</button>
<button class="p-2 ml-4 mr-1" id="volume-button">
<div class="size-6 invert group-hover:scale-110" id="volume-icon-wrapper">
<img src="../../docs/assets/volume-off-icon.svg" class="size-full">
<img src="../../docs/assets/volume-x-icon.svg" class="size-full">
<img src="../../docs/assets/volume-0-icon.svg" class="size-full">
<img src="../../docs/assets/volume-1-icon.svg" class="size-full">
<img src="../../docs/assets/volume-2-icon.svg" class="size-full">
</div>
</button>
<div class="hidden sm:flex items-center">
<button class="p-2 mr-1 group" id="volume-button">
<div class="size-6 invert group-hover:scale-110" id="volume-icon-wrapper">
<img src="../../docs/assets/volume-off-icon.svg" class="size-full">
<img src="../../docs/assets/volume-x-icon.svg" class="size-full">
<img src="../../docs/assets/volume-0-icon.svg" class="size-full">
<img src="../../docs/assets/volume-1-icon.svg" class="size-full">
<img src="../../docs/assets/volume-2-icon.svg" class="size-full">
</div>
</button>
<div class="w-16 mr-2 relative rounded-full bg-zinc-800/75 h-1.5 group" id="volume-bar-container">
<div class="absolute h-full top-0 left-0 rounded-full bg-zinc-200 group-hover:bg-white" id="volume-bar">
<div class="size-3 rounded-full bg-zinc-200 absolute top-1/2 right-0 -translate-y-1/2 translate-x-1/2 group-hover:scale-110 group-hover:bg-white group-hover:shadow"></div>
<div class="w-16 relative rounded-full bg-zinc-800/75 h-1.5 group touch-none" id="volume-bar-container">
<div class="absolute h-full top-0 left-0 rounded-full bg-zinc-200 group-hover:bg-white" id="volume-bar">
<div class="size-3 rounded-full bg-zinc-200 absolute top-1/2 right-0 -translate-y-1/2 translate-x-1/2 group-hover:scale-110 group-hover:bg-white group-hover:shadow"></div>
</div>
</div>
</div>
<p id="current-time" class="tabular-nums w-24 text-right text-sm"></p>
<p id="current-time" class="tabular-nums w-10 sm:w-24 text-right text-sm"></p>
<div class="flex-1 relative rounded-full bg-zinc-800/75 h-2 mx-4 group" id="progress-bar-container">
<div class="flex-1 relative rounded-full bg-zinc-800/75 h-2 mx-4 group touch-none" id="progress-bar-container">
<div class="absolute h-full top-0 left-0 rounded-full bg-zinc-200 group-hover:bg-white" id="progress-bar">
<div class="size-4 rounded-full bg-zinc-200 absolute top-1/2 right-0 -translate-y-1/2 translate-x-1/2 group-hover:scale-110 group-hover:bg-white group-hover:shadow"></div>
</div>
</div>
<p id="duration" class="tabular-nums w-24 text-sm"></p>
<p id="duration" class="tabular-nums w-10 sm:w-24 text-sm"></p>
<button class="p-2 group outline-none" id="fullscreen-button">
<div class="size-6 invert group-hover:scale-110">
+68 -14
View File
@@ -424,23 +424,28 @@ const updateProgressBarTime = (seconds: number) => {
progressBarContainer.addEventListener('pointerdown', (event) => {
draggingProgressBar = true;
progressBarContainer.setPointerCapture(event.pointerId);
const rect = progressBarContainer.getBoundingClientRect();
const completion = Math.max(Math.min((event.clientX - rect.left) / rect.width, 1), 0);
updateProgressBarTime(completion * totalDuration);
clearTimeout(hideControlsTimeout);
window.addEventListener('pointerup', (event) => {
draggingProgressBar = false;
progressBarContainer.releasePointerCapture(event.pointerId);
const rect = progressBarContainer.getBoundingClientRect();
const completion = Math.max(Math.min((event.clientX - rect.left) / rect.width, 1), 0);
const newTime = completion * totalDuration;
void seekToTime(newTime);
showControlsTemporarily();
}, { once: true });
});
window.addEventListener('pointermove', (event) => {
progressBarContainer.addEventListener('pointermove', (event) => {
if (draggingProgressBar) {
const rect = progressBarContainer.getBoundingClientRect();
const completion = Math.max(Math.min((event.clientX - rect.left) / rect.width, 1), 0);
@@ -465,18 +470,24 @@ const updateVolume = () => {
volumeBarContainer.addEventListener('pointerdown', (event) => {
draggingVolumeBar = true;
volumeBarContainer.setPointerCapture(event.pointerId);
const rect = volumeBarContainer.getBoundingClientRect();
volume = Math.max(Math.min((event.clientX - rect.left) / rect.width, 1), 0);
volumeMuted = false;
updateVolume();
clearTimeout(hideControlsTimeout);
window.addEventListener('pointerup', (event) => {
draggingVolumeBar = false;
volumeBarContainer.releasePointerCapture(event.pointerId);
const rect = volumeBarContainer.getBoundingClientRect();
volume = Math.max(Math.min((event.clientX - rect.left) / rect.width, 1), 0);
updateVolume();
showControlsTemporarily();
}, { once: true });
});
@@ -485,7 +496,7 @@ volumeButton.addEventListener('click', () => {
updateVolume();
});
window.addEventListener('pointermove', (event) => {
volumeBarContainer.addEventListener('pointermove', (event) => {
if (draggingVolumeBar) {
const rect = volumeBarContainer.getBoundingClientRect();
volume = Math.max(Math.min((event.clientX - rect.left) / rect.width, 1), 0);
@@ -502,26 +513,43 @@ const showControlsTemporarily = () => {
}
controlsElement.style.opacity = '1';
controlsElement.style.pointerEvents = '';
playerContainer.style.cursor = '';
clearTimeout(hideControlsTimeout);
hideControlsTimeout = window.setTimeout(() => {
controlsElement.style.opacity = '0';
if (draggingProgressBar) {
return;
}
hideControls();
playerContainer.style.cursor = 'none';
}, 2000);
};
const hideControls = () => {
controlsElement.style.opacity = '0';
controlsElement.style.pointerEvents = 'none';
};
hideControls();
let hideControlsTimeout = -1;
playerContainer.addEventListener('pointermove', () => {
showControlsTemporarily();
playerContainer.addEventListener('pointermove', (event) => {
if (event.pointerType !== 'touch') {
showControlsTemporarily();
}
});
playerContainer.addEventListener('pointerleave', () => {
playerContainer.addEventListener('pointerleave', (event) => {
if (!videoSink) {
// Shouldn't run if there's only an audio track
return;
}
controlsElement.style.opacity = '0';
if (draggingProgressBar || draggingVolumeBar || event.pointerType === 'touch') {
return;
}
hideControls();
clearTimeout(hideControlsTimeout);
});
@@ -563,17 +591,35 @@ fullscreenButton.addEventListener('click', () => {
}
});
// I'm sorry for this
const isTouchDevice = () => {
return (('ontouchstart' in window)
|| (navigator.maxTouchPoints > 0)
|| ('msMaxTouchPoints' in navigator && (navigator.msMaxTouchPoints as number) > 0));
};
playerContainer.addEventListener('click', () => {
togglePlay();
if (isTouchDevice()) {
if (controlsElement.style.opacity === '1') {
hideControls();
} else {
showControlsTemporarily();
}
} else {
togglePlay();
}
});
controlsElement.addEventListener('click', (event) => {
// Make sure this does NOT toggle play
event.stopPropagation();
showControlsTemporarily();
});
/** === UTILS === */
const formatSeconds = (seconds: number) => {
const showMilliseconds = window.innerWidth >= 640;
seconds = Math.round(seconds * 1000) / 1000; // Round to milliseconds
const hours = Math.floor(seconds / 3600);
@@ -581,11 +627,19 @@ const formatSeconds = (seconds: number) => {
const remainingSeconds = Math.floor(seconds % 60);
const millisecs = Math.floor(1000 * seconds % 1000).toString().padStart(3, '0');
let result: string;
if (hours > 0) {
return `${hours}:${minutes.toString().padStart(2, '0')}`
+ `:${remainingSeconds.toString().padStart(2, '0')}.${millisecs}`;
result = `${hours}:${minutes.toString().padStart(2, '0')}`
+ `:${remainingSeconds.toString().padStart(2, '0')}`;
} else {
result = `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
}
return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}.${millisecs}`;
if (showMilliseconds) {
result += `.${millisecs}`;
}
return result;
};
/** === FILE SELECTION LOGIC === */
@@ -608,9 +662,9 @@ selectMediaButton.addEventListener('click', () => {
loadUrlButton.addEventListener('click', () => {
const url = prompt(
'Please enter a URL of a media file. Note that it must support cross-origin requests, so have the right'
+ ' CORS headers set.',
'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
'Please enter a URL of a media file. Note that it must be HTTPS and support cross-origin requests, so have the'
+ ' right CORS headers set.',
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
);
if (!url) {
return;
@@ -176,9 +176,9 @@ selectMediaButton.addEventListener('click', () => {
loadUrlButton.addEventListener('click', () => {
const url = prompt(
'Please enter a URL of a media file. Note that it must support cross-origin requests, so have the right'
+ ' CORS headers set.',
'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
'Please enter a URL of a media file. Note that it must be HTTPS and support cross-origin requests, so have the'
+ ' right CORS headers set.',
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
);
if (!url) {
return;
@@ -128,9 +128,9 @@ selectMediaButton.addEventListener('click', () => {
loadUrlButton.addEventListener('click', () => {
const url = prompt(
'Please enter a URL of a media file. Note that it must support cross-origin requests, so have the right'
+ ' CORS headers set.',
'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
'Please enter a URL of a media file. Note that it must be HTTPS and support cross-origin requests, so have the'
+ ' right CORS headers set.',
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
);
if (!url) {
return;
+6 -6
View File
@@ -1,12 +1,12 @@
{
"name": "mediabunny",
"version": "1.12.0",
"version": "1.12.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mediabunny",
"version": "1.12.0",
"version": "1.12.1",
"license": "MPL-2.0",
"workspaces": [
"packages/*"
@@ -6147,9 +6147,9 @@
}
},
"node_modules/mediabunny": {
"version": "1.11.2",
"resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.11.2.tgz",
"integrity": "sha512-dZpaq+YMKo5dUz6HlwWtZJfKPf3rZCq9BXurC7/zVmgjB3sTW5l32VT65gWm/ojv0vUwH+WbtolkmOQX1viz5g==",
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.12.0.tgz",
"integrity": "sha512-BARnydaYDY9iCar+ZRdnaKER31c2J4otsEVcSRsizXRrFLv97LXSd5ilD5FQF52AaM6FiqLm7iB3Zl1C+a5u9w==",
"license": "MPL-2.0",
"peer": true,
"workspaces": [
@@ -9478,7 +9478,7 @@
},
"packages/mp3-encoder": {
"name": "@mediabunny/mp3-encoder",
"version": "1.12.0",
"version": "1.12.1",
"license": "MPL-2.0",
"devDependencies": {
"@types/emscripten": "^1.40.1"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "mediabunny",
"author": "Vanilagy",
"version": "1.12.0",
"version": "1.12.1",
"description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.",
"type": "module",
"workspaces": [
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@mediabunny/mp3-encoder",
"author": "Vanilagy",
"version": "1.12.0",
"version": "1.12.1",
"description": "MP3 encoder extension for Mediabunny, based on LAME.",
"main": "./dist/bundles/mediabunny-mp3-encoder.mjs",
"module": "./dist/bundles/mediabunny-mp3-encoder.mjs",
+17 -2
View File
@@ -1217,6 +1217,10 @@ class AudioDecoderWrapper extends DecoderWrapper<AudioSample> {
customDecoderCallSerializer = new CallSerializer();
customDecoderQueueSize = 0;
// Internal state to accumulate a precise current timestamp based on audio durations, not the (potentially
// inaccurate) packet timestamps.
currentTimestamp: number | null = null;
constructor(
onSample: (sample: AudioSample) => unknown,
onError: (error: DOMException) => unknown,
@@ -1226,6 +1230,17 @@ class AudioDecoderWrapper extends DecoderWrapper<AudioSample> {
super(onSample, onError);
const sampleHandler = (sample: AudioSample) => {
if (
this.currentTimestamp === null
|| Math.abs(sample.timestamp - this.currentTimestamp) >= sample.duration
) {
// We need to sync with the sample timestamp again
this.currentTimestamp = sample.timestamp;
}
const preciseTimestamp = this.currentTimestamp;
this.currentTimestamp += sample.duration;
if (sample.numberOfFrames === 0) {
// We skip zero-data (empty) AudioSamples. These are sometimes emitted, for example, by Firefox when it
// decodes Vorbis (at the start).
@@ -1235,7 +1250,7 @@ class AudioDecoderWrapper extends DecoderWrapper<AudioSample> {
// Round the timestamp to the sample rate
const sampleRate = decoderConfig.sampleRate;
sample.setTimestamp(Math.round(sample.timestamp * sampleRate) / sampleRate);
sample.setTimestamp(Math.round(preciseTimestamp * sampleRate) / sampleRate);
onSample(sample);
};
@@ -1320,7 +1335,7 @@ class PcmAudioDecoderWrapper extends DecoderWrapper<AudioSample> {
writeOutputValue: (view: DataView, byteOffset: number, value: number) => void;
// Internal state to accumulate a precise current timestamp based on audio durations, not the (potentially
// inaccurate) sample timestamps.
// inaccurate) packet timestamps.
currentTimestamp: number | null = null;
constructor(
+1
View File
@@ -28,6 +28,7 @@ export default defineConfig({
],
server: {
hmr: false,
allowedHosts: true,
},
build: {
outDir: 'dist-docs', // Build them directly into the docs build folder