diff --git a/dev/demux.html b/dev/demux.html index 8fd7a01..7d7796e 100644 --- a/dev/demux.html +++ b/dev/demux.html @@ -43,6 +43,8 @@ const videoTrack = await input.getPrimaryVideoTrack(); const sink = new Metamuxer.VideoFrameSink(videoTrack); + console.log(await videoTrack.getLanguageCode()); + for await (const thing of sink.framesAtTimestamps([0.2, 1, 2, 423])) { console.log(thing) } diff --git a/src/input-track.ts b/src/input-track.ts index bc8d09a..1282bf7 100644 --- a/src/input-track.ts +++ b/src/input-track.ts @@ -7,6 +7,7 @@ export interface InputTrackBacking { getCodec(): Promise; getFirstTimestamp(): Promise; computeDuration(): Promise; + getLanguageCode(): Promise; } /** @public */ @@ -39,6 +40,10 @@ export abstract class InputTrack { computeDuration() { return this._backing.computeDuration(); } + + getLanguageCode() { + return this._backing.getLanguageCode(); + } } export interface InputVideoTrackBacking extends InputTrackBacking { diff --git a/src/isobmff/isobmff-boxes.ts b/src/isobmff/isobmff-boxes.ts index b7f0449..55259ab 100644 --- a/src/isobmff/isobmff-boxes.ts +++ b/src/isobmff/isobmff-boxes.ts @@ -11,6 +11,7 @@ import { colorSpaceIsComplete, IDENTITY_MATRIX, rotationMatrix, + UNDETERMINED_LANGUAGE, } from '../misc'; import { AudioCodec, @@ -429,7 +430,7 @@ export const mdhd = ( const u32OrU64 = needsU64 ? u64 : u32; let language = 0; - for (const character of (trackData.track.metadata.languageCode ?? 'und')) { + for (const character of (trackData.track.metadata.languageCode ?? UNDETERMINED_LANGUAGE)) { language <<= 5; language += character.charCodeAt(0) - 0x60; } diff --git a/src/isobmff/isobmff-demuxer.ts b/src/isobmff/isobmff-demuxer.ts index 3b39ffc..9f191b8 100644 --- a/src/isobmff/isobmff-demuxer.ts +++ b/src/isobmff/isobmff-demuxer.ts @@ -34,6 +34,7 @@ import { last, AsyncMutex, findLastIndex, + UNDETERMINED_LANGUAGE, } from '../misc'; import { Reader } from '../reader'; import { EncodedAudioSample, EncodedVideoSample, PLACEHOLDER_DATA, SampleType } from '../sample'; @@ -47,6 +48,7 @@ type InternalTrack = { durationInMovieTimescale: number; durationInMediaTimescale: number; rotation: Rotation; + languageCode: string; sampleTableByteOffset: number; sampleTable: SampleTable | null; fragmentLookupTable: FragmentLookupTableEntry[] | null; @@ -557,6 +559,7 @@ export class IsobmffDemuxer extends Demuxer { durationInMovieTimescale: -1, durationInMediaTimescale: -1, rotation: 0, + languageCode: UNDETERMINED_LANGUAGE, sampleTableByteOffset: -1, sampleTable: null, fragmentLookupTable: null, @@ -642,6 +645,17 @@ export class IsobmffDemuxer extends Demuxer { track.timescale = this.isobmffReader.readU32(); track.durationInMediaTimescale = this.isobmffReader.readU64(); } + + let language = this.isobmffReader.readU16(); + + if (language > 0) { + track.languageCode = ''; + + for (let i = 0; i < 3; i++) { + track.languageCode = String.fromCharCode(0x60 + (language & 0b11111)) + track.languageCode; + language >>= 5; + } + } }; break; case 'hdlr': { @@ -1689,6 +1703,10 @@ abstract class IsobmffTrackBacking< return (lastSample?.timestamp ?? 0) + (lastSample?.duration ?? 0); } + async getLanguageCode() { + return this.internalTrack.languageCode; + } + async getFirstTimestamp() { const firstSample = await this.getFirstSample({ metadataOnly: true }); return firstSample?.timestamp ?? 0; diff --git a/src/matroska/matroska-demuxer.ts b/src/matroska/matroska-demuxer.ts index f355c40..8b82991 100644 --- a/src/matroska/matroska-demuxer.ts +++ b/src/matroska/matroska-demuxer.ts @@ -30,6 +30,7 @@ import { MATRIX_COEFFICIENTS_MAP_INVERSE, Rotation, TRANSFER_CHARACTERISTICS_MAP_INVERSE, + UNDETERMINED_LANGUAGE, } from '../misc'; import { Reader } from '../reader'; import { EncodedAudioSample, EncodedVideoSample, PLACEHOLDER_DATA, SampleType } from '../sample'; @@ -107,6 +108,7 @@ type InternalTrack = { inputTrack: InputTrack | null; codecId: string | null; codecPrivate: Uint8Array | null; + languageCode: string; info: | null | { @@ -583,6 +585,7 @@ export class MatroskaDemuxer extends Demuxer { inputTrack: null, codecId: null, codecPrivate: null, + languageCode: UNDETERMINED_LANGUAGE, info: null, }; @@ -733,6 +736,12 @@ export class MatroskaDemuxer extends Demuxer { this.currentTrack.codecPrivate = reader.readBytes(size); }; break; + case EBMLId.Language: { + if (!this.currentTrack) break; + + this.currentTrack.languageCode = reader.readString(size); + }; break; + case EBMLId.Video: { if (this.currentTrack?.info?.type !== 'video') break; @@ -967,6 +976,10 @@ abstract class MatroskaTrackBacking< return (lastSample?.timestamp ?? 0) + (lastSample?.duration ?? 0); } + async getLanguageCode() { + return this.internalTrack.languageCode; + } + async getFirstTimestamp() { const firstSample = await this.getFirstSample({ metadataOnly: true }); return firstSample?.timestamp ?? 0; diff --git a/src/matroska/matroska-muxer.ts b/src/matroska/matroska-muxer.ts index 7c6f4a7..36f8210 100644 --- a/src/matroska/matroska-muxer.ts +++ b/src/matroska/matroska-muxer.ts @@ -2,6 +2,7 @@ import { COLOR_PRIMARIES_MAP, MATRIX_COEFFICIENTS_MAP, TRANSFER_CHARACTERISTICS_MAP, + UNDETERMINED_LANGUAGE, assert, colorSpaceIsComplete, readBits, @@ -241,7 +242,7 @@ export class MatroskaMuxer extends Muxer { { id: EBMLId.TrackUID, data: trackData.track.id }, { id: EBMLId.TrackType, data: TRACK_TYPE_MAP[trackData.type] }, { id: EBMLId.FlagLacing, data: 0 }, - { id: EBMLId.Language, data: trackData.track.metadata.languageCode ?? 'und' }, + { id: EBMLId.Language, data: trackData.track.metadata.languageCode ?? UNDETERMINED_LANGUAGE }, { id: EBMLId.CodecID, data: codecId }, { id: EBMLId.CodecDelay, data: 0 }, { id: EBMLId.SeekPreRoll, data: seekPreRollNs }, diff --git a/src/misc.ts b/src/misc.ts index 5bfd2b3..500eea6 100644 --- a/src/misc.ts +++ b/src/misc.ts @@ -329,3 +329,5 @@ export const mapAsyncGenerator = ( export const clamp = (value: number, min: number, max: number) => { return Math.max(min, Math.min(max, value)); }; + +export const UNDETERMINED_LANGUAGE = 'und'; diff --git a/src/wave/wave-demuxer.ts b/src/wave/wave-demuxer.ts index 557085a..68e856e 100644 --- a/src/wave/wave-demuxer.ts +++ b/src/wave/wave-demuxer.ts @@ -3,7 +3,7 @@ import { Demuxer } from '../demuxer'; import { Input } from '../input'; import { InputAudioTrack, InputAudioTrackBacking } from '../input-track'; import { SampleRetrievalOptions } from '../media-sink'; -import { assert } from '../misc'; +import { assert, UNDETERMINED_LANGUAGE } from '../misc'; import { Reader } from '../reader'; import { EncodedAudioSample, PLACEHOLDER_DATA } from '../sample'; import { RiffReader } from './riff-reader'; @@ -215,6 +215,10 @@ class WaveAudioTrackBacking implements InputAudioTrackBacking { return this.demuxer.audioInfo.sampleRate; } + async getLanguageCode() { + return UNDETERMINED_LANGUAGE; + } + async getFirstTimestamp() { return 0; }