Merge branch 'main' into hls

This commit is contained in:
Vanilagy
2026-03-13 17:07:31 +01:00
17 changed files with 105 additions and 45 deletions
+1 -1
View File
@@ -223,7 +223,7 @@
start, start,
end: start + 5, end: start + 5,
//start: 0, //start: 0,
//end: 10 end: 10
}, },
}); });
console.log(conversion); console.log(conversion);
+4 -4
View File
@@ -19,7 +19,7 @@
button.addEventListener('click', async () => { button.addEventListener('click', async () => {
const stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: false }); const stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: false });
const videoTrack = stream.getVideoTracks()[0]; const videoTrack = stream.getVideoTracks()[0];
const audioTrack = null;//stream.getAudioTracks()[0]; const audioTrack = stream.getAudioTracks()[0];
const output = new Mediabunny.Output({ const output = new Mediabunny.Output({
target: new Mediabunny.BufferTarget(), target: new Mediabunny.BufferTarget(),
@@ -29,7 +29,7 @@
let audioSource = null; let audioSource = null;
if (videoTrack) { if (videoTrack) {
videoSource = new Mediabunny.MediaStreamVideoTrackSource(videoTrack, { videoSource = new Mediabunny.MediaStreamVideoTrackSource(videoTrack, {
codec: 'avc', codec: 'vp9',
bitrate: Mediabunny.QUALITY_MEDIUM, bitrate: Mediabunny.QUALITY_MEDIUM,
sizeChangeBehavior: 'passThrough', sizeChangeBehavior: 'passThrough',
}); });
@@ -40,13 +40,13 @@
} }
if (audioTrack) { if (audioTrack) {
audioSource = new Mediabunny.MediaStreamAudioTrackSource(audioTrack, { audioSource = new Mediabunny.MediaStreamAudioTrackSource(audioTrack, {
codec: 'mp3', codec: 'opus',
bitrate: Mediabunny.QUALITY_MEDIUM bitrate: Mediabunny.QUALITY_MEDIUM
}); });
audioSource.errorPromise.catch((d) => console.log("Hello!!???", d)); audioSource.errorPromise.catch((d) => console.log("Hello!!???", d));
//output.addAudioTrack(audioSource); output.addAudioTrack(audioSource);
} }
await output.start(); await output.start();
+1
View File
@@ -17,6 +17,7 @@ export default withMermaid({
sitemap: { sitemap: {
hostname: 'https://mediabunny.dev', hostname: 'https://mediabunny.dev',
}, },
lastUpdated: true,
head: [ head: [
['link', { rel: 'icon', type: 'image/png', href: '/mediabunny-logo.png' }], ['link', { rel: 'icon', type: 'image/png', href: '/mediabunny-logo.png' }],
['link', { rel: 'icon', type: 'image/svg+xml', href: '/mediabunny-logo.svg' }], ['link', { rel: 'icon', type: 'image/svg+xml', href: '/mediabunny-logo.svg' }],
+5 -1
View File
@@ -97,9 +97,13 @@ await sink.getKeyPacket(5); // => EncodedPacket | null
When retrieving a packet using a timestamp, the last packet (in [presentation order](#decode-vs-presentation-order)) with a timestamp less than or equal to the search timestamp will be returned. The methods return `null` if there exists no such packet. When retrieving a packet using a timestamp, the last packet (in [presentation order](#decode-vs-presentation-order)) with a timestamp less than or equal to the search timestamp will be returned. The methods return `null` if there exists no such packet.
There is a special method for retrieving the first packet (in [decode order](#decode-vs-presentation-order)): There are special methods for retrieving the first packet (in [decode order](#decode-vs-presentation-order)):
```ts ```ts
await sink.getFirstPacket(); // => EncodedPacket | null await sink.getFirstPacket(); // => EncodedPacket | null
// The first packet is typically a key frame, but this is not required.
// This method returns the first key frame:
await sink.getFirstKeyPacket(); // => EncodedPacket | null
``` ```
The last packet (in [presentation order](#decode-vs-presentation-order)) can be retrieved like so: The last packet (in [presentation order](#decode-vs-presentation-order)) can be retrieved like so:
```ts ```ts
+4
View File
@@ -0,0 +1,4 @@
User-agent: *
Allow: /
Sitemap: https://mediabunny.dev/sitemap.xml
+9 -9
View File
@@ -1,12 +1,12 @@
{ {
"name": "mediabunny", "name": "mediabunny",
"version": "1.38.0", "version": "1.39.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mediabunny", "name": "mediabunny",
"version": "1.38.0", "version": "1.39.2",
"license": "MPL-2.0", "license": "MPL-2.0",
"workspaces": [ "workspaces": [
"packages/*" "packages/*"
@@ -7751,9 +7751,9 @@
} }
}, },
"node_modules/mediabunny": { "node_modules/mediabunny": {
"version": "1.37.0", "version": "1.39.1",
"resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.37.0.tgz", "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.39.1.tgz",
"integrity": "sha512-eV7M9IJ29pr/8RNL1sYtIxNbdMfDMN1hMwMaOFfNLhwuKKGSC+eKwiJFpdVjEJ3zrMA4LGerF4Hps0SENFSAlg==", "integrity": "sha512-gaQKDWtfLhgSN9tRj37lgznHf9MOvey7sbT6GdQA7mWl1kaE5R+P1lfl9CDsgtzongx12IG9X/y2vgZmTHzGDg==",
"license": "MPL-2.0", "license": "MPL-2.0",
"peer": true, "peer": true,
"workspaces": [ "workspaces": [
@@ -12077,7 +12077,7 @@
}, },
"packages/aac-encoder": { "packages/aac-encoder": {
"name": "@mediabunny/aac-encoder", "name": "@mediabunny/aac-encoder",
"version": "1.38.0", "version": "1.39.2",
"license": "MPL-2.0", "license": "MPL-2.0",
"devDependencies": { "devDependencies": {
"@types/emscripten": "^1.40.1" "@types/emscripten": "^1.40.1"
@@ -12092,7 +12092,7 @@
}, },
"packages/ac3": { "packages/ac3": {
"name": "@mediabunny/ac3", "name": "@mediabunny/ac3",
"version": "1.38.0", "version": "1.39.2",
"license": "MPL-2.0", "license": "MPL-2.0",
"devDependencies": { "devDependencies": {
"@types/emscripten": "^1.40.1" "@types/emscripten": "^1.40.1"
@@ -12107,7 +12107,7 @@
}, },
"packages/flac-encoder": { "packages/flac-encoder": {
"name": "@mediabunny/flac-encoder", "name": "@mediabunny/flac-encoder",
"version": "1.38.0", "version": "1.39.2",
"license": "MPL-2.0", "license": "MPL-2.0",
"devDependencies": { "devDependencies": {
"@types/emscripten": "^1.40.1" "@types/emscripten": "^1.40.1"
@@ -12122,7 +12122,7 @@
}, },
"packages/mp3-encoder": { "packages/mp3-encoder": {
"name": "@mediabunny/mp3-encoder", "name": "@mediabunny/mp3-encoder",
"version": "1.38.0", "version": "1.39.2",
"license": "MPL-2.0", "license": "MPL-2.0",
"devDependencies": { "devDependencies": {
"@types/emscripten": "^1.40.1" "@types/emscripten": "^1.40.1"
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "mediabunny", "name": "mediabunny",
"author": "Vanilagy", "author": "Vanilagy",
"version": "1.38.0", "version": "1.39.2",
"description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.", "description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.",
"type": "module", "type": "module",
"workspaces": [ "workspaces": [
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@mediabunny/aac-encoder", "name": "@mediabunny/aac-encoder",
"author": "Vanilagy", "author": "Vanilagy",
"version": "1.38.0", "version": "1.39.2",
"description": "AAC encoder extension for Mediabunny, based on FFmpeg.", "description": "AAC encoder extension for Mediabunny, based on FFmpeg.",
"main": "./dist/bundles/mediabunny-aac-encoder.mjs", "main": "./dist/bundles/mediabunny-aac-encoder.mjs",
"module": "./dist/bundles/mediabunny-aac-encoder.mjs", "module": "./dist/bundles/mediabunny-aac-encoder.mjs",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@mediabunny/ac3", "name": "@mediabunny/ac3",
"author": "Vanilagy", "author": "Vanilagy",
"version": "1.38.0", "version": "1.39.2",
"description": "AC-3 and E-AC-3 (Dolby Digital) decoder and encoder extension for Mediabunny, based on FFmpeg.", "description": "AC-3 and E-AC-3 (Dolby Digital) decoder and encoder extension for Mediabunny, based on FFmpeg.",
"main": "./dist/bundles/mediabunny-ac3.mjs", "main": "./dist/bundles/mediabunny-ac3.mjs",
"module": "./dist/bundles/mediabunny-ac3.mjs", "module": "./dist/bundles/mediabunny-ac3.mjs",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@mediabunny/flac-encoder", "name": "@mediabunny/flac-encoder",
"author": "Vanilagy", "author": "Vanilagy",
"version": "1.38.0", "version": "1.39.2",
"description": "FLAC encoder extension for Mediabunny, based on libFLAC.", "description": "FLAC encoder extension for Mediabunny, based on libFLAC.",
"main": "./dist/bundles/mediabunny-flac-encoder.mjs", "main": "./dist/bundles/mediabunny-flac-encoder.mjs",
"module": "./dist/bundles/mediabunny-flac-encoder.mjs", "module": "./dist/bundles/mediabunny-flac-encoder.mjs",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@mediabunny/mp3-encoder", "name": "@mediabunny/mp3-encoder",
"author": "Vanilagy", "author": "Vanilagy",
"version": "1.38.0", "version": "1.39.2",
"description": "MP3 encoder extension for Mediabunny, based on LAME.", "description": "MP3 encoder extension for Mediabunny, based on LAME.",
"main": "./dist/bundles/mediabunny-mp3-encoder.mjs", "main": "./dist/bundles/mediabunny-mp3-encoder.mjs",
"module": "./dist/bundles/mediabunny-mp3-encoder.mjs", "module": "./dist/bundles/mediabunny-mp3-encoder.mjs",
+3
View File
@@ -515,6 +515,9 @@ export class Conversion {
/** /**
* Whether this conversion, as it has been configured, is valid and can be executed. If this field is `false`, check * Whether this conversion, as it has been configured, is valid and can be executed. If this field is `false`, check
* the `discardedTracks` field for reasons. * the `discardedTracks` field for reasons.
*
* Note: a conversion having discarded tracks does not automatically mean it is invalid; if the remaining, utilized
* tracks make for a valid output file, the conversion is still allowed.
*/ */
isValid = false; isValid = false;
/** The list of tracks that are included in the output file. */ /** The list of tracks that are included in the output file. */
+1 -1
View File
@@ -542,7 +542,7 @@ export class MpegTsInputFormat extends InputFormat {
} else if (bytes[0] === 0x47 && bytes[TS_PACKET_SIZE + 16] === 0x47) { } else if (bytes[0] === 0x47 && bytes[TS_PACKET_SIZE + 16] === 0x47) {
// MPEG-TS with Forward Error Correction // MPEG-TS with Forward Error Correction
return true; return true;
} else if (bytes[4] === 0x47 && bytes[4 + TS_PACKET_SIZE] === 0x47) { } else if (bytes[4] === 0x47 && bytes[4 + TS_PACKET_SIZE + 4] === 0x47) {
// MPEG-2-TS (DVHS) // MPEG-2-TS (DVHS)
return true; return true;
} }
+1 -1
View File
@@ -529,7 +529,7 @@ export abstract class BaseMediaSampleSink<
const packetSink = this._createPacketSink(); const packetSink = this._createPacketSink();
const keyPacket = await packetSink.getKeyPacket(startTimestamp, { verifyKeyPackets: true }) const keyPacket = await packetSink.getKeyPacket(startTimestamp, { verifyKeyPackets: true })
?? await packetSink.getFirstKeyPacket(); ?? await packetSink.getFirstKeyPacket({ verifyKeyPackets: true });
let currentPacket: EncodedPacket | null = keyPacket; let currentPacket: EncodedPacket | null = keyPacket;
+48 -3
View File
@@ -1076,6 +1076,8 @@ export class MediaStreamVideoTrackSource extends VideoSource {
private _lastVideoFrame: VideoFrame | null = null; private _lastVideoFrame: VideoFrame | null = null;
/** @internal */ /** @internal */
private _timerHandle: UnthrottledTimerHandle | null = null; private _timerHandle: UnthrottledTimerHandle | null = null;
/** @internal */
private _videoElement: HTMLVideoElement | null = null;
/** A promise that rejects upon any error within this source. This promise never resolves. */ /** A promise that rejects upon any error within this source. This promise never resolves. */
get errorPromise() { get errorPromise() {
@@ -1160,12 +1162,13 @@ export class MediaStreamVideoTrackSource extends VideoSource {
while (now - lastFrameTime > 1000 / frameRate) { while (now - lastFrameTime > 1000 / frameRate) {
lastFrameTime += 1000 / frameRate; lastFrameTime += 1000 / frameRate;
const timestamp = firstVideoFrameTimestamp + frameCount / frameRate; const timestamp = firstVideoFrameTimestamp + frameCount / frameRate;
const clone = new VideoFrame(this._lastVideoFrame, {
const frame = new VideoFrame(this._videoElement ?? this._lastVideoFrame, {
timestamp: 1e6 * timestamp, timestamp: 1e6 * timestamp,
duration: 1e6 / frameRate, duration: 1e6 / frameRate,
}); });
addVideoFrame(clone, now); addVideoFrame(frame, now);
} }
}; };
@@ -1298,8 +1301,44 @@ export class MediaStreamVideoTrackSource extends VideoSource {
}; };
mediaStreamTrackProcessorWorker!.addEventListener('message', this._workerListener); mediaStreamTrackProcessorWorker!.addEventListener('message', this._workerListener);
} else if (frameRate !== null) {
// No MediaStreamTrackProcessor support at all (e.g. Firefox), but we have a frame rate, so we can
// manually sample from a hidden <video> element instead.
const video = document.createElement('video');
video.style.position = 'fixed';
video.style.left = '-10000px';
video.style.top = '-10000px';
video.style.width = '1px';
video.style.height = '1px';
video.style.opacity = '0';
video.style.pointerEvents = 'none';
video.muted = true;
video.srcObject = new MediaStream([this._track]);
document.body.appendChild(video);
this._videoElement = video;
video.addEventListener('loadeddata', () => {
if (errored || !this._videoElement) {
return;
}
// This will be the first frame
const frame = new VideoFrame(video, {
timestamp: 1000 * performance.now(),
});
onVideoFrame(frame);
frame.close();
}, { once: true });
void video.play().catch((error) => {
errored = true;
this._promiseWithResolvers.reject(error);
});
} else { } else {
throw new Error('MediaStreamTrackProcessor is required but not supported by this browser.'); throw new Error(
'When no explicit frame rate is set, MediaStreamTrackProcessor is required; but it\'s not supported'
+ ' by this browser.',
);
} }
} }
} }
@@ -1329,6 +1368,12 @@ export class MediaStreamVideoTrackSource extends VideoSource {
} }
this._lastVideoFrame?.close(); this._lastVideoFrame?.close();
if (this._videoElement) {
this._videoElement.srcObject = null;
this._videoElement.remove();
this._videoElement = null;
}
if (this._workerTrackId !== null) { if (this._workerTrackId !== null) {
assert(this._workerListener); assert(this._workerListener);
+2 -2
View File
@@ -175,10 +175,10 @@ export class MpegTsDemuxer extends Demuxer {
// MPEG-TS with Forward Error Correction // MPEG-TS with Forward Error Correction
this.packetOffset = 0; this.packetOffset = 0;
this.packetStride = TS_PACKET_SIZE + 16; this.packetStride = TS_PACKET_SIZE + 16;
} else if (startingBytes[4] === 0x47 && startingBytes[4 + TS_PACKET_SIZE] === 0x47) { } else if (startingBytes[4] === 0x47 && startingBytes[4 + TS_PACKET_SIZE + 4] === 0x47) {
// MPEG-2-TS (DVHS) // MPEG-2-TS (DVHS)
this.packetOffset = 4; this.packetOffset = 4;
this.packetStride = TS_PACKET_SIZE; this.packetStride = TS_PACKET_SIZE + 4;
} else { } else {
throw new Error('Unreachable.'); throw new Error('Unreachable.');
} }
+21 -18
View File
@@ -46,12 +46,16 @@ export abstract class Muxer {
private trackTimestampInfo = new WeakMap<OutputTrack, { private trackTimestampInfo = new WeakMap<OutputTrack, {
maxTimestamp: number; maxTimestamp: number;
maxTimestampBeforeLastKeyPacket: number; maxTimestampBeforeLastKeyPacket: number | null;
}>(); }>();
protected validateAndNormalizeTimestamp(track: OutputTrack, timestampInSeconds: number, isKeyPacket: boolean) { protected validateAndNormalizeTimestamp(track: OutputTrack, timestampInSeconds: number, isKeyPacket: boolean) {
timestampInSeconds += track.source._timestampOffset; timestampInSeconds += track.source._timestampOffset;
if (timestampInSeconds < 0) {
throw new Error(`Timestamps must be non-negative (got ${timestampInSeconds}s).`);
}
let timestampInfo = this.trackTimestampInfo.get(track); let timestampInfo = this.trackTimestampInfo.get(track);
if (!timestampInfo) { if (!timestampInfo) {
if (!isKeyPacket) { if (!isKeyPacket) {
@@ -60,29 +64,28 @@ export abstract class Muxer {
timestampInfo = { timestampInfo = {
maxTimestamp: timestampInSeconds, maxTimestamp: timestampInSeconds,
maxTimestampBeforeLastKeyPacket: timestampInSeconds, maxTimestampBeforeLastKeyPacket: null,
}; };
this.trackTimestampInfo.set(track, timestampInfo); this.trackTimestampInfo.set(track, timestampInfo);
} } else {
if (isKeyPacket) {
timestampInfo.maxTimestampBeforeLastKeyPacket = timestampInfo.maxTimestamp;
}
if (timestampInSeconds < 0) { if (
throw new Error(`Timestamps must be non-negative (got ${timestampInSeconds}s).`); timestampInfo.maxTimestampBeforeLastKeyPacket !== null
} && timestampInSeconds < timestampInfo.maxTimestampBeforeLastKeyPacket
) {
throw new Error(
`Timestamps cannot be smaller than the largest timestamp of the previous GOP (a GOP begins with a`
+ ` key packet and ends right before the next key packet). Got ${timestampInSeconds}s, but largest`
+ ` timestamp is ${timestampInfo.maxTimestampBeforeLastKeyPacket}s.`,
);
}
if (isKeyPacket) { timestampInfo.maxTimestamp = Math.max(timestampInfo.maxTimestamp, timestampInSeconds);
timestampInfo.maxTimestampBeforeLastKeyPacket = timestampInfo.maxTimestamp;
} }
if (timestampInSeconds < timestampInfo.maxTimestampBeforeLastKeyPacket) {
throw new Error(
`Timestamps cannot be smaller than the largest timestamp of the previous GOP (a GOP begins with a key`
+ ` packet and ends right before the next key packet). Got ${timestampInSeconds}s, but largest`
+ ` timestamp is ${timestampInfo.maxTimestampBeforeLastKeyPacket}s.`,
);
}
timestampInfo.maxTimestamp = Math.max(timestampInfo.maxTimestamp, timestampInSeconds);
return timestampInSeconds; return timestampInSeconds;
} }
} }