Skip to content

Commit ddeed03

Browse files
committed
feat: add per-track segmentation init mode
1 parent 9b35d1f commit ddeed03

5 files changed

Lines changed: 143 additions & 40 deletions

File tree

.changeset/kind-turtles-dance.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"mp4box": minor
3+
---
4+
5+
feat: add per-track initialization segments for segmentation

README.md

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -270,28 +270,29 @@ mp4boxfile.onSegment = function (id, user, buffer, sampleNumber, last) {
270270
};
271271
```
272272

273-
#### initializeSegmentation()
273+
#### initializeSegmentation(mode)
274274

275-
Indicates that the application is ready to receive segments. Returns an array of objects containing the following properties:
275+
Indicates that the application is ready to receive segments.
276+
`mode` can be `'combined'` (default) or `'per-track'`.
276277

277-
- **id**: Number, the track id
278-
- **user**: Object, the caller of the segmentation for this track, as given in [setSegmentOptions](##setsegmentoptionstrack_id-user-options)
279-
- **buffer**: ArrayBuffer, the initialization segment for this track.
280-
- **sampleNumber**: Number, sample number of the last sample in the segment, plus 1.
281-
- **last**: Boolean, indication if this is the last segment to be received.
278+
By default, it returns a single initialization segment containing all tracks configured with [setSegmentOptions](#setsegmentoptionstrack_id-user-options):
279+
280+
```json
281+
{
282+
"tracks": [
283+
{ "id": 2, "user": "[SourceBuffer]" },
284+
{ "id": 3, "user": "[SourceBuffer]" }
285+
],
286+
"buffer": "[ArrayBuffer]"
287+
}
288+
```
289+
290+
If called with `'per-track'`, it returns one initialization segment per fragmented track:
282291

283292
```json
284293
[
285-
{
286-
"id": 2,
287-
"buffer": "[ArrayBuffer]",
288-
"user": "[SourceBuffer]"
289-
},
290-
{
291-
"id": 3,
292-
"buffer": "[ArrayBuffer]",
293-
"user": "[SourceBuffer]"
294-
}
294+
{ "id": 2, "buffer": "[ArrayBuffer]", "user": "[SourceBuffer]" },
295+
{ "id": 3, "buffer": "[ArrayBuffer]", "user": "[SourceBuffer]" }
295296
]
296297
```
297298

entries/types.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,23 @@ export interface FragmentedTrack<TUser> {
119119
accumulatedSize: number;
120120
};
121121
}
122+
123+
export interface SegmentationInitializationTrack<TUser> {
124+
id: number;
125+
user: TUser;
126+
}
127+
128+
export interface SegmentationInitialization<TUser> {
129+
tracks: Array<SegmentationInitializationTrack<TUser>>;
130+
buffer: ArrayBuffer;
131+
}
132+
133+
export interface SegmentationInitializationPerTrack<
134+
TUser,
135+
> extends SegmentationInitializationTrack<TUser> {
136+
buffer: ArrayBuffer;
137+
}
138+
122139
export interface ExtractedTrack<TUser> {
123140
id: number;
124141
user: TUser;

src/isofile.ts

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ import type {
9393
Item,
9494
Movie,
9595
Output,
96+
SegmentationInitialization,
97+
SegmentationInitializationPerTrack,
9698
Sample,
9799
SampleEntryFourCC,
98100
SubSample,
@@ -1303,42 +1305,65 @@ export class ISOFile<TSegmentUser = unknown, TSampleUser = unknown> {
13031305
}
13041306

13051307
/** @bundle isofile-write.js */
1306-
initializeSegmentation() {
1308+
initializeSegmentation(mode?: 'combined'): SegmentationInitialization<TSegmentUser>;
1309+
initializeSegmentation(
1310+
mode: 'per-track',
1311+
): Array<SegmentationInitializationPerTrack<TSegmentUser>>;
1312+
initializeSegmentation(
1313+
mode?: 'combined' | 'per-track',
1314+
):
1315+
| SegmentationInitialization<TSegmentUser>
1316+
| Array<SegmentationInitializationPerTrack<TSegmentUser>> {
13071317
if (!this.onSegment) {
13081318
Log.warn('MP4Box', 'No segmentation callback set!');
13091319
}
1320+
if (mode !== undefined && mode !== 'combined' && mode !== 'per-track') {
1321+
throw new Error(`Invalid segmentation mode: ${mode}`);
1322+
}
13101323
if (!this.isFragmentationInitialized) {
13111324
this.isFragmentationInitialized = true;
13121325
this.resetTables();
13131326
}
13141327

1315-
// Create the moov that will hold all the tracks
1316-
const moov = new moovBox();
1317-
moov.addBox(this.moov.mvhd);
1318-
1319-
// Add the tracks we want to fragment
1320-
for (let i = 0; i < this.fragmentedTracks.length; i++) {
1321-
const trak = this.getTrackById(this.fragmentedTracks[i].id);
1328+
const tracksToInitialize: Array<{ id: number; user: TSegmentUser; trak: trakBox }> = [];
1329+
for (const fragmentedTrack of this.fragmentedTracks) {
1330+
const trak = this.getTrackById(fragmentedTrack.id);
13221331
if (!trak) {
13231332
Log.warn(
13241333
'ISOFile',
1325-
`Track with id ${this.fragmentedTracks[i].id} not found, skipping fragmentation initialization`,
1334+
`Track with id ${fragmentedTrack.id} not found, skipping fragmentation initialization`,
13261335
);
13271336
continue;
13281337
}
1329-
moov.addBox(trak);
1338+
tracksToInitialize.push({ id: fragmentedTrack.id, user: fragmentedTrack.user, trak });
1339+
}
1340+
1341+
const fragmentDuration = this.moov?.mvex?.mehd.fragment_duration;
1342+
1343+
if (mode === 'per-track') {
1344+
return tracksToInitialize.map(({ id, user, trak }) => {
1345+
const moov = new moovBox();
1346+
moov.addBox(this.moov.mvhd);
1347+
moov.addBox(trak);
1348+
1349+
return {
1350+
id,
1351+
user,
1352+
buffer: ISOFile.writeInitializationSegment(this.ftyp, moov, fragmentDuration),
1353+
};
1354+
});
1355+
}
1356+
1357+
// Create the moov that will hold all selected fragmented tracks
1358+
const moov = new moovBox();
1359+
moov.addBox(this.moov.mvhd);
1360+
for (const track of tracksToInitialize) {
1361+
moov.addBox(track.trak);
13301362
}
13311363

13321364
return {
1333-
tracks: moov.traks.map((trak, i) => ({
1334-
id: trak.tkhd.track_id,
1335-
user: this.fragmentedTracks[i].user,
1336-
})),
1337-
buffer: ISOFile.writeInitializationSegment(
1338-
this.ftyp,
1339-
moov,
1340-
this.moov?.mvex?.mehd.fragment_duration,
1341-
),
1365+
tracks: tracksToInitialize.map(({ id, user }) => ({ id, user })),
1366+
buffer: ISOFile.writeInitializationSegment(this.ftyp, moov, fragmentDuration),
13421367
};
13431368
}
13441369

tests/segmentation.test.ts

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ describe('File Segmentation', () => {
3131
const init = mp4.initializeSegmentation();
3232

3333
// Write the initialization segments to the output stream
34-
out.insertBuffer(init.buffer);
34+
out.insertBuffer(MP4BoxBuffer.fromArrayBuffer(init.buffer, offset));
3535
offset += init.buffer.byteLength;
3636
saveBufferToFile(init.buffer, task.id, true);
3737

@@ -81,7 +81,7 @@ describe('File Segmentation', () => {
8181
const init = mp4.initializeSegmentation();
8282

8383
// Write the initialization segments to the output stream
84-
out.insertBuffer(init.buffer);
84+
out.insertBuffer(MP4BoxBuffer.fromArrayBuffer(init.buffer, offset));
8585
offset += init.buffer.byteLength;
8686
saveBufferToFile(init.buffer, task.id, true);
8787

@@ -128,7 +128,7 @@ describe('File Segmentation', () => {
128128
const init = mp4.initializeSegmentation();
129129

130130
// Write the initialization segments to the output stream
131-
out.insertBuffer(init.buffer);
131+
out.insertBuffer(MP4BoxBuffer.fromArrayBuffer(init.buffer, offset));
132132
offset += init.buffer.byteLength;
133133
saveBufferToFile(init.buffer, task.id, true);
134134

@@ -178,7 +178,7 @@ describe('File Segmentation', () => {
178178
const init = mp4.initializeSegmentation();
179179

180180
// Write the initialization segments to the output stream
181-
out.insertBuffer(init.buffer);
181+
out.insertBuffer(MP4BoxBuffer.fromArrayBuffer(init.buffer, offset));
182182
offset += init.buffer.byteLength;
183183
saveBufferToFile(init.buffer, task.id, true);
184184

@@ -291,4 +291,59 @@ describe('File Segmentation', () => {
291291
expect(fragTrack.state.accumulatedSize).toBe(0);
292292
expect(fragTrack.segmentStream).toBeUndefined();
293293
});
294+
295+
it('with one init segment per fragmented track', async () => {
296+
const { testFile } = getFilePath('isobmff', '01_simple.mp4');
297+
const { mp4 } = await loadAndGetInfo(testFile, true, true);
298+
299+
mp4.setSegmentOptions(101, undefined, {
300+
nbSamples: 50,
301+
rapAlignement: false,
302+
});
303+
mp4.setSegmentOptions(201, undefined, {
304+
nbSamples: 50,
305+
rapAlignement: false,
306+
});
307+
308+
const initSegments = mp4.initializeSegmentation('per-track');
309+
expect(initSegments.length).toBe(2);
310+
311+
for (const initSegment of initSegments) {
312+
const initMp4 = createFile();
313+
initMp4.appendBuffer(MP4BoxBuffer.fromArrayBuffer(initSegment.buffer, 0));
314+
initMp4.flush();
315+
316+
const info = initMp4.getInfo();
317+
expect(info.tracks.length).toBe(1);
318+
expect(info.tracks[0].id).toBe(initSegment.id);
319+
expect(initMp4.moov.mvex.trexs.length).toBe(1);
320+
expect(initMp4.moov.mvex.trexs[0].track_id).toBe(initSegment.id);
321+
}
322+
});
323+
324+
it('with explicit combined mode', async () => {
325+
const { testFile } = getFilePath('isobmff', '01_simple.mp4');
326+
const { mp4 } = await loadAndGetInfo(testFile, true, true);
327+
328+
mp4.setSegmentOptions(101, undefined, {
329+
nbSamples: 50,
330+
rapAlignement: false,
331+
});
332+
mp4.setSegmentOptions(201, undefined, {
333+
nbSamples: 50,
334+
rapAlignement: false,
335+
});
336+
337+
const defaultInit = mp4.initializeSegmentation();
338+
const combinedInit = mp4.initializeSegmentation('combined');
339+
340+
expect(combinedInit.tracks.map(track => track.id)).toEqual(
341+
defaultInit.tracks.map(track => track.id),
342+
);
343+
344+
const initMp4 = createFile();
345+
initMp4.appendBuffer(MP4BoxBuffer.fromArrayBuffer(combinedInit.buffer, 0));
346+
initMp4.flush();
347+
expect(initMp4.getInfo().tracks.length).toBe(2);
348+
});
294349
});

0 commit comments

Comments
 (0)