Fix tkhd issue for Apple ecosystem compatibility (#391)

* fix: ensure at least one track per type is enabled for Apple ecosystem compatibility

* Add ensureOneEnabledTrack() to ISOBMFF muxer

* Add test case

---------

Co-authored-by: Vanilagy <[email protected]>
This commit is contained in:
StringWeaver
2026-06-01 22:55:02 +02:00
committed by GitHub
co-authored by Vanilagy
parent c6c768d93e
commit 96d0d9fb22
3 changed files with 71 additions and 7 deletions
+8 -6
View File
@@ -378,12 +378,14 @@ export const free = (size: number): Box => ({ type: 'free', size });
* Movie Box: Used to specify the information that defines a movie - that is, the information that allows
* an application to interpret the sample data that is stored elsewhere.
*/
export const moov = (muxer: IsobmffMuxer) => box('moov', undefined, [
mvhd(muxer.creationTime, muxer.trackDatas),
...muxer.trackDatas.map(x => trak(x, muxer.creationTime)),
muxer.isFragmented ? mvex(muxer.trackDatas) : null,
udta(muxer),
]);
export const moov = (muxer: IsobmffMuxer) => {
return box('moov', undefined, [
mvhd(muxer.creationTime, muxer.trackDatas),
...muxer.trackDatas.map(x => trak(x, muxer.creationTime)),
muxer.isFragmented ? mvex(muxer.trackDatas) : null,
udta(muxer),
]);
};
/** Movie Header Box: Used to specify the characteristics of the entire movie, such as timescale and duration. */
export const mvhd = (
+28 -1
View File
@@ -22,7 +22,7 @@ import {
vtte,
} from './isobmff-boxes';
import { Muxer } from '../muxer';
import { Output, OutputAudioTrack, OutputSubtitleTrack, OutputTrack, OutputVideoTrack } from '../output';
import { Output, OutputAudioTrack, OutputSubtitleTrack, OutputTrack, OutputVideoTrack, TrackType } from '../output';
import { Writer } from '../writer';
import { BufferTarget } from '../target';
import { assert, computeRationalApproximation, last, promiseWithResolvers, Rational, simplifyRational } from '../misc';
@@ -1183,6 +1183,8 @@ export class IsobmffMuxer extends Muxer {
boxWriter.writer.startTrackingWrites();
}
this.ensureOneEnabledTrack();
// Write the moov box now that we have all decoder configs
const movieBox = moov(this);
boxWriter.writeBox(movieBox);
@@ -1299,6 +1301,8 @@ export class IsobmffMuxer extends Muxer {
if (this.allTracksAreKnown()) {
if (!this.mdat) {
this.ensureOneEnabledTrack();
// We finally know all tracks, let's reserve space for the moov box
const moovBox = moov(this);
const moovSize = this.boxWriter.measureBox(moovBox);
@@ -1389,11 +1393,34 @@ export class IsobmffMuxer extends Muxer {
release();
}
ensureOneEnabledTrack() {
// If no track of a given type is enabled, force the first one to be enabled. Otherwise the video won't play in
// players like QuickTime.
// https://github.com/Vanilagy/mediabunny/pull/391
for (const type of ['video', 'audio', 'subtitle'] as TrackType[]) {
const tracks = this.trackDatas.filter(t => t.type === type);
if (tracks.length === 0) {
continue;
}
const hasEnabled = tracks.some(t => t.track.metadata.disposition?.default !== false);
if (!hasEnabled) {
const firstTrack = tracks[0]!;
firstTrack.track.metadata.disposition = {
...firstTrack.track.metadata.disposition,
default: true,
};
}
}
}
/** Finalizes the file, making it ready for use. Must be called after all video and audio chunks have been added. */
async finalize() {
const release = await this.mutex.acquire();
this.allTracksKnown.resolve();
this.ensureOneEnabledTrack();
for (const trackData of this.trackDatas) {
trackData.closed = true;