| // mp4-generator.js — In-memory fragmented MP4 generator for MSE layout tests. |
| // |
| // Generates valid fMP4 init segments and media segments that go through the real |
| // platform parser pipeline (SourceBufferParserAVFObjC / AVStreamDataParser), unlike |
| // mock-media-source.js which uses a synthetic format bypassing all real parsing. |
| // |
| // Usage: |
| // const { init, media } = MP4.samples({ |
| // timescale: 1000, |
| // track: { id: 1, type: 'video' }, |
| // samples: [ |
| // { pts: 0, dts: 0, duration: 1000, isSync: true }, |
| // { pts: 1000, dts: 1000, duration: 1000, isSync: false }, |
| // ] |
| // }); |
| // sourceBuffer.appendBuffer(init); |
| // // ... wait for updateend ... |
| // sourceBuffer.appendBuffer(media); |
| // |
| // In-band CEA-608 captions: add a second track with type 'captions' and supply |
| // caption samples built by Captions.buildPaintOnCueSamples() from caption-generator.js. |
| // |
| // <script src="caption-generator.js"></script> |
| // <script src="mp4-generator.js"></script> |
| // const captionSamples = Captions.buildPaintOnCueSamples({ cues: [...], totalDurationTicks }); |
| // const { init, media } = MP4.samples({ |
| // timescale: 30000, |
| // tracks: [{ id: 1, type: 'video' }, { id: 2, type: 'captions' }], |
| // trackSamples: { |
| // 1: videoSamples, |
| // 2: captionSamples.map(s => ({ duration: s.duration, data: s.data, isSync: true, dts: FRAME_DURATION, pts: FRAME_DURATION })), |
| // }, |
| // }); |
| |
| const MP4 = (function() { |
| |
| // ======================================================================== |
| // Encoding Utilities |
| // ======================================================================== |
| |
| function uint8(v) { |
| return new Uint8Array([v & 0xFF]); |
| } |
| |
| function uint16(v) { |
| return new Uint8Array([(v >> 8) & 0xFF, v & 0xFF]); |
| } |
| |
| function uint32(v) { |
| return new Uint8Array([ |
| (v >> 24) & 0xFF, (v >> 16) & 0xFF, |
| (v >> 8) & 0xFF, v & 0xFF |
| ]); |
| } |
| |
| function int32(v) { |
| const buf = new ArrayBuffer(4); |
| new DataView(buf).setInt32(0, v, false); |
| return new Uint8Array(buf); |
| } |
| |
| function uint64(v) { |
| const hi = Math.floor(v / 0x100000000); |
| const lo = v >>> 0; |
| return new Uint8Array([ |
| (hi >> 24) & 0xFF, (hi >> 16) & 0xFF, (hi >> 8) & 0xFF, hi & 0xFF, |
| (lo >> 24) & 0xFF, (lo >> 16) & 0xFF, (lo >> 8) & 0xFF, lo & 0xFF |
| ]); |
| } |
| |
| function fourCC(str) { |
| return new Uint8Array([ |
| str.charCodeAt(0), str.charCodeAt(1), |
| str.charCodeAt(2), str.charCodeAt(3) |
| ]); |
| } |
| |
| function concat(...arrays) { |
| let totalLength = 0; |
| for (const arr of arrays) |
| totalLength += arr.byteLength; |
| const result = new Uint8Array(totalLength); |
| let offset = 0; |
| for (const arr of arrays) { |
| result.set(arr instanceof Uint8Array ? arr : new Uint8Array(arr), offset); |
| offset += arr.byteLength; |
| } |
| return result; |
| } |
| |
| function zeros(n) { |
| return new Uint8Array(n); |
| } |
| |
| // ======================================================================== |
| // Box Primitives (ISO 14496-12) |
| // ======================================================================== |
| |
| function box(type, ...payloads) { |
| const payload = concat(...payloads); |
| const size = 8 + payload.byteLength; |
| return concat(uint32(size), fourCC(type), payload); |
| } |
| |
| function fullBox(type, version, flags, ...payloads) { |
| return box(type, uint8(version), new Uint8Array([ |
| (flags >> 16) & 0xFF, (flags >> 8) & 0xFF, flags & 0xFF |
| ]), ...payloads); |
| } |
| |
| // ======================================================================== |
| // Hardcoded Codec Configuration |
| // ======================================================================== |
| |
| // Generated with: ffmpeg -f lavfi -i "color=c=black:s=640x480:r=24" -frames:v 2 |
| // -c:v libx264 -profile:v main -level 3.0 -bf 0 -refs 1 |
| // -movflags frag_keyframe+empty_moov+default_base_moof |
| // H.264 Main Profile, Level 3.0, 640x480 |
| |
| const H264_SPS = new Uint8Array([ |
| 0x67, 0x4D, 0x40, 0x1E, 0xDA, 0x02, 0x80, 0xF6, |
| 0xC0, 0x44, 0x00, 0x00, 0x03, 0x00, 0x04, 0x00, |
| 0x00, 0x03, 0x00, 0xC0, 0x3C, 0x58, 0xBA, 0x80 |
| ]); |
| |
| const H264_PPS = new Uint8Array([0x68, 0xEF, 0x0F, 0x2C, 0x80]); |
| |
| const VIDEO_WIDTH = 640; |
| const VIDEO_HEIGHT = 480; |
| const VIDEO_TIMESCALE_DEFAULT = 19200; |
| |
| // AAC-LC AudioSpecificConfig: 44100 Hz, stereo |
| const AAC_AUDIO_SPECIFIC_CONFIG = new Uint8Array([0x12, 0x10]); |
| const AUDIO_SAMPLE_RATE = 44100; |
| const AUDIO_CHANNELS = 2; |
| const AUDIO_TIMESCALE_DEFAULT = 44100; |
| |
| const VIDEO_CODEC_STRING = 'avc1.4d401e'; |
| const AUDIO_CODEC_STRING = 'mp4a.40.2'; |
| |
| // Valid H.264 sample data generated by libx264 for a black 640x480 frame. |
| // Real NAL units with 4-byte length prefixes that the decoder can process |
| // without triggering MEDIA_ERR_DECODE errors. |
| const VIDEO_SYNC_SAMPLE = new Uint8Array([ |
| 0x00, 0x00, 0x00, 0x64, 0x65, 0x88, 0x84, 0x0B, |
| 0xFF, 0xFE, 0xF6, 0xAE, 0xFC, 0xCB, 0x2B, 0x74, |
| 0x7E, 0x95, 0x2E, 0x1D, 0x59, 0x7B, 0xB3, 0x51, |
| 0xF2, 0xE8, 0x49, 0x72, 0xFD, 0x88, 0x30, 0x7D, |
| 0x68, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, |
| 0x00, 0x08, 0x07, 0xEA, 0x2F, 0x7A, 0xE6, 0x31, |
| 0x2D, 0xF2, 0x90, 0x00, 0x00, 0x04, 0xF0, 0x01, |
| 0x1C, 0x06, 0x88, 0x3B, 0x42, 0xBA, 0x22, 0xE2, |
| 0x10, 0x23, 0x83, 0x3C, 0x4B, 0x08, 0x50, 0xE9, |
| 0x19, 0x40, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, |
| 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, |
| 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, |
| 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x12, 0xF1 |
| ]); |
| |
| const VIDEO_NONSYNC_SAMPLE = new Uint8Array([ |
| 0x00, 0x00, 0x00, 0x11, 0x41, 0x9A, 0x26, 0x21, |
| 0x5F, 0xFE, 0x38, 0x40, 0x00, 0x00, 0x03, 0x00, |
| 0x00, 0x03, 0x00, 0x05, 0x4C |
| ]); |
| |
| // Valid AAC-LC silence frame for 44100 Hz stereo, generated by libfdk-aac. |
| const AUDIO_SAMPLE_DATA = new Uint8Array([ |
| 0x21, 0x10, 0x04, 0x60, 0x8C, 0x1C |
| ]); |
| |
| // ======================================================================== |
| // Init Segment Box Builders |
| // ======================================================================== |
| |
| function ftyp() { |
| return box('ftyp', |
| fourCC('isom'), |
| uint32(1), |
| fourCC('isom'), fourCC('iso6'), fourCC('dsms'), |
| fourCC('msix'), fourCC('dash') |
| ); |
| } |
| |
| function mvhd(timescale, duration) { |
| return fullBox('mvhd', 0, 0, |
| uint32(0), uint32(0), |
| uint32(timescale), uint32(duration), |
| uint32(0x00010000), // rate = 1.0 |
| uint16(0x0100), // volume = 1.0 |
| zeros(10), |
| uint32(0x00010000), zeros(4), zeros(4), |
| zeros(4), uint32(0x00010000), zeros(4), |
| zeros(4), zeros(4), uint32(0x40000000), |
| zeros(24), |
| uint32(0xFFFF) |
| ); |
| } |
| |
| function tkhd(trackId, duration, width, height, isAudio) { |
| return fullBox('tkhd', 0, 0x000003, |
| uint32(0), uint32(0), |
| uint32(trackId), |
| zeros(4), |
| uint32(duration), |
| zeros(8), |
| uint16(0), |
| uint16(isAudio ? 1 : 0), |
| uint16(isAudio ? 0x0100 : 0), |
| zeros(2), |
| uint32(0x00010000), zeros(4), zeros(4), |
| zeros(4), uint32(0x00010000), zeros(4), |
| zeros(4), zeros(4), uint32(0x40000000), |
| uint32((isAudio ? 0 : width) << 16), |
| uint32((isAudio ? 0 : height) << 16) |
| ); |
| } |
| |
| function mdhd(timescale, duration) { |
| return fullBox('mdhd', 0, 0, |
| uint32(0), uint32(0), |
| uint32(timescale), uint32(duration), |
| uint16(0x55C4), // language = 'und' |
| zeros(2) |
| ); |
| } |
| |
| function hdlr(handlerType, name) { |
| const nameBytes = new Uint8Array(name.length + 1); |
| for (let i = 0; i < name.length; i++) |
| nameBytes[i] = name.charCodeAt(i); |
| return fullBox('hdlr', 0, 0, |
| zeros(4), fourCC(handlerType), zeros(12), nameBytes |
| ); |
| } |
| |
| function vmhd() { |
| return fullBox('vmhd', 0, 1, zeros(8)); |
| } |
| |
| function smhd() { |
| return fullBox('smhd', 0, 0, zeros(4)); |
| } |
| |
| function dinf() { |
| return box('dinf', |
| fullBox('dref', 0, 0, uint32(1), fullBox('url ', 0, 1)) |
| ); |
| } |
| |
| function avcC() { |
| return box('avcC', |
| uint8(1), uint8(0x4D), uint8(0x40), uint8(0x1E), |
| uint8(0xFF), uint8(0xE1), |
| uint16(H264_SPS.byteLength), H264_SPS, |
| uint8(1), |
| uint16(H264_PPS.byteLength), H264_PPS |
| ); |
| } |
| |
| function avc1() { |
| return box('avc1', |
| zeros(6), uint16(1), zeros(16), |
| uint16(VIDEO_WIDTH), uint16(VIDEO_HEIGHT), |
| uint32(0x00480000), uint32(0x00480000), |
| zeros(4), uint16(1), zeros(32), |
| uint16(0x0018), uint16(0xFFFF), |
| avcC() |
| ); |
| } |
| |
| function esds() { |
| return fullBox('esds', 0, 0, |
| new Uint8Array([ |
| 0x03, 0x19, 0x00, 0x00, 0x00, |
| 0x04, 0x11, 0x40, 0x15, |
| 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, |
| 0x05, AAC_AUDIO_SPECIFIC_CONFIG.byteLength, |
| ]), |
| AAC_AUDIO_SPECIFIC_CONFIG, |
| new Uint8Array([0x06, 0x01, 0x02]) |
| ); |
| } |
| |
| function mp4a() { |
| return box('mp4a', |
| zeros(6), uint16(1), zeros(8), |
| uint16(AUDIO_CHANNELS), uint16(16), zeros(4), |
| uint32(AUDIO_SAMPLE_RATE << 16), |
| esds() |
| ); |
| } |
| |
| // Closed-caption sample entry. QuickTime/ISO form: 6-byte reserved + 2-byte |
| // data_reference_index, with no codec-specific fields. Valid for both 'c608' |
| // and 'c708' — only the fourcc differs. |
| function c608Entry() { |
| return box('c608', zeros(6), uint16(1)); |
| } |
| |
| // Null media header — 12-byte fullbox with no payload. ISO BMFF standard |
| // header for non-av tracks (subtitles, metadata, captions). |
| function nmhd() { |
| return fullBox('nmhd', 0, 0); |
| } |
| |
| function stsd(trackType) { |
| let entry; |
| if (trackType === 'video') |
| entry = avc1(); |
| else if (trackType === 'captions') |
| entry = c608Entry(); |
| else if (trackType === 'audio') |
| entry = mp4a(); |
| else |
| throw new Error(`stsd: unknown track type '${trackType}'`); |
| |
| return fullBox('stsd', 0, 0, |
| uint32(1), |
| entry |
| ); |
| } |
| |
| function stbl(trackType) { |
| return box('stbl', |
| stsd(trackType), |
| fullBox('stts', 0, 0, uint32(0)), |
| fullBox('stsc', 0, 0, uint32(0)), |
| fullBox('stsz', 0, 0, uint32(0), uint32(0)), |
| fullBox('stco', 0, 0, uint32(0)) |
| ); |
| } |
| |
| function minf(trackType) { |
| let header; |
| if (trackType === 'video') |
| header = vmhd(); |
| else if (trackType === 'captions') |
| header = nmhd(); |
| else if (trackType === 'audio') |
| header = smhd(); |
| else |
| throw new Error(`minf: unknown track type '${trackType}'`); |
| |
| return box('minf', |
| header, |
| dinf(), stbl(trackType) |
| ); |
| } |
| |
| function mdia(trackType, timescale, duration) { |
| let handlerType, handlerName; |
| if (trackType === 'video') { |
| handlerType = 'vide'; |
| handlerName = 'VideoHandler'; |
| } else if (trackType === 'captions') { |
| handlerType = 'clcp'; |
| handlerName = 'Closed Caption Media Handler'; |
| } else if (trackType === 'audio') { |
| handlerType = 'soun'; |
| handlerName = 'SoundHandler'; |
| } else |
| throw new Error(`mdia: unknown track type '${trackType}'`); |
| |
| return box('mdia', |
| mdhd(timescale, duration), |
| hdlr(handlerType, handlerName), |
| minf(trackType) |
| ); |
| } |
| |
| function trak(trackDef) { |
| let defaultTimescale; |
| if (trackDef.type === 'video') |
| defaultTimescale = VIDEO_TIMESCALE_DEFAULT; |
| else if (trackDef.type === 'captions') |
| defaultTimescale = VIDEO_TIMESCALE_DEFAULT; |
| else if (trackDef.type === 'audio') |
| defaultTimescale = AUDIO_TIMESCALE_DEFAULT; |
| else |
| throw new Error(`trak: unknown track type '${trackDef.type}'`); |
| |
| // 'captions' and 'audio' both want width/height = 0. Only 'video' carries geometry. |
| const isGeometric = trackDef.type === 'video'; |
| const timescale = trackDef.timescale || defaultTimescale; |
| return box('trak', |
| tkhd(trackDef.id, 0, VIDEO_WIDTH, VIDEO_HEIGHT, !isGeometric), |
| mdia(trackDef.type, timescale, 0) |
| ); |
| } |
| |
| function trex(trackId) { |
| return fullBox('trex', 0, 0, |
| uint32(trackId), uint32(1), uint32(0), uint32(0), uint32(0) |
| ); |
| } |
| |
| function moov(movieTimescale, tracks) { |
| return box('moov', |
| mvhd(movieTimescale, 0), |
| box('mvex', ...tracks.map(t => trex(t.id))), |
| ...tracks.map(t => trak(t)) |
| ); |
| } |
| |
| // ======================================================================== |
| // Media Segment Box Builders |
| // ======================================================================== |
| |
| function mfhd(sequenceNumber) { |
| return fullBox('mfhd', 0, 0, uint32(sequenceNumber)); |
| } |
| |
| function tfhd(trackId) { |
| return fullBox('tfhd', 0, 0x020000, uint32(trackId)); |
| } |
| |
| function tfdt(baseDecodeTime, use64bit) { |
| if (use64bit) |
| return fullBox('tfdt', 1, 0, uint64(baseDecodeTime)); |
| return fullBox('tfdt', 0, 0, uint32(baseDecodeTime)); |
| } |
| |
| function sampleDataSize(isSync, trackType) { |
| if (trackType === 'video') |
| return isSync ? VIDEO_SYNC_SAMPLE.byteLength : VIDEO_NONSYNC_SAMPLE.byteLength; |
| if (trackType === 'audio') |
| return AUDIO_SAMPLE_DATA.byteLength; |
| |
| throw new Error(`sampleDataSize: unknown track type '${trackType}'`); |
| } |
| |
| function trun(samples, dataOffset, trackType) { |
| const needsSigned = samples.some(s => (s.compositionTimeOffset || 0) < 0); |
| const version = needsSigned ? 1 : 0; |
| // data-offset | sample-duration | sample-size | sample-flags | sample-cto |
| const flags = 0x01 | 0x100 | 0x200 | 0x400 | 0x800; |
| |
| const entries = []; |
| for (const sample of samples) { |
| const size = sample.size || (sample.data ? sample.data.byteLength : sampleDataSize(sample.isSync, trackType)); |
| // ISO 14496-12: sample_depends_on (bits 25-24), sample_is_non_sync_sample (bit 16) |
| const sampleFlags = sample.isSync ? 0x01000000 : 0x02010000; |
| const cto = sample.compositionTimeOffset || 0; |
| entries.push(concat( |
| uint32(sample.duration), uint32(size), uint32(sampleFlags), |
| needsSigned ? int32(cto) : uint32(cto) |
| )); |
| } |
| |
| return fullBox('trun', version, flags, |
| uint32(samples.length), int32(dataOffset), ...entries |
| ); |
| } |
| |
| function buildMdatData(samples, trackType) { |
| const parts = []; |
| for (const sample of samples) { |
| if (sample.data) |
| parts.push(sample.data); |
| else if (trackType === 'video') |
| parts.push(sample.isSync ? VIDEO_SYNC_SAMPLE : VIDEO_NONSYNC_SAMPLE); |
| else if (trackType === 'audio') |
| parts.push(AUDIO_SAMPLE_DATA.slice(0, sample.size || AUDIO_SAMPLE_DATA.byteLength)); |
| else |
| throw new Error(`buildMdatData: unknown track type '${trackType}'`); |
| } |
| return concat(...parts); |
| } |
| |
| // ======================================================================== |
| // Public API |
| // ======================================================================== |
| |
| return { |
| VIDEO_CODEC: VIDEO_CODEC_STRING, |
| AUDIO_CODEC: AUDIO_CODEC_STRING, |
| |
| VIDEO_TYPE: `video/mp4; codecs="${VIDEO_CODEC_STRING}"`, |
| AUDIO_TYPE: `audio/mp4; codecs="${AUDIO_CODEC_STRING}"`, |
| AV_TYPE: `video/mp4; codecs="${VIDEO_CODEC_STRING},${AUDIO_CODEC_STRING}"`, |
| |
| // Concatenate Uint8Arrays. Exposed for tests that need to combine |
| // init and media segments into a single appendBuffer call. |
| concat, |
| |
| // Generate a valid fMP4 initialization segment. |
| // |
| // options.timescale - Movie-level timescale (default: first track's timescale) |
| // options.tracks[] - Array of track definitions: |
| // .id - Track ID (integer) |
| // .type - 'video', 'audio', or 'captions' |
| // .timescale - Track timescale (optional, defaults per type) |
| initSegment(options) { |
| const tracks = options.tracks || [{ id: 1, type: 'video' }]; |
| for (const trackDef of tracks) { |
| if (!trackDef.timescale) { |
| if (trackDef.type === 'video' || trackDef.type === 'captions') |
| trackDef.timescale = VIDEO_TIMESCALE_DEFAULT; |
| else if (trackDef.type === 'audio') |
| trackDef.timescale = AUDIO_TIMESCALE_DEFAULT; |
| else |
| throw new Error(`initSegment: unknown track type '${trackDef.type}'`); |
| } |
| } |
| const movieTimescale = options.timescale || tracks[0].timescale; |
| return concat(ftyp(), moov(movieTimescale, tracks)); |
| }, |
| |
| // Generate a valid fMP4 media segment (moof + mdat). |
| // |
| // options.sequenceNumber - Fragment sequence number (default: 1) |
| // options.tracks[] - Array of track data: |
| // .id - Track ID (must match init segment) |
| // .type - 'video' or 'audio' |
| // .baseDecodeTime - Base decode time in track timescale units |
| // .samples[] - Array of sample descriptors: |
| // .duration - Sample duration in timescale units |
| // .compositionTimeOffset - CTO (PTS = DTS + CTO), default 0 |
| // .isSync - true for sync/keyframe samples |
| // .size - Override sample data size (optional) |
| // .data - Override sample data (Uint8Array, optional) |
| mediaSegment(options) { |
| const seqNum = options.sequenceNumber || 1; |
| const tracks = options.tracks; |
| |
| const trafs = []; |
| const mdatParts = []; |
| |
| for (const track of tracks) { |
| const trackType = track.type || 'video'; |
| const use64bit = track.baseDecodeTime > 0xFFFFFFFF; |
| const sampleData = buildMdatData(track.samples, trackType); |
| trafs.push({ |
| trackId: track.id, sampleData, trackType, |
| samples: track.samples, use64bit, |
| baseDecodeTime: track.baseDecodeTime, |
| }); |
| mdatParts.push(sampleData); |
| } |
| |
| const mdatBox = box('mdat', concat(...mdatParts)); |
| const mfhdBox = mfhd(seqNum); |
| |
| // First pass: build moof to measure its size (data_offset depends on it). |
| const dummyTrafs = trafs.map(t => box('traf', |
| tfhd(t.trackId), |
| tfdt(t.baseDecodeTime, t.use64bit), |
| trun(t.samples, 0, t.trackType) |
| )); |
| const moofSize = box('moof', mfhdBox, ...dummyTrafs).byteLength; |
| |
| // Second pass: build traf boxes with correct data_offsets. |
| let mdatPayloadOffset = 0; |
| const finalTrafs = trafs.map(t => { |
| const dataOffset = moofSize + 8 + mdatPayloadOffset; |
| mdatPayloadOffset += t.sampleData.byteLength; |
| return box('traf', |
| tfhd(t.trackId), |
| tfdt(t.baseDecodeTime, t.use64bit), |
| trun(t.samples, dataOffset, t.trackType) |
| ); |
| }); |
| |
| return concat(box('moof', mfhdBox, ...finalTrafs), mdatBox); |
| }, |
| |
| // Convenience: generate init + media from a flat sample list. |
| // |
| // options.timescale - Timescale for all timestamps (default: 1000) |
| // options.track - Single track: { id, type } |
| // options.tracks - Or multiple tracks: [{ id, type }, ...] |
| // options.samples - For single track: [{ pts, dts, duration, isSync }, ...] |
| // options.trackSamples - For multiple tracks: { trackId: [samples], ... } |
| // |
| // Returns { init: Uint8Array, media: Uint8Array, mimeType: string } |
| samples(options) { |
| const timescale = options.timescale || 1000; |
| |
| function convertSamples(rawSamples) { |
| return (rawSamples || []).map(s => ({ |
| duration: s.duration, |
| compositionTimeOffset: (s.pts !== undefined && s.dts !== undefined) ? (s.pts - s.dts) : 0, |
| isSync: s.isSync !== undefined ? s.isSync : !!(s.flags & 1), |
| size: s.size, |
| data: s.data, |
| })); |
| } |
| |
| function minDts(rawSamples) { |
| if (!rawSamples || !rawSamples.length) |
| return 0; |
| return Math.max(0, Math.min(...rawSamples.map(s => s.dts !== undefined ? s.dts : s.pts))); |
| } |
| |
| if (options.track) { |
| const track = { id: options.track.id || 1, type: options.track.type || 'video', timescale }; |
| const init = this.initSegment({ timescale, tracks: [track] }); |
| const media = this.mediaSegment({ |
| sequenceNumber: options.sequenceNumber || 1, |
| tracks: [{ |
| id: track.id, type: track.type, |
| baseDecodeTime: minDts(options.samples), |
| samples: convertSamples(options.samples), |
| }] |
| }); |
| const mimeType = (track.type === 'video') ? this.VIDEO_TYPE : this.AUDIO_TYPE; |
| return { init, media, mimeType }; |
| } |
| |
| const tracks = (options.tracks || []).map(t => ({ |
| id: t.id, type: t.type || 'video', timescale, |
| })); |
| const init = this.initSegment({ timescale, tracks }); |
| const trackSamples = options.trackSamples || {}; |
| const media = this.mediaSegment({ |
| sequenceNumber: options.sequenceNumber || 1, |
| tracks: tracks.map(t => ({ |
| id: t.id, type: t.type, |
| baseDecodeTime: minDts(trackSamples[t.id]), |
| samples: convertSamples(trackSamples[t.id]), |
| })), |
| }); |
| |
| const hasVideo = tracks.some(t => t.type === 'video'); |
| const hasAudio = tracks.some(t => t.type === 'audio'); |
| let mimeType; |
| if (hasVideo && hasAudio) |
| mimeType = this.AV_TYPE; |
| else if (hasVideo) |
| mimeType = this.VIDEO_TYPE; |
| else |
| mimeType = this.AUDIO_TYPE; |
| return { init, media, mimeType }; |
| }, |
| |
| // Generate only a media segment from a flat sample list (for appends |
| // after the init segment has already been sent). |
| mediaSamples(options) { |
| const timescale = options.timescale || 1000; |
| const track = options.track || { id: 1, type: 'video' }; |
| const rawSamples = options.samples || []; |
| const samples = rawSamples.map(s => ({ |
| duration: s.duration, |
| compositionTimeOffset: (s.pts !== undefined && s.dts !== undefined) ? (s.pts - s.dts) : 0, |
| isSync: s.isSync !== undefined ? s.isSync : !!(s.flags & 1), |
| size: s.size, |
| data: s.data, |
| })); |
| const baseDecodeTime = rawSamples.length |
| ? Math.max(0, Math.min(...rawSamples.map(s => s.dts !== undefined ? s.dts : s.pts))) |
| : 0; |
| return this.mediaSegment({ |
| sequenceNumber: options.sequenceNumber || 1, |
| tracks: [{ |
| id: track.id || 1, type: track.type || 'video', |
| baseDecodeTime, samples, |
| }] |
| }); |
| }, |
| }; |
| })(); |
| |