diff --git a/dev/demux.html b/dev/demux.html
index c0c8346..8d02ae1 100644
--- a/dev/demux.html
+++ b/dev/demux.html
@@ -14,12 +14,18 @@
source: new Mediabunny.BlobSource(file),
});
+ console.log(await input.getTracks());
+
+ /*
+ return;
+
const audioTrack = await input.getPrimaryAudioTrack();
const sink = new Mediabunny.EncodedPacketSink(audioTrack);
for await (const packet of sink.packets()) {
console.log(packet.timestamp, packet.duration, packet.type)
}
+ */
/*
const audioTrack = await input.getPrimaryAudioTrack();
diff --git a/docs/guide/reading-media-files.md b/docs/guide/reading-media-files.md
index 8457088..dab7aec 100644
--- a/docs/guide/reading-media-files.md
+++ b/docs/guide/reading-media-files.md
@@ -106,6 +106,10 @@ track.languageCode; // => string
// A user-defined name for this track.
track.name; // => string
+
+// Information about the intended usage of the track
+// (default, commentary, hearing-impaired, visually-impaired, etc.)
+track.disposition; // TrackDisposition
```
#### Codec information
diff --git a/docs/guide/writing-media-files.md b/docs/guide/writing-media-files.md
index c9ef8ed..6882eb4 100644
--- a/docs/guide/writing-media-files.md
+++ b/docs/guide/writing-media-files.md
@@ -56,6 +56,7 @@ output.addVideoTrack(videoSource, {
output.addAudioTrack(audioSourceEng, {
language: 'eng', // ISO 639-2/T language code
name: 'Developer Commentary', // Sets a user-defined track name
+ disposition: { commentary: true }, // Sets additional flags in the file
});
output.addAudioTrack(audioSourceGer, {
language: 'ger',
diff --git a/src/adts/adts-demuxer.ts b/src/adts/adts-demuxer.ts
index 209365e..3295a9a 100644
--- a/src/adts/adts-demuxer.ts
+++ b/src/adts/adts-demuxer.ts
@@ -21,6 +21,7 @@ import {
} from '../misc';
import { EncodedPacket, PLACEHOLDER_DATA } from '../packet';
import { readBytes, Reader } from '../reader';
+import { DEFAULT_TRACK_DISPOSITION } from '../metadata';
import { FrameHeader, MAX_FRAME_HEADER_SIZE, MIN_FRAME_HEADER_SIZE, readFrameHeader } from './adts-reader';
const SAMPLES_PER_AAC_FRAME = 1024;
@@ -188,6 +189,12 @@ class AdtsAudioTrackBacking implements InputAudioTrackBacking {
return sampleRate;
}
+ getDisposition() {
+ return {
+ ...DEFAULT_TRACK_DISPOSITION,
+ };
+ }
+
async getDecoderConfig(): Promise {
assert(this.demuxer.firstFrameHeader);
diff --git a/src/codec-data.ts b/src/codec-data.ts
index 13888f2..eef04ec 100644
--- a/src/codec-data.ts
+++ b/src/codec-data.ts
@@ -24,7 +24,7 @@ import {
toUint8Array,
} from './misc';
import { PacketType } from './packet';
-import { MetadataTags } from './tags';
+import { MetadataTags } from './metadata';
// References for AVC/HEVC code:
// ISO 14496-15
diff --git a/src/conversion.ts b/src/conversion.ts
index 2771f7c..33bbd4d 100644
--- a/src/conversion.ts
+++ b/src/conversion.ts
@@ -48,7 +48,7 @@ import {
import { Output, TrackType } from './output';
import { Mp4OutputFormat } from './output-format';
import { AudioSample, clampCropRectangle, validateCropRectangle, VideoSample } from './sample';
-import { MetadataTags, validateMetadataTags } from './tags';
+import { MetadataTags, validateMetadataTags } from './metadata';
import { NullTarget } from './target';
/**
@@ -1184,9 +1184,10 @@ export class Conversion {
this.output.addVideoTrack(videoSource, {
frameRate: trackOptions.frameRate,
- // TEMP: This condition can be removed when all demuxers properly homogenize to BCP47 in v2
+ // TODO: This condition can be removed when all demuxers properly homogenize to BCP47 in v2
languageCode: isIso639Dash2LanguageCode(track.languageCode) ? track.languageCode : undefined,
name: track.name ?? undefined,
+ disposition: track.disposition,
rotation: needsRerender ? 0 : totalRotation, // Rerendering will bake the rotation into the output
});
this._addedCounts.video++;
@@ -1423,9 +1424,10 @@ export class Conversion {
}
this.output.addAudioTrack(audioSource, {
- // TEMP: This condition can be removed when all demuxers properly homogenize to BCP47 in v2
+ // TODO: This condition can be removed when all demuxers properly homogenize to BCP47 in v2
languageCode: isIso639Dash2LanguageCode(track.languageCode) ? track.languageCode : undefined,
name: track.name ?? undefined,
+ disposition: track.disposition,
});
this._addedCounts.audio++;
this._totalTrackCount++;
diff --git a/src/demuxer.ts b/src/demuxer.ts
index f654dab..5b53fb2 100644
--- a/src/demuxer.ts
+++ b/src/demuxer.ts
@@ -8,7 +8,7 @@
import { Input } from './input';
import { InputTrack } from './input-track';
-import { MetadataTags } from './tags';
+import { MetadataTags } from './metadata';
export abstract class Demuxer {
input: Input;
diff --git a/src/flac/flac-demuxer.ts b/src/flac/flac-demuxer.ts
index 4303fd4..e362f44 100644
--- a/src/flac/flac-demuxer.ts
+++ b/src/flac/flac-demuxer.ts
@@ -28,7 +28,7 @@ import {
readU32Be,
readU8,
} from '../reader';
-import { MetadataTags } from '../tags';
+import { DEFAULT_TRACK_DISPOSITION, MetadataTags } from '../metadata';
import {
calculateCrc8,
readBlockSize,
@@ -565,6 +565,12 @@ class FlacAudioTrackBacking implements InputAudioTrackBacking {
return this.demuxer.audioInfo.sampleRate;
}
+ getDisposition() {
+ return {
+ ...DEFAULT_TRACK_DISPOSITION,
+ };
+ }
+
async getFirstTimestamp() {
return 0;
}
diff --git a/src/flac/flac-muxer.ts b/src/flac/flac-muxer.ts
index 782234a..b444f0b 100644
--- a/src/flac/flac-muxer.ts
+++ b/src/flac/flac-muxer.ts
@@ -20,7 +20,7 @@ import { Output, OutputAudioTrack } from '../output';
import { FlacOutputFormat } from '../output-format';
import { EncodedPacket } from '../packet';
import { FileSlice, readBytes } from '../reader';
-import { AttachedImage, metadataTagsAreEmpty } from '../tags';
+import { AttachedImage, metadataTagsAreEmpty } from '../metadata';
import { Writer } from '../writer';
import {
readBlockSize,
diff --git a/src/id3.ts b/src/id3.ts
index f231176..15db50b 100644
--- a/src/id3.ts
+++ b/src/id3.ts
@@ -7,7 +7,7 @@
*/
import { decodeSynchsafe, encodeSynchsafe } from '../shared/mp3-misc';
-import { MetadataTags } from './tags';
+import { MetadataTags } from './metadata';
import {
coalesceIndex,
textDecoder,
diff --git a/src/index.ts b/src/index.ts
index e3abe7a..f5dbdf1 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -202,6 +202,7 @@ export {
AttachedImage,
RichImageData,
AttachedFile,
-} from './tags';
+ TrackDisposition,
+} from './metadata';
// 🐡🦔
diff --git a/src/input-track.ts b/src/input-track.ts
index 25b5100..8c137fc 100644
--- a/src/input-track.ts
+++ b/src/input-track.ts
@@ -14,6 +14,7 @@ import { EncodedPacketSink, PacketRetrievalOptions } from './media-sink';
import { assert, Rotation } from './misc';
import { TrackType } from './output';
import { EncodedPacket, PacketType } from './packet';
+import { TrackDisposition } from './metadata';
/**
* Contains aggregate statistics about the encoded packets of a track.
@@ -36,6 +37,7 @@ export interface InputTrackBacking {
getName(): string | null;
getLanguageCode(): string;
getTimeResolution(): number;
+ getDisposition(): TrackDisposition;
getFirstTimestamp(): Promise;
computeDuration(): Promise;
@@ -128,6 +130,11 @@ export abstract class InputTrack {
return this._backing.getTimeResolution();
}
+ /** The track's disposition, i.e. information about its intended usage. */
+ get disposition() {
+ return this._backing.getDisposition();
+ }
+
/**
* Returns the start timestamp of the first packet of this track, in seconds. While often near zero, this value
* may be positive or even negative. A negative starting timestamp means the track's timing has been offset. Samples
diff --git a/src/isobmff/isobmff-boxes.ts b/src/isobmff/isobmff-boxes.ts
index f2fccc4..b69ac5e 100644
--- a/src/isobmff/isobmff-boxes.ts
+++ b/src/isobmff/isobmff-boxes.ts
@@ -44,7 +44,7 @@ import {
Sample,
} from './isobmff-muxer';
import { parseOpusIdentificationHeader } from '../codec-data';
-import { MetadataTags, RichImageData } from '../tags';
+import { MetadataTags, RichImageData } from '../metadata';
export class IsobmffBoxWriter {
private helper = new Uint8Array(8);
@@ -421,7 +421,12 @@ export const tkhd = (
matrix = IDENTITY_MATRIX;
}
- return fullBox('tkhd', +needsU64, 3, [
+ let flags = 0x2; // Track in movie
+ if (trackData.track.metadata.disposition?.default !== false) {
+ flags |= 0x1; // Track enabled
+ }
+
+ return fullBox('tkhd', +needsU64, flags, [
u32OrU64(creationTime), // Creation time
u32OrU64(creationTime), // Modification time
u32(trackData.track.id), // Track ID
diff --git a/src/isobmff/isobmff-demuxer.ts b/src/isobmff/isobmff-demuxer.ts
index aa34771..b76fd85 100644
--- a/src/isobmff/isobmff-demuxer.ts
+++ b/src/isobmff/isobmff-demuxer.ts
@@ -86,13 +86,13 @@ import {
readU8,
readAscii,
} from '../reader';
-import { MetadataTags, RichImageData } from '../tags';
+import { DEFAULT_TRACK_DISPOSITION, MetadataTags, RichImageData, TrackDisposition } from '../metadata';
type InternalTrack = {
id: number;
demuxer: IsobmffDemuxer;
inputTrack: InputTrack | null;
- isDefault: boolean;
+ disposition: TrackDisposition;
timescale: number;
durationInMovieTimescale: number;
durationInMediaTimescale: number;
@@ -317,7 +317,7 @@ export class IsobmffDemuxer extends Demuxer {
this.readContiguousBoxes(this.moovSlice);
// Put default tracks first
- this.tracks.sort((a, b) => Number(b.isDefault) - Number(a.isDefault));
+ this.tracks.sort((a, b) => Number(b.disposition.default) - Number(a.disposition.default));
for (const track of this.tracks) {
// Modify the edit list offset based on the previous segment durations. They are in different
@@ -655,7 +655,9 @@ export class IsobmffDemuxer extends Demuxer {
id: -1,
demuxer: this,
inputTrack: null,
- isDefault: false,
+ disposition: {
+ ...DEFAULT_TRACK_DISPOSITION,
+ },
info: null,
timescale: -1,
durationInMovieTimescale: -1,
@@ -703,7 +705,7 @@ export class IsobmffDemuxer extends Demuxer {
// Spec says disabled tracks are to be treated like they don't exist, but in practice, they are treated
// more like non-default tracks.
const trackEnabled = !!(flags & 0x1);
- track.isDefault = trackEnabled;
+ track.disposition.default = trackEnabled;
// Skip over creation & modification time to reach the track ID
if (version === 0) {
@@ -2350,6 +2352,10 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
return this.internalTrack.timescale;
}
+ getDisposition() {
+ return this.internalTrack.disposition;
+ }
+
async computeDuration() {
const lastPacket = await this.getPacket(Infinity, { metadataOnly: true });
return (lastPacket?.timestamp ?? 0) + (lastPacket?.duration ?? 0);
diff --git a/src/isobmff/isobmff-reader.ts b/src/isobmff/isobmff-reader.ts
index 67e0ab8..2579fc9 100644
--- a/src/isobmff/isobmff-reader.ts
+++ b/src/isobmff/isobmff-reader.ts
@@ -6,7 +6,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
-import { RichImageData } from '../tags';
+import { RichImageData } from '../metadata';
import { textDecoder } from '../misc';
import { FileSlice, readAscii, readBytes, readI32Be, readU16Be, readU32Be, readU64Be, readU8 } from '../reader';
diff --git a/src/matroska/ebml.ts b/src/matroska/ebml.ts
index 5097b77..d695405 100644
--- a/src/matroska/ebml.ts
+++ b/src/matroska/ebml.ts
@@ -88,6 +88,10 @@ export enum EBMLId {
FlagEnabled = 0xb9,
FlagDefault = 0x88,
FlagForced = 0x55aa,
+ FlagOriginal = 0x55ae,
+ FlagHearingImpaired = 0x55ab,
+ FlagVisualImpaired = 0x55ac,
+ FlagCommentary = 0x55af,
FlagLacing = 0x9c,
Name = 0x536e,
Language = 0x22b59c,
diff --git a/src/matroska/matroska-demuxer.ts b/src/matroska/matroska-demuxer.ts
index 4c69c35..0cf97d7 100644
--- a/src/matroska/matroska-demuxer.ts
+++ b/src/matroska/matroska-demuxer.ts
@@ -31,7 +31,7 @@ import {
InputVideoTrack,
InputVideoTrackBacking,
} from '../input-track';
-import { AttachedFile, MetadataTags } from '../tags';
+import { AttachedFile, DEFAULT_TRACK_DISPOSITION, MetadataTags, TrackDisposition } from '../metadata';
import { PacketRetrievalOptions } from '../media-sink';
import {
assert,
@@ -188,7 +188,7 @@ type InternalTrack = {
}[];
cuePoints: CuePoint[];
- isDefault: boolean;
+ disposition: TrackDisposition;
inputTrack: InputTrack | null;
codecId: string | null;
codecPrivate: Uint8Array | null;
@@ -537,7 +537,7 @@ export class MatroskaDemuxer extends Demuxer {
}
// Put default tracks first
- this.currentSegment.tracks.sort((a, b) => Number(b.isDefault) - Number(a.isDefault));
+ this.currentSegment.tracks.sort((a, b) => Number(b.disposition.default) - Number(a.disposition.default));
// Now, let's distribute the cue points to the tracks
const idToTrack = new Map(this.currentSegment.tracks.map(x => [x.id, x]));
@@ -989,7 +989,9 @@ export class MatroskaDemuxer extends Demuxer {
clusterPositionCache: [],
cuePoints: [],
- isDefault: false,
+ disposition: {
+ ...DEFAULT_TRACK_DISPOSITION,
+ },
inputTrack: null,
codecId: null,
codecPrivate: null,
@@ -1154,7 +1156,37 @@ export class MatroskaDemuxer extends Demuxer {
case EBMLId.FlagDefault: {
if (!this.currentTrack) break;
- this.currentTrack.isDefault = !!readUnsignedInt(slice, size);
+ this.currentTrack.disposition.default = !!readUnsignedInt(slice, size);
+ }; break;
+
+ case EBMLId.FlagForced: {
+ if (!this.currentTrack) break;
+
+ this.currentTrack.disposition.forced = !!readUnsignedInt(slice, size);
+ }; break;
+
+ case EBMLId.FlagOriginal: {
+ if (!this.currentTrack) break;
+
+ this.currentTrack.disposition.original = !!readUnsignedInt(slice, size);
+ }; break;
+
+ case EBMLId.FlagHearingImpaired: {
+ if (!this.currentTrack) break;
+
+ this.currentTrack.disposition.hearingImpaired = !!readUnsignedInt(slice, size);
+ }; break;
+
+ case EBMLId.FlagVisualImpaired: {
+ if (!this.currentTrack) break;
+
+ this.currentTrack.disposition.visuallyImpaired = !!readUnsignedInt(slice, size);
+ }; break;
+
+ case EBMLId.FlagCommentary: {
+ if (!this.currentTrack) break;
+
+ this.currentTrack.disposition.commentary = !!readUnsignedInt(slice, size);
}; break;
case EBMLId.CodecID: {
@@ -1847,6 +1879,10 @@ abstract class MatroskaTrackBacking implements InputTrackBacking {
return this.internalTrack.segment.timestampFactor;
}
+ getDisposition() {
+ return this.internalTrack.disposition;
+ }
+
async getFirstPacket(options: PacketRetrievalOptions) {
return this.performClusterLookup(
null,
diff --git a/src/matroska/matroska-muxer.ts b/src/matroska/matroska-muxer.ts
index deff3e0..920eab8 100644
--- a/src/matroska/matroska-muxer.ts
+++ b/src/matroska/matroska-muxer.ts
@@ -63,7 +63,7 @@ import { Muxer } from '../muxer';
import { Writer } from '../writer';
import { EncodedPacket } from '../packet';
import { parseOpusIdentificationHeader } from '../codec-data';
-import { AttachedFile } from '../tags';
+import { AttachedFile } from '../metadata';
const MIN_CLUSTER_TIMESTAMP_MS = /* #__PURE__ */ -(2 ** 15);
const MAX_CLUSTER_TIMESTAMP_MS = /* #__PURE__ */ 2 ** 15 - 1;
@@ -304,6 +304,24 @@ export class MatroskaMuxer extends Muxer {
{ id: EBMLId.TrackNumber, data: trackData.track.id },
{ id: EBMLId.TrackUID, data: trackData.track.id },
{ id: EBMLId.TrackType, data: TRACK_TYPE_MAP[trackData.type] },
+ trackData.track.metadata.disposition?.default === false
+ ? { id: EBMLId.FlagDefault, data: 0 }
+ : null,
+ trackData.track.metadata.disposition?.forced
+ ? { id: EBMLId.FlagForced, data: 1 }
+ : null,
+ trackData.track.metadata.disposition?.hearingImpaired
+ ? { id: EBMLId.FlagHearingImpaired, data: 1 }
+ : null,
+ trackData.track.metadata.disposition?.visuallyImpaired
+ ? { id: EBMLId.FlagVisualImpaired, data: 1 }
+ : null,
+ trackData.track.metadata.disposition?.original
+ ? { id: EBMLId.FlagOriginal, data: 1 }
+ : null,
+ trackData.track.metadata.disposition?.commentary
+ ? { id: EBMLId.FlagCommentary, data: 1 }
+ : null,
{ id: EBMLId.FlagLacing, data: 0 },
{ id: EBMLId.Language, data: trackData.track.metadata.languageCode ?? UNDETERMINED_LANGUAGE },
{ id: EBMLId.CodecID, data: codecId },
diff --git a/src/tags.ts b/src/metadata.ts
similarity index 81%
rename from src/tags.ts
rename to src/metadata.ts
index 9055efc..dc1d166 100644
--- a/src/tags.ts
+++ b/src/metadata.ts
@@ -259,3 +259,62 @@ export const metadataTagsAreEmpty = (tags: MetadataTags) => {
&& tags.comment === undefined
&& (tags.raw === undefined || Object.keys(tags.raw).length === 0);
};
+
+/**
+ * Specifies a track's disposition, i.e. information about its intended usage.
+ * @public
+ * @group Miscellaneous
+ */
+export type TrackDisposition = {
+ /**
+ * Indicates that this track is eligible for automatic selection by a player; that it is the main track among other,
+ * non-default tracks of the same type.
+ */
+ default: boolean;
+ /**
+ * Indicates that players should always display this track by default, even if it goes against the user's default
+ * preferences. For example, a subtitle track only containing translations of foreign-language audio.
+ */
+ forced: boolean;
+ /** Indicates that this track is in the content's original language. */
+ original: boolean;
+ /** Indicates that this track contains commentary. */
+ commentary: boolean;
+ /** Indicates that this track is intended for hearing-impaired users. */
+ hearingImpaired: boolean;
+ /** Indicates that this track is intended for visually-impaired users. */
+ visuallyImpaired: boolean;
+};
+
+export const DEFAULT_TRACK_DISPOSITION: TrackDisposition = {
+ default: true,
+ forced: false,
+ original: false,
+ commentary: false,
+ hearingImpaired: false,
+ visuallyImpaired: false,
+};
+
+export const validateTrackDisposition = (disposition: Partial) => {
+ if (!disposition || typeof disposition !== 'object') {
+ throw new TypeError('disposition must be an object.');
+ }
+ if (disposition.default !== undefined && typeof disposition.default !== 'boolean') {
+ throw new TypeError('disposition.default must be a boolean.');
+ }
+ if (disposition.forced !== undefined && typeof disposition.forced !== 'boolean') {
+ throw new TypeError('disposition.forced must be a boolean.');
+ }
+ if (disposition.original !== undefined && typeof disposition.original !== 'boolean') {
+ throw new TypeError('disposition.original must be a boolean.');
+ }
+ if (disposition.commentary !== undefined && typeof disposition.commentary !== 'boolean') {
+ throw new TypeError('disposition.commentary must be a boolean.');
+ }
+ if (disposition.hearingImpaired !== undefined && typeof disposition.hearingImpaired !== 'boolean') {
+ throw new TypeError('disposition.hearingImpaired must be a boolean.');
+ }
+ if (disposition.visuallyImpaired !== undefined && typeof disposition.visuallyImpaired !== 'boolean') {
+ throw new TypeError('disposition.visuallyImpaired must be a boolean.');
+ }
+};
diff --git a/src/mp3/mp3-demuxer.ts b/src/mp3/mp3-demuxer.ts
index a8333b5..fe654d8 100644
--- a/src/mp3/mp3-demuxer.ts
+++ b/src/mp3/mp3-demuxer.ts
@@ -10,7 +10,7 @@ import { AudioCodec } from '../codec';
import { Demuxer } from '../demuxer';
import { Input } from '../input';
import { InputAudioTrack, InputAudioTrackBacking } from '../input-track';
-import { MetadataTags } from '../tags';
+import { DEFAULT_TRACK_DISPOSITION, MetadataTags } from '../metadata';
import { PacketRetrievalOptions } from '../media-sink';
import { assert, AsyncMutex, binarySearchExact, binarySearchLessOrEqual, UNDETERMINED_LANGUAGE } from '../misc';
import { EncodedPacket, PLACEHOLDER_DATA } from '../packet';
@@ -257,6 +257,12 @@ class Mp3AudioTrackBacking implements InputAudioTrackBacking {
return this.demuxer.firstFrameHeader.sampleRate;
}
+ getDisposition() {
+ return {
+ ...DEFAULT_TRACK_DISPOSITION,
+ };
+ }
+
async getDecoderConfig(): Promise {
assert(this.demuxer.firstFrameHeader);
diff --git a/src/mp3/mp3-muxer.ts b/src/mp3/mp3-muxer.ts
index dd310da..f2c86e8 100644
--- a/src/mp3/mp3-muxer.ts
+++ b/src/mp3/mp3-muxer.ts
@@ -7,7 +7,7 @@
*/
import { assert, toDataView } from '../misc';
-import { metadataTagsAreEmpty } from '../tags';
+import { metadataTagsAreEmpty } from '../metadata';
import { Muxer } from '../muxer';
import { Output, OutputAudioTrack } from '../output';
import { Mp3OutputFormat } from '../output-format';
diff --git a/src/ogg/ogg-demuxer.ts b/src/ogg/ogg-demuxer.ts
index 556e3af..c5164b9 100644
--- a/src/ogg/ogg-demuxer.ts
+++ b/src/ogg/ogg-demuxer.ts
@@ -12,7 +12,7 @@ import { Demuxer } from '../demuxer';
import { Input } from '../input';
import { InputAudioTrack, InputAudioTrackBacking } from '../input-track';
import { PacketRetrievalOptions } from '../media-sink';
-import { MetadataTags } from '../tags';
+import { DEFAULT_TRACK_DISPOSITION, MetadataTags } from '../metadata';
import {
assert,
AsyncMutex,
@@ -463,6 +463,12 @@ class OggAudioTrackBacking implements InputAudioTrackBacking {
return UNDETERMINED_LANGUAGE;
}
+ getDisposition() {
+ return {
+ ...DEFAULT_TRACK_DISPOSITION,
+ };
+ }
+
async getFirstTimestamp() {
return 0;
}
diff --git a/src/output.ts b/src/output.ts
index 6379388..4046c1f 100644
--- a/src/output.ts
+++ b/src/output.ts
@@ -7,7 +7,7 @@
*/
import { AsyncMutex, isIso639Dash2LanguageCode, Rotation } from './misc';
-import { MetadataTags, validateMetadataTags } from './tags';
+import { MetadataTags, TrackDisposition, validateMetadataTags, validateTrackDisposition } from './metadata';
import { Muxer } from './muxer';
import { OutputFormat } from './output-format';
import { AudioSource, MediaSource, SubtitleSource, VideoSource } from './media-source';
@@ -74,6 +74,8 @@ export type BaseTrackMetadata = {
languageCode?: string;
/** A user-defined name for this track, like "English" or "Director Commentary". */
name?: string;
+ /** The track's disposition, i.e. information about its intended usage. */
+ disposition?: Partial;
/**
* The maximum amount of encoded packets that will be added to this track. Setting this field provides the muxer
* with an additional signal that it can use to preallocate space in the file.
@@ -129,6 +131,9 @@ const validateBaseTrackMetadata = (metadata: BaseTrackMetadata) => {
if (metadata.name !== undefined && typeof metadata.name !== 'string') {
throw new TypeError('metadata.name, when provided, must be a string.');
}
+ if (metadata.disposition !== undefined) {
+ validateTrackDisposition(metadata.disposition);
+ }
if (
metadata.maximumPacketCount !== undefined
&& (!Number.isInteger(metadata.maximumPacketCount) || metadata.maximumPacketCount < 0)
diff --git a/src/wave/wave-demuxer.ts b/src/wave/wave-demuxer.ts
index c552827..56b5db5 100644
--- a/src/wave/wave-demuxer.ts
+++ b/src/wave/wave-demuxer.ts
@@ -11,7 +11,7 @@ import { Demuxer } from '../demuxer';
import { Input } from '../input';
import { InputAudioTrack, InputAudioTrackBacking } from '../input-track';
import { PacketRetrievalOptions } from '../media-sink';
-import { MetadataTags } from '../tags';
+import { DEFAULT_TRACK_DISPOSITION, MetadataTags } from '../metadata';
import { assert, UNDETERMINED_LANGUAGE } from '../misc';
import { EncodedPacket, PLACEHOLDER_DATA } from '../packet';
import { readAscii, readBytes, Reader, readU16, readU32, readU64 } from '../reader';
@@ -407,6 +407,12 @@ class WaveAudioTrackBacking implements InputAudioTrackBacking {
return UNDETERMINED_LANGUAGE;
}
+ getDisposition() {
+ return {
+ ...DEFAULT_TRACK_DISPOSITION,
+ };
+ }
+
async getFirstTimestamp() {
return 0;
}
diff --git a/src/wave/wave-muxer.ts b/src/wave/wave-muxer.ts
index 63c8f96..74b6abf 100644
--- a/src/wave/wave-muxer.ts
+++ b/src/wave/wave-muxer.ts
@@ -15,7 +15,7 @@ import { Writer } from '../writer';
import { EncodedPacket } from '../packet';
import { WavOutputFormat } from '../output-format';
import { assert, assertNever, isIso88591Compatible, keyValueIterator } from '../misc';
-import { MetadataTags, metadataTagsAreEmpty } from '../tags';
+import { MetadataTags, metadataTagsAreEmpty } from '../metadata';
import { Id3V2Writer } from '../id3';
export class WaveMuxer extends Muxer {
diff --git a/test/node/disposition.test.ts b/test/node/disposition.test.ts
new file mode 100644
index 0000000..a949121
--- /dev/null
+++ b/test/node/disposition.test.ts
@@ -0,0 +1,92 @@
+import { expect, test } from 'vitest';
+import { Output } from '../../src/output.js';
+import { MkvOutputFormat } from '../../src/output-format.js';
+import { BufferTarget } from '../../src/target.js';
+import { EncodedVideoPacketSource } from '../../src/media-source.js';
+import { EncodedPacket } from '../../src/packet.js';
+import { Input } from '../../src/input.js';
+import { BufferSource } from '../../src/source.js';
+import { ALL_FORMATS } from '../../src/input-format.js';
+
+test('Default track disposition', async () => {
+ const output = new Output({
+ format: new MkvOutputFormat(),
+ target: new BufferTarget(),
+ });
+
+ const source = new EncodedVideoPacketSource('avc');
+ output.addVideoTrack(source);
+
+ await output.start();
+ await source.add(new EncodedPacket(new Uint8Array(1024), 'key', 0, 1), {
+ decoderConfig: {
+ codec: 'avc1.123456',
+ codedWidth: 1920,
+ codedHeight: 1080,
+ },
+ });
+
+ await output.finalize();
+
+ using input = new Input({
+ source: new BufferSource(output.target.buffer!),
+ formats: ALL_FORMATS,
+ });
+
+ const track = (await input.getPrimaryVideoTrack())!;
+
+ expect(track.disposition).toEqual({
+ default: true,
+ forced: false,
+ original: false,
+ hearingImpaired: false,
+ visionImpaired: false,
+ commentary: false,
+ });
+});
+
+test('Customized track disposition', async () => {
+ const output = new Output({
+ format: new MkvOutputFormat(),
+ target: new BufferTarget(),
+ });
+
+ const source = new EncodedVideoPacketSource('avc');
+ output.addVideoTrack(source, {
+ disposition: {
+ default: false,
+ forced: true,
+ original: true,
+ hearingImpaired: true,
+ visuallyImpaired: true,
+ commentary: true,
+ },
+ });
+
+ await output.start();
+ await source.add(new EncodedPacket(new Uint8Array(1024), 'key', 0, 1), {
+ decoderConfig: {
+ codec: 'avc1.123456',
+ codedWidth: 1920,
+ codedHeight: 1080,
+ },
+ });
+
+ await output.finalize();
+
+ using input = new Input({
+ source: new BufferSource(output.target.buffer!),
+ formats: ALL_FORMATS,
+ });
+
+ const track = (await input.getPrimaryVideoTrack())!;
+
+ expect(track.disposition).toEqual({
+ default: false,
+ forced: true,
+ original: true,
+ hearingImpaired: true,
+ visionImpaired: true,
+ commentary: true,
+ });
+});
diff --git a/test/node/metadata-tags.test.ts b/test/node/metadata-tags.test.ts
index ae7ceb2..d9e50c2 100644
--- a/test/node/metadata-tags.test.ts
+++ b/test/node/metadata-tags.test.ts
@@ -15,7 +15,7 @@ import { EncodedPacket } from '../../src/packet.js';
import { Input } from '../../src/input.js';
import { BufferSource, FilePathSource } from '../../src/source.js';
import { ALL_FORMATS } from '../../src/input-format.js';
-import { AttachedFile, MetadataTags } from '../../src/tags.js';
+import { AttachedFile, MetadataTags } from '../../src/metadata.js';
import path from 'node:path';
import { AudioCodec, buildAudioCodecString } from '../../src/codec.js';
import { Conversion } from '../../src/conversion.js';