diff --git a/dev/mux.html b/dev/mux.html
index f950c26..7d05a50 100644
--- a/dev/mux.html
+++ b/dev/mux.html
@@ -42,6 +42,7 @@
format = new Metamuxer.Mp4OutputFormat({ fastStart: 'fragmented' }); // new Metamuxer.MkvOutputFormat();// new Metamuxer.Mp4OutputFormat({ fastStart: false });
format = new Metamuxer.OggOutputFormat();
format = new Metamuxer.Mp4OutputFormat();
+ format = new Metamuxer.WebMOutputFormat();
let target = new Metamuxer.BufferTarget();
/*
@@ -85,10 +86,12 @@
download(new Blob([target.buffer]), 'test.mkv');
*/
+ /*
let videoSource = new Metamuxer.CanvasSource(canvas, {
codec: 'av1',
bitrate: 1e6
- });
+ });*/
+ let videoSource = new Metamuxer.EncodedVideoPacketSource('av1');
let audioSource = new Metamuxer.AudioBufferSource({
codec: 'opus',
bitrate: 128e3,
@@ -159,12 +162,23 @@ Testing... <00:17.350>One... <00:18.125>Two...
//subtitleSource.add(simpleWebvttFile);
//subtitleSource.close();
+ /*
for (let i = 0; i < 100; i++) {
context.fillStyle = ['red', 'green', 'blue', 'yellow'][i % 4];
context.fillRect(canvas.width * Math.random(), canvas.height * Math.random(), canvas.width * Math.random(), canvas.height * Math.random());
await videoSource.add(i / 10, 1 / 10);
}
+ */
+ for (let i = 0; i < 40; i += 0.1) {
+ await videoSource.add(new Metamuxer.EncodedPacket(new Uint8Array(1234), i === 0 ? 'key' : 'delta', i, 0.1), {
+ decoderConfig: {
+ codec: 'vp09.00.10.08',
+ codedWidth: 64,
+ codedHeight: 64
+ }
+ });
+ }
let audioContext = new AudioContext();
let audioBuffer = await audioContext.decodeAudioData(await (await fetch('./CantinaBand60.wav')).arrayBuffer());
@@ -176,5 +190,5 @@ Testing... <00:17.350>One... <00:18.125>Two...
await output.finalize();
console.log(target);
- download(new Blob([target.buffer]), 'test' + format.getFileExtension());
+ download(new Blob([target.buffer]), 'test' + format.fileExtension);
\ No newline at end of file
diff --git a/src/isobmff/isobmff-demuxer.ts b/src/isobmff/isobmff-demuxer.ts
index 3e5bdd5..7dc3a9a 100644
--- a/src/isobmff/isobmff-demuxer.ts
+++ b/src/isobmff/isobmff-demuxer.ts
@@ -3,6 +3,7 @@ import {
AudioCodec,
Av1CodecInfo,
extractAudioCodecString,
+ extractAv1CodecInfoFromFrame,
extractVideoCodecString,
extractVp9CodecInfoFromFrame,
MediaCodec,
@@ -2349,6 +2350,9 @@ class IsobmffVideoTrackBacking extends IsobmffTrackBacking implements InputVideo
if (this.internalTrack.info.codec === 'vp9' && !this.internalTrack.info.vp9CodecInfo) {
const firstPacket = await this.getFirstPacket({});
this.internalTrack.info.vp9CodecInfo = firstPacket && extractVp9CodecInfoFromFrame(firstPacket.data);
+ } else if (this.internalTrack.info.codec === 'av1' && !this.internalTrack.info.av1CodecInfo) {
+ const firstPacket = await this.getFirstPacket({});
+ this.internalTrack.info.av1CodecInfo = firstPacket && extractAv1CodecInfoFromFrame(firstPacket.data);
}
return {
diff --git a/src/matroska/matroska-muxer.ts b/src/matroska/matroska-muxer.ts
index f6d9a08..14db5ca 100644
--- a/src/matroska/matroska-muxer.ts
+++ b/src/matroska/matroska-muxer.ts
@@ -46,7 +46,8 @@ import { Muxer } from '../muxer';
import { Writer } from '../writer';
import { EncodedPacket } from '../packet';
-const MAX_CHUNK_LENGTH_MS = 2 ** 15;
+const MIN_CLUSTER_TIMESTAMP_MS = -(2 ** 15);
+const MAX_CLUSTER_TIMESTAMP_MS = 2 ** 15 - 1;
const APP_NAME = 'https://github.com/Vanilagy/webm-muxer'; // TODO
const SEGMENT_SIZE_BYTES = 6;
const CLUSTER_SIZE_BYTES = 5;
@@ -661,7 +662,8 @@ export class MatroskaMuxer extends Muxer {
}
const msTimestamp = Math.round(1000 * chunk.timestamp);
- // We can only finalize this cluster (and begin a new one) if we know that each track will be able to
+
+ // We wanna only finalize this cluster (and begin a new one) if we know that each track will be able to
// start the new one with a key frame.
const keyFrameQueuedEverywhere = this.trackDatas.every((otherTrackData) => {
if (trackData === otherTrackData) {
@@ -676,35 +678,39 @@ export class MatroskaMuxer extends Muxer {
return otherTrackData.track.source._closed;
});
- if (
- !this.currentCluster
- || (
+ let shouldCreateNewCluster = false;
+ if (!this.currentCluster) {
+ shouldCreateNewCluster = true;
+ } else {
+ assert(this.currentClusterStartMsTimestamp !== null);
+ assert(this.currentClusterMaxMsTimestamp !== null);
+
+ const relativeTimestamp = msTimestamp - this.currentClusterStartMsTimestamp;
+
+ shouldCreateNewCluster = (
keyFrameQueuedEverywhere
// This check is required because that means there is already a block with this timestamp in the
- // CURRENT chunk, meaning that starting the next cluster at the same timestamp is forbidden (since the
- // already-written block would belong into it instead).
- && msTimestamp > this.currentClusterMaxMsTimestamp!
- && msTimestamp - this.currentClusterStartMsTimestamp! >= 1000
+ // CURRENT chunk, meaning that starting the next cluster at the same timestamp is forbidden (since
+ // the already-written block would belong into it instead).
+ && msTimestamp > this.currentClusterMaxMsTimestamp
+ && relativeTimestamp >= 1000
)
- ) {
+ // The cluster would exceed its maximum allowed length. This puts us in an unfortunate position and forces
+ // us to begin the next cluster with a delta frame. Although this is undesirable, it is not forbidden by the
+ // spec and is supported by players.
+ || relativeTimestamp > MAX_CLUSTER_TIMESTAMP_MS;
+ }
+
+ if (shouldCreateNewCluster) {
this.createNewCluster(msTimestamp);
}
const relativeTimestamp = msTimestamp - this.currentClusterStartMsTimestamp!;
- if (relativeTimestamp < -(2 ** 15)) {
+ if (relativeTimestamp < MIN_CLUSTER_TIMESTAMP_MS) {
// The block lies too far in the past, it's not representable within this cluster
return;
}
- const clusterIsTooLong = relativeTimestamp >= MAX_CHUNK_LENGTH_MS;
- if (clusterIsTooLong) {
- throw new Error(
- `Current Matroska cluster exceeded its maximum allowed length of ${MAX_CHUNK_LENGTH_MS} `
- + `milliseconds. In order to produce a correct WebM file, you must pass in a key frame at least every `
- + `${MAX_CHUNK_LENGTH_MS} milliseconds.`,
- );
- }
-
const prelude = new Uint8Array(4);
const view = new DataView(prelude.buffer);
// 0x80 to indicate it's the last byte of a multi-byte number
diff --git a/src/media-source.ts b/src/media-source.ts
index ee4096f..fbd600e 100644
--- a/src/media-source.ts
+++ b/src/media-source.ts
@@ -264,7 +264,7 @@ class VideoEncoderWrapper {
// Ensure a key frame every keyFrameInterval seconds. It is important that all video tracks follow the same
// "key frame" rhythm, because aligned key frames are required to start new fragments in ISOBMFF or clusters
- // in Matroska.
+ // in Matroska (or at least desirable).
const finalEncodeOptions = {
...encodeOptions,
keyFrame: encodeOptions?.keyFrame
diff --git a/todo.txt b/todo.txt
index 07318e7..abc3ac6 100644
--- a/todo.txt
+++ b/todo.txt
@@ -3,4 +3,5 @@
- is this fixed? https://github.com/Vanilagy/webm-muxer/issues/50
- cross-track offset for streaming sources
- configurable fragmented mp4 fragment size, like the mp4-muxer PR
-- textsubtitlesource, chunked piping
\ No newline at end of file
+- textsubtitlesource, chunked piping
+- for matroska demuxing, don't blindly pipe codecPrivate into description. This should be if-else'd on a per-codec basis, for the codecs that actually need it
\ No newline at end of file