import { toM3u8, generateSidxKey, addMediaSequenceValues, flattenMediaGroupPlaylists } from '../src/toM3u8'; import QUnit from 'qunit'; QUnit.module('toM3u8'); QUnit.test('playlists', function(assert) { const dashPlaylists = [{ attributes: { id: '1', codecs: 'foo;bar', sourceDuration: 100, duration: 0, bandwidth: 20000, periodStart: 0, mimeType: 'audio/mp4', type: 'static' }, segments: [] }, { attributes: { id: '2', codecs: 'foo;bar', sourceDuration: 100, duration: 0, bandwidth: 10000, periodStart: 0, mimeType: 'audio/mp4', type: 'static' }, segments: [] }, { attributes: { sourceDuration: 100, id: '1', width: 800, height: 600, codecs: 'foo;bar', duration: 0, bandwidth: 10000, frameRate: 30, periodStart: 0, mimeType: 'video/mp4', type: 'static' }, segments: [] }, { attributes: { sourceDuration: 100, id: '1', bandwidth: 20000, periodStart: 0, mimeType: 'text/vtt', type: 'static', baseUrl: 'https://www.example.com/vtt' } }, { attributes: { sourceDuration: 100, id: '2', bandwidth: 10000, periodStart: 0, mimeType: 'text/vtt', type: 'static', baseUrl: 'https://www.example.com/vtt' } }]; const expected = { allowCache: true, discontinuityStarts: [], timelineStarts: [{ start: 0, timeline: 0 }], duration: 100, endList: true, mediaGroups: { AUDIO: { audio: { main: { autoselect: true, default: true, language: '', playlists: [{ attributes: { BANDWIDTH: 20000, CODECS: 'foo;bar', NAME: '1', ['PROGRAM-ID']: 1 }, mediaSequence: 0, discontinuitySequence: 0, discontinuityStarts: [], timelineStarts: [{ start: 0, timeline: 0 }], endList: true, resolvedUri: '', segments: [], timeline: 0, uri: '', targetDuration: 0 }, { attributes: { BANDWIDTH: 10000, CODECS: 'foo;bar', NAME: '2', ['PROGRAM-ID']: 1 }, mediaSequence: 0, discontinuitySequence: 0, discontinuityStarts: [], timelineStarts: [{ start: 0, timeline: 0 }], endList: true, resolvedUri: '', segments: [], timeline: 0, uri: '', targetDuration: 0 }], uri: '' } } }, ['CLOSED-CAPTIONS']: {}, SUBTITLES: { subs: { text: { autoselect: false, default: false, language: 'text', playlists: [{ attributes: { BANDWIDTH: 20000, NAME: '1', ['PROGRAM-ID']: 1 }, mediaSequence: 0, discontinuitySequence: 0, discontinuityStarts: [], timelineStarts: [{ start: 0, timeline: 0 }], targetDuration: 100, endList: true, resolvedUri: 'https://www.example.com/vtt', segments: [{ duration: 100, resolvedUri: 'https://www.example.com/vtt', timeline: 0, uri: 'https://www.example.com/vtt', number: 0 }], timeline: 0, uri: '' }, { attributes: { BANDWIDTH: 10000, NAME: '2', ['PROGRAM-ID']: 1 }, mediaSequence: 0, discontinuitySequence: 0, discontinuityStarts: [], timelineStarts: [{ start: 0, timeline: 0 }], targetDuration: 100, endList: true, resolvedUri: 'https://www.example.com/vtt', segments: [{ duration: 100, resolvedUri: 'https://www.example.com/vtt', timeline: 0, uri: 'https://www.example.com/vtt', number: 0 }], timeline: 0, uri: '' }], uri: '' } } }, VIDEO: {} }, playlists: [{ attributes: { AUDIO: 'audio', SUBTITLES: 'subs', BANDWIDTH: 10000, CODECS: 'foo;bar', NAME: '1', ['FRAME-RATE']: 30, ['PROGRAM-ID']: 1, RESOLUTION: { height: 600, width: 800 } }, endList: true, mediaSequence: 0, discontinuitySequence: 0, discontinuityStarts: [], timelineStarts: [{ start: 0, timeline: 0 }], targetDuration: 0, resolvedUri: '', segments: [], timeline: 0, uri: '' }], segments: [], uri: '' }; assert.deepEqual(toM3u8({ dashPlaylists }), expected); }); QUnit.test('playlists with content steering and resolvable URLs', function(assert) { const contentSteering = { defaultServiceLocation: 'beta', proxyServerURL: 'http://127.0.0.1:3455/steer', queryBeforeStart: false, serverURL: 'https://example.com/app/url' }; const dashPlaylists = [ { attributes: { bandwidth: 5000000, baseUrl: 'https://cdn1.example.com/video', clientOffset: 0, codecs: 'avc1.64001e', duration: 0, height: 404, id: 'test', mimeType: 'video/mp4', periodStart: 0, role: { value: 'main' }, serviceLocation: 'alpha', sourceDuration: 0, type: 'dyanmic', width: 720 }, segments: [ { duration: 0, map: { resolvedUri: 'https://cdn1.example.com/video', uri: '' }, number: 1, presentationTime: 0, resolvedUri: 'https://cdn1.example.com/video', timeline: 0, uri: '' } ] }, { attributes: { bandwidth: 5000000, baseUrl: 'https://cdn2.example.com/video', clientOffset: 0, codecs: 'avc1.64001e', duration: 0, height: 404, id: 'test', mimeType: 'video/mp4', periodStart: 0, role: { value: 'main' }, serviceLocation: 'beta', sourceDuration: 0, type: 'dyanmic', width: 720 }, segments: [ { duration: 0, map: { resolvedUri: 'https://cdn2.example.com/video', uri: '' }, number: 1, presentationTime: 0, resolvedUri: 'https://cdn2.example.com/video', timeline: 0, uri: '' } ] }, { attributes: { bandwidth: 256, baseUrl: 'https://cdn1.example.com/vtt', clientOffset: 0, duration: 0, id: 'en', lang: 'en', mimeType: 'text/vtt', periodStart: 0, role: {}, serviceLocation: 'alpha', sourceDuration: 0, type: 'dyanmic' }, segments: [ { duration: 0, map: { resolvedUri: 'https://cdn1.example.com/vtt', uri: '' }, number: 1, presentationTime: 0, resolvedUri: 'https://cdn1.example.com/vtt', timeline: 0, uri: '' } ] }, { attributes: { bandwidth: 256, baseUrl: 'https://cdn2.example.com/vtt', clientOffset: 0, id: 'en', lang: 'en', mimeType: 'text/vtt', periodStart: 0, role: {}, serviceLocation: 'beta', sourceDuration: 0, type: 'dyanmic' } } ]; const expected = { allowCache: true, contentSteering: { defaultServiceLocation: 'beta', proxyServerURL: 'http://127.0.0.1:3455/steer', queryBeforeStart: false, serverURL: 'https://example.com/app/url' }, discontinuityStarts: [], duration: 0, endList: true, mediaGroups: { AUDIO: {}, ['CLOSED-CAPTIONS']: {}, SUBTITLES: { subs: { en: { autoselect: false, default: false, language: 'en', playlists: [ { attributes: { BANDWIDTH: 256, NAME: 'en', ['PROGRAM-ID']: 1, serviceLocation: 'alpha' }, discontinuitySequence: 0, discontinuityStarts: [], endList: false, mediaSequence: 0, resolvedUri: 'https://cdn1.example.com/vtt', segments: [ { duration: 0, map: { resolvedUri: 'https://cdn1.example.com/vtt', uri: '' }, number: 0, presentationTime: 0, resolvedUri: 'https://cdn1.example.com/vtt', timeline: 0, uri: '' } ], targetDuration: 0, timeline: 0, timelineStarts: [ { start: 0, timeline: 0 } ], uri: '' }, { attributes: { BANDWIDTH: 256, NAME: 'en', ['PROGRAM-ID']: 1, serviceLocation: 'beta' }, discontinuitySequence: 0, discontinuityStarts: [], endList: false, mediaSequence: 0, resolvedUri: 'https://cdn2.example.com/vtt', segments: [ { duration: 0, number: 0, resolvedUri: 'https://cdn2.example.com/vtt', timeline: 0, uri: 'https://cdn2.example.com/vtt' } ], targetDuration: 0, timeline: 0, timelineStarts: [ { start: 0, timeline: 0 } ], uri: '' } ], uri: '' } } }, VIDEO: {} }, playlists: [ { attributes: { AUDIO: 'audio', BANDWIDTH: 5000000, CODECS: 'avc1.64001e', NAME: 'test', ['PROGRAM-ID']: 1, RESOLUTION: { height: 404, width: 720 }, SUBTITLES: 'subs', serviceLocation: 'alpha' }, discontinuitySequence: 0, discontinuityStarts: [], endList: false, mediaSequence: 0, resolvedUri: 'https://cdn1.example.com/video', segments: [ { duration: 0, map: { resolvedUri: 'https://cdn1.example.com/video', uri: '' }, number: 0, presentationTime: 0, resolvedUri: 'https://cdn1.example.com/video', timeline: 0, uri: '' } ], targetDuration: 0, timeline: 0, timelineStarts: [ { start: 0, timeline: 0 } ], uri: '' }, { attributes: { AUDIO: 'audio', BANDWIDTH: 5000000, CODECS: 'avc1.64001e', NAME: 'test', ['PROGRAM-ID']: 1, RESOLUTION: { height: 404, width: 720 }, SUBTITLES: 'subs', serviceLocation: 'beta' }, discontinuitySequence: 0, discontinuityStarts: [], endList: false, mediaSequence: 0, resolvedUri: 'https://cdn2.example.com/video', segments: [ { duration: 0, map: { resolvedUri: 'https://cdn2.example.com/video', uri: '' }, number: 0, presentationTime: 0, resolvedUri: 'https://cdn2.example.com/video', timeline: 0, uri: '' } ], targetDuration: 0, timeline: 0, timelineStarts: [ { start: 0, timeline: 0 } ], uri: '' } ], segments: [], timelineStarts: [ { start: 0, timeline: 0 } ], uri: '' }; assert.deepEqual(toM3u8({ dashPlaylists, contentSteering }), expected); }); QUnit.test('playlists with content steering', function(assert) { const contentSteering = { defaultServiceLocation: 'beta', proxyServerURL: 'http://127.0.0.1:3455/steer', queryBeforeStart: false, serverURL: 'https://example.com/app/url' }; const dashPlaylists = [{ attributes: { bandwidth: 5000000, baseUrl: 'https://cdn1.example.com/', clientOffset: 0, codecs: 'avc1.64001e', duration: 0, height: 404, id: 'test', mimeType: 'video/mp4', periodStart: 0, role: { value: 'main' }, serviceLocation: 'alpha', sourceDuration: 0, type: 'dyanmic', width: 720 }, segments: [ { duration: 0, map: { resolvedUri: 'https://cdn1.example.com/', uri: '' }, number: 1, presentationTime: 0, resolvedUri: 'https://cdn1.example.com/', timeline: 0, uri: '' } ] }, { attributes: { bandwidth: 5000000, baseUrl: 'https://cdn2.example.com/', clientOffset: 0, codecs: 'avc1.64001e', duration: 0, height: 404, id: 'test', mimeType: 'video/mp4', periodStart: 0, role: { value: 'main' }, serviceLocation: 'beta', sourceDuration: 0, type: 'dyanmic', width: 720 }, segments: [ { duration: 0, map: { resolvedUri: 'https://cdn2.example.com/', uri: '' }, number: 1, presentationTime: 0, resolvedUri: 'https://cdn2.example.com/', timeline: 0, uri: '' } ] }, { attributes: { bandwidth: 256, baseUrl: 'https://example.com/en.vtt', clientOffset: 0, id: 'en', lang: 'en', mimeType: 'text/vtt', periodStart: 0, role: {}, sourceDuration: 0, type: 'dyanmic' } }, { attributes: { bandwidth: 256, baseUrl: 'https://example.com/en.vtt', clientOffset: 0, id: 'en', lang: 'en', mimeType: 'text/vtt', periodStart: 0, role: {}, sourceDuration: 0, type: 'dyanmic' } }]; const expected = { allowCache: true, contentSteering: { defaultServiceLocation: 'beta', proxyServerURL: 'http://127.0.0.1:3455/steer', queryBeforeStart: false, serverURL: 'https://example.com/app/url' }, discontinuityStarts: [], duration: 0, endList: true, mediaGroups: { AUDIO: {}, ['CLOSED-CAPTIONS']: {}, SUBTITLES: { subs: { en: { autoselect: false, default: false, language: 'en', playlists: [ { attributes: { BANDWIDTH: 256, NAME: 'en', ['PROGRAM-ID']: 1 }, discontinuitySequence: 0, discontinuityStarts: [], endList: false, mediaSequence: 0, resolvedUri: 'https://example.com/en.vtt', segments: [ { duration: 0, number: 0, resolvedUri: 'https://example.com/en.vtt', timeline: 0, uri: 'https://example.com/en.vtt' } ], targetDuration: 0, timeline: 0, timelineStarts: [ { start: 0, timeline: 0 }, { start: 0, timeline: 0 } ], uri: '' } ], uri: '' } } }, VIDEO: {} }, playlists: [ { attributes: { AUDIO: 'audio', BANDWIDTH: 5000000, CODECS: 'avc1.64001e', NAME: 'test', ['PROGRAM-ID']: 1, RESOLUTION: { height: 404, width: 720 }, SUBTITLES: 'subs', serviceLocation: 'alpha' }, discontinuitySequence: 0, discontinuityStarts: [], endList: false, mediaSequence: 0, resolvedUri: 'https://cdn1.example.com/', segments: [ { duration: 0, map: { resolvedUri: 'https://cdn1.example.com/', uri: '' }, number: 0, presentationTime: 0, resolvedUri: 'https://cdn1.example.com/', timeline: 0, uri: '' } ], targetDuration: 0, timeline: 0, timelineStarts: [ { start: 0, timeline: 0 } ], uri: '' }, { attributes: { AUDIO: 'audio', BANDWIDTH: 5000000, CODECS: 'avc1.64001e', NAME: 'test', ['PROGRAM-ID']: 1, RESOLUTION: { height: 404, width: 720 }, SUBTITLES: 'subs', serviceLocation: 'beta' }, discontinuitySequence: 0, discontinuityStarts: [], endList: false, mediaSequence: 0, resolvedUri: 'https://cdn2.example.com/', segments: [ { duration: 0, map: { resolvedUri: 'https://cdn2.example.com/', uri: '' }, number: 0, presentationTime: 0, resolvedUri: 'https://cdn2.example.com/', timeline: 0, uri: '' } ], targetDuration: 0, timeline: 0, timelineStarts: [ { start: 0, timeline: 0 } ], uri: '' } ], segments: [], timelineStarts: [ { start: 0, timeline: 0 } ], uri: '' }; assert.deepEqual(toM3u8({ dashPlaylists, contentSteering }), expected); }); QUnit.test('playlists with segments', function(assert) { const dashPlaylists = [{ attributes: { id: '1', codecs: 'foo;bar', duration: 2, sourceDuration: 100, bandwidth: 20000, periodStart: 0, mimeType: 'audio/mp4', type: 'static' }, segments: [{ uri: '', timeline: 0, duration: 2, resolvedUri: '', map: { uri: '', resolvedUri: '' }, number: 0 }, { uri: '', timeline: 0, duration: 2, resolvedUri: '', map: { uri: '', resolvedUri: '' }, number: 1 }] }, { attributes: { id: '2', codecs: 'foo;bar', sourceDuration: 100, duration: 2, bandwidth: 10000, periodStart: 0, mimeType: 'audio/mp4', type: 'static' }, segments: [{ uri: '', timeline: 0, duration: 2, resolvedUri: '', map: { uri: '', resolvedUri: '' }, number: 0 }, { uri: '', timeline: 0, duration: 2, resolvedUri: '', map: { uri: '', resolvedUri: '' }, number: 1 }] }, { attributes: { sourceDuration: 100, id: '1', width: 800, duration: 2, height: 600, codecs: 'foo;bar', bandwidth: 10000, periodStart: 0, mimeType: 'video/mp4', type: 'static' }, segments: [{ uri: '', timeline: 0, duration: 2, resolvedUri: '', map: { uri: '', resolvedUri: '' }, number: 0 }, { uri: '', timeline: 0, duration: 2, resolvedUri: '', map: { uri: '', resolvedUri: '' }, number: 1 }] }, { attributes: { sourceDuration: 100, id: '1', duration: 2, bandwidth: 20000, periodStart: 0, mimeType: 'text/vtt', type: 'static', baseUrl: 'https://www.example.com/vtt' }, segments: [{ uri: '', timeline: 0, duration: 2, resolvedUri: '', map: { uri: '', resolvedUri: '' }, number: 0 }, { uri: '', timeline: 0, duration: 2, resolvedUri: '', map: { uri: '', resolvedUri: '' }, number: 1 }] }, { attributes: { sourceDuration: 100, duration: 2, id: '2', bandwidth: 10000, periodStart: 0, mimeType: 'text/vtt', type: 'static', baseUrl: 'https://www.example.com/vtt' }, segments: [{ uri: '', timeline: 0, duration: 2, resolvedUri: '', map: { uri: '', resolvedUri: '' }, number: 0 }, { uri: '', timeline: 0, duration: 2, resolvedUri: '', map: { uri: '', resolvedUri: '' }, number: 1 }] }]; const expected = { allowCache: true, discontinuityStarts: [], duration: 100, endList: true, mediaGroups: { AUDIO: { audio: { main: { autoselect: true, default: true, language: '', playlists: [{ attributes: { BANDWIDTH: 20000, CODECS: 'foo;bar', NAME: '1', ['PROGRAM-ID']: 1 }, targetDuration: 2, mediaSequence: 0, discontinuitySequence: 0, discontinuityStarts: [], endList: true, resolvedUri: '', segments: [{ uri: '', timeline: 0, duration: 2, resolvedUri: '', map: { uri: '', resolvedUri: '' }, number: 0 }, { uri: '', timeline: 0, duration: 2, resolvedUri: '', map: { uri: '', resolvedUri: '' }, number: 1 }], timeline: 0, timelineStarts: [{ start: 0, timeline: 0 }], uri: '' }, { attributes: { BANDWIDTH: 10000, CODECS: 'foo;bar', NAME: '2', ['PROGRAM-ID']: 1 }, targetDuration: 2, mediaSequence: 0, discontinuitySequence: 0, discontinuityStarts: [], endList: true, resolvedUri: '', segments: [{ uri: '', timeline: 0, duration: 2, resolvedUri: '', map: { uri: '', resolvedUri: '' }, number: 0 }, { uri: '', timeline: 0, duration: 2, resolvedUri: '', map: { uri: '', resolvedUri: '' }, number: 1 }], timeline: 0, timelineStarts: [{ start: 0, timeline: 0 }], uri: '' }], uri: '' } } }, ['CLOSED-CAPTIONS']: {}, SUBTITLES: { subs: { text: { autoselect: false, default: false, language: 'text', playlists: [{ attributes: { BANDWIDTH: 20000, NAME: '1', ['PROGRAM-ID']: 1 }, endList: true, targetDuration: 2, mediaSequence: 0, discontinuitySequence: 0, discontinuityStarts: [], resolvedUri: 'https://www.example.com/vtt', segments: [{ uri: '', timeline: 0, duration: 2, resolvedUri: '', map: { uri: '', resolvedUri: '' }, number: 0 }, { uri: '', timeline: 0, duration: 2, resolvedUri: '', map: { uri: '', resolvedUri: '' }, number: 1 }], timeline: 0, timelineStarts: [{ start: 0, timeline: 0 }], uri: '' }, { attributes: { BANDWIDTH: 10000, NAME: '2', ['PROGRAM-ID']: 1 }, endList: true, targetDuration: 2, mediaSequence: 0, discontinuitySequence: 0, discontinuityStarts: [], resolvedUri: 'https://www.example.com/vtt', segments: [{ uri: '', timeline: 0, duration: 2, resolvedUri: '', map: { uri: '', resolvedUri: '' }, number: 0 }, { uri: '', timeline: 0, duration: 2, resolvedUri: '', map: { uri: '', resolvedUri: '' }, number: 1 }], timeline: 0, timelineStarts: [{ start: 0, timeline: 0 }], uri: '' }], uri: '' } } }, VIDEO: {} }, playlists: [{ attributes: { AUDIO: 'audio', SUBTITLES: 'subs', BANDWIDTH: 10000, CODECS: 'foo;bar', NAME: '1', ['PROGRAM-ID']: 1, RESOLUTION: { height: 600, width: 800 } }, endList: true, resolvedUri: '', mediaSequence: 0, discontinuitySequence: 0, discontinuityStarts: [], targetDuration: 2, segments: [{ uri: '', timeline: 0, duration: 2, resolvedUri: '', map: { uri: '', resolvedUri: '' }, number: 0 }, { uri: '', timeline: 0, duration: 2, resolvedUri: '', map: { uri: '', resolvedUri: '' }, number: 1 }], timeline: 0, timelineStarts: [{ start: 0, timeline: 0 }], uri: '' }], segments: [], timelineStarts: [{ start: 0, timeline: 0 }], uri: '' }; assert.deepEqual(toM3u8({ dashPlaylists }), expected); }); QUnit.test('playlists with sidx and sidxMapping', function(assert) { const dashPlaylists = [{ attributes: { sourceDuration: 100, id: '1', width: 800, height: 600, codecs: 'foo;bar', duration: 0, bandwidth: 10000, periodStart: 0, mimeType: 'video/mp4', type: 'static' }, segments: [], sidx: { byterange: { offset: 10, length: 10 }, uri: 'sidx.mp4', resolvedUri: 'http://example.com/sidx.mp4', timeline: 0, duration: 10 }, uri: 'http://example.com/fmp4.mp4' }]; const sidxMapping = { 'sidx.mp4-10-19': { sidx: { timescale: 1, firstOffset: 0, references: [{ referenceType: 0, referencedSize: 5, subsegmentDuration: 2 }] } } }; const expected = [{ attributes: { AUDIO: 'audio', SUBTITLES: 'subs', BANDWIDTH: 10000, CODECS: 'foo;bar', NAME: '1', ['PROGRAM-ID']: 1, RESOLUTION: { height: 600, width: 800 } }, sidx: { byterange: { offset: 10, length: 10 }, uri: 'sidx.mp4', resolvedUri: 'http://example.com/sidx.mp4', timeline: 0, duration: 10 }, targetDuration: 0, timeline: 0, timelineStarts: [{ start: 0, timeline: 0 }], uri: '', segments: [{ map: { resolvedUri: 'http://example.com/sidx.mp4', uri: '' }, byterange: { offset: 20, length: 5 }, uri: 'http://example.com/sidx.mp4', resolvedUri: 'http://example.com/sidx.mp4', duration: 2, number: 0, presentationTime: 0, timeline: 0 }], endList: true, mediaSequence: 0, discontinuitySequence: 0, discontinuityStarts: [], resolvedUri: '' }]; assert.deepEqual(toM3u8({ dashPlaylists, sidxMapping }).playlists, expected); }); QUnit.test('playlists without minimumUpdatePeriod dont assign default value', function(assert) { const dashPlaylists = [{ attributes: { sourceDuration: 100, id: '1', width: 800, height: 600, codecs: 'foo;bar', duration: 0, bandwidth: 10000, periodStart: 0, mimeType: 'video/mp4', type: 'static' }, segments: [], sidx: { byterange: { offset: 10, length: 10 }, uri: 'sidx.mp4', resolvedUri: 'http://example.com/sidx.mp4', duration: 10 }, uri: 'http://example.com/fmp4.mp4' }]; assert.equal(toM3u8({ dashPlaylists }).minimumUpdatePeriod, undefined); }); QUnit.test('playlists with minimumUpdatePeriod = 0', function(assert) { const dashPlaylists = [{ attributes: { sourceDuration: 100, id: '1', width: 800, height: 600, codecs: 'foo;bar', duration: 0, bandwidth: 10000, periodStart: 0, mimeType: 'video/mp4', type: 'static', minimumUpdatePeriod: 0 }, segments: [], sidx: { byterange: { offset: 10, length: 10 }, uri: 'sidx.mp4', resolvedUri: 'http://example.com/sidx.mp4', duration: 10 }, uri: 'http://example.com/fmp4.mp4' }]; assert.equal(toM3u8({ dashPlaylists }).minimumUpdatePeriod, 0); }); QUnit.test('playlists with integer value for minimumUpdatePeriod', function(assert) { const dashPlaylists = [{ attributes: { sourceDuration: 100, id: '1', width: 800, height: 600, codecs: 'foo;bar', duration: 0, bandwidth: 10000, periodStart: 0, mimeType: 'video/mp4', type: 'static', minimumUpdatePeriod: 2 }, segments: [], sidx: { byterange: { offset: 10, length: 10 }, uri: 'sidx.mp4', resolvedUri: 'http://example.com/sidx.mp4', duration: 10 }, uri: 'http://example.com/fmp4.mp4' }]; assert.equal( toM3u8({ dashPlaylists }).minimumUpdatePeriod, 2000, 'converts update period to ms' ); }); QUnit.test('no playlists', function(assert) { assert.deepEqual(toM3u8({ dashPlaylists: [] }), {}); }); QUnit.test('dynamic playlists with suggestedPresentationDelay', function(assert) { const dashPlaylists = [{ attributes: { id: '1', codecs: 'foo;bar', sourceDuration: 100, duration: 0, bandwidth: 20000, periodStart: 0, mimeType: 'audio/mp4', type: 'dynamic', suggestedPresentationDelay: 18 }, segments: [] }, { attributes: { id: '2', codecs: 'foo;bar', sourceDuration: 100, duration: 0, bandwidth: 10000, periodStart: 0, mimeType: 'audio/mp4', type: 'static' }, segments: [] }, { attributes: { sourceDuration: 100, id: '1', width: 800, height: 600, codecs: 'foo;bar', duration: 0, bandwidth: 10000, periodStart: 0, mimeType: 'video/mp4', type: 'static' }, segments: [] }, { attributes: { sourceDuration: 100, id: '1', bandwidth: 20000, periodStart: 0, mimeType: 'text/vtt', type: 'static', baseUrl: 'https://www.example.com/vtt' } }, { attributes: { sourceDuration: 100, id: '1', bandwidth: 10000, periodStart: 0, mimeType: 'text/vtt', type: 'static', baseUrl: 'https://www.example.com/vtt' } }]; const output = toM3u8({ dashPlaylists }); assert.ok('suggestedPresentationDelay' in output); assert.deepEqual(output.suggestedPresentationDelay, 18); }); QUnit.test('playlists with label', function(assert) { const label = 'English with commentary'; const dashPlaylists = [{ attributes: { id: '1', codecs: 'foo;bar', sourceDuration: 100, duration: 0, bandwidth: 20000, periodStart: 0, mimeType: 'audio/mp4', type: 'dynamic', label }, segments: [] }, { attributes: { id: '2', codecs: 'foo;bar', sourceDuration: 100, duration: 0, bandwidth: 10000, periodStart: 0, mimeType: 'audio/mp4', type: 'static' }, segments: [] }, { attributes: { sourceDuration: 100, id: '1', width: 800, height: 600, codecs: 'foo;bar', duration: 0, bandwidth: 10000, periodStart: 0, mimeType: 'video/mp4', type: 'static' }, segments: [] }, { attributes: { sourceDuration: 100, id: '1', width: 800, height: 600, codecs: 'foo;bar', duration: 0, bandwidth: 10000, periodStart: 0, mimeType: 'text/vtt', type: 'static', label }, segments: [] }]; const output = toM3u8({ dashPlaylists }); assert.ok(label in output.mediaGroups.AUDIO.audio, 'label exists'); assert.ok(label in output.mediaGroups.SUBTITLES.subs, 'label exists'); }); QUnit.test('608 captions', function(assert) { const dashPlaylists = [{ attributes: { captionServices: [{ channel: 'CC1', language: 'CC1' }, { channel: 'CC2', language: 'CC2' }, { channel: undefined, language: 'English' }, { channel: 'CC4', language: 'eng' }], id: '1', codecs: 'foo;bar', sourceDuration: 100, duration: 0, bandwidth: 20000, periodStart: 0, mimeType: 'audio/mp4', type: 'dynamic' }, segments: [] }, { attributes: { id: '2', codecs: 'foo;bar', sourceDuration: 100, duration: 0, bandwidth: 10000, periodStart: 0, mimeType: 'audio/mp4', type: 'static' }, segments: [] }, { attributes: { sourceDuration: 100, id: '1', width: 800, height: 600, codecs: 'foo;bar', duration: 0, bandwidth: 10000, periodStart: 0, mimeType: 'video/mp4', type: 'static' }, segments: [] }]; const output = toM3u8({ dashPlaylists }); const cc = output.mediaGroups['CLOSED-CAPTIONS'].cc; Object.keys(cc).forEach((key) => { assert.notOk(cc[key].autoselect, 'no autoselect'); assert.notOk(cc[key].default, 'no default'); }); assert.deepEqual(Object.keys(cc), ['CC1', 'CC2', 'English', 'eng'], 'we have 4 channels'); assert.equal(cc.CC1.instreamId, 'CC1', 'CC1 has an instreamId of CC1'); assert.equal(cc.CC2.instreamId, 'CC2', 'CC2 has an instreamId of CC1'); assert.equal(cc.English.instreamId, undefined, 'English captions dont have an instreamId'); assert.equal(cc.eng.instreamId, 'CC4', 'eng captions have an instreamId of CC4'); }); QUnit.module('generateSidxKey'); QUnit.test('generates correct key', function(assert) { const sidxInfo = { byterange: { offset: 1, length: 5 }, uri: 'uri' }; assert.strictEqual( generateSidxKey(sidxInfo), 'uri-1-5', 'the key byterange should have a inclusive end' ); }); QUnit.module('addMediaSequenceValues'); QUnit.test('resets media sequence values', function(assert) { const playlists = [{ timeline: 17, mediaSequence: 2, discontinuitySequence: 3, segments: [{ number: 5, presentationTime: 17, timeline: 17 }, { number: 6, presentationTime: 19, timeline: 17 }, { number: 7, presentationTime: 21, timeline: 21 }] }, { timeline: 21, mediaSequence: 2, discontinuitySequence: 3, segments: [{ number: 1, presentationTime: 21, timeline: 21 }] }, { timeline: 17, mediaSequence: 2, discontinuitySequence: 2, segments: [] }]; const timelineStarts = [{ timeline: 17, start: 17 }, { timeline: 21, start: 21 }]; addMediaSequenceValues(playlists, timelineStarts); assert.deepEqual( playlists, [{ timeline: 17, mediaSequence: 0, discontinuitySequence: 0, segments: [{ number: 0, presentationTime: 17, timeline: 17 }, { number: 1, presentationTime: 19, timeline: 17 }, { number: 2, presentationTime: 21, timeline: 21 }] }, { timeline: 21, mediaSequence: 0, discontinuitySequence: 1, segments: [{ number: 0, presentationTime: 21, timeline: 21 }] }, { timeline: 17, mediaSequence: 0, discontinuitySequence: 0, segments: [] }], 'updated media sequence values' ); }); QUnit.module('flattenMediaGroupPlaylists'); QUnit.test('includes all media group playlists', function(assert) { assert.deepEqual( flattenMediaGroupPlaylists({ en: { playlists: [ { attributes: { NAME: 'A' } }, { attributes: { NAME: 'B' } } ] }, es: { playlists: [ { attributes: { NAME: 'C' } }, { attributes: { NAME: 'D' } } ] } }), [ { attributes: { NAME: 'A' } }, { attributes: { NAME: 'B' } }, { attributes: { NAME: 'C' } }, { attributes: { NAME: 'D' } } ], 'included all media group playlists' ); }); QUnit.module('eventStream'); QUnit.test('eventStreams with playlists', function(assert) { const dashPlaylists = [{ attributes: { id: '1', codecs: 'foo;bar', sourceDuration: 100, duration: 0, bandwidth: 20000, periodStart: 0, mimeType: 'audio/mp4', type: 'static' }, segments: [] }, { attributes: { id: '2', codecs: 'foo;bar', sourceDuration: 100, duration: 0, bandwidth: 10000, periodStart: 0, mimeType: 'audio/mp4', type: 'static' }, segments: [] }, { attributes: { sourceDuration: 100, id: '1', width: 800, height: 600, codecs: 'foo;bar', duration: 0, bandwidth: 10000, frameRate: 30, periodStart: 0, mimeType: 'video/mp4', type: 'static' }, segments: [] }, { attributes: { sourceDuration: 100, id: '1', bandwidth: 20000, periodStart: 0, mimeType: 'text/vtt', type: 'static', baseUrl: 'https://www.example.com/vtt' } }, { attributes: { sourceDuration: 100, id: '2', bandwidth: 10000, periodStart: 0, mimeType: 'text/vtt', type: 'static', baseUrl: 'https://www.example.com/vtt' } }]; const eventStream = [ { end: 1, id: 'one', messageData: 'foo', schemeIdUri: 'urn:foo.bar.2023', start: 1, value: 'bar' }, { end: 2, id: 'two', messageData: 'bar', schemeIdUri: 'urn:foo.bar.2023', start: 2, value: 'foo' }, { end: 3, id: 'three', messageData: 'foo_bar', schemeIdUri: 'urn:foo.bar.2023', start: 3, value: 'bar_foo' } ]; const expected = { allowCache: true, discontinuityStarts: [], timelineStarts: [{ start: 0, timeline: 0 }], duration: 100, endList: true, eventStream: [ { end: 1, id: 'one', messageData: 'foo', schemeIdUri: 'urn:foo.bar.2023', start: 1, value: 'bar' }, { end: 2, id: 'two', messageData: 'bar', schemeIdUri: 'urn:foo.bar.2023', start: 2, value: 'foo' }, { end: 3, id: 'three', messageData: 'foo_bar', schemeIdUri: 'urn:foo.bar.2023', start: 3, value: 'bar_foo' } ], mediaGroups: { AUDIO: { audio: { main: { autoselect: true, default: true, language: '', playlists: [{ attributes: { BANDWIDTH: 20000, CODECS: 'foo;bar', NAME: '1', ['PROGRAM-ID']: 1 }, mediaSequence: 0, discontinuitySequence: 0, discontinuityStarts: [], timelineStarts: [{ start: 0, timeline: 0 }], endList: true, resolvedUri: '', segments: [], timeline: 0, uri: '', targetDuration: 0 }, { attributes: { BANDWIDTH: 10000, CODECS: 'foo;bar', NAME: '2', ['PROGRAM-ID']: 1 }, mediaSequence: 0, discontinuitySequence: 0, discontinuityStarts: [], timelineStarts: [{ start: 0, timeline: 0 }], endList: true, resolvedUri: '', segments: [], timeline: 0, uri: '', targetDuration: 0 }], uri: '' } } }, ['CLOSED-CAPTIONS']: {}, SUBTITLES: { subs: { text: { autoselect: false, default: false, language: 'text', playlists: [{ attributes: { BANDWIDTH: 20000, NAME: '1', ['PROGRAM-ID']: 1 }, mediaSequence: 0, discontinuitySequence: 0, discontinuityStarts: [], timelineStarts: [{ start: 0, timeline: 0 }], targetDuration: 100, endList: true, resolvedUri: 'https://www.example.com/vtt', segments: [{ duration: 100, resolvedUri: 'https://www.example.com/vtt', timeline: 0, uri: 'https://www.example.com/vtt', number: 0 }], timeline: 0, uri: '' }, { attributes: { BANDWIDTH: 10000, NAME: '2', ['PROGRAM-ID']: 1 }, mediaSequence: 0, discontinuitySequence: 0, discontinuityStarts: [], timelineStarts: [{ start: 0, timeline: 0 }], targetDuration: 100, endList: true, resolvedUri: 'https://www.example.com/vtt', segments: [{ duration: 100, resolvedUri: 'https://www.example.com/vtt', timeline: 0, uri: 'https://www.example.com/vtt', number: 0 }], timeline: 0, uri: '' }], uri: '' } } }, VIDEO: {} }, playlists: [{ attributes: { AUDIO: 'audio', SUBTITLES: 'subs', BANDWIDTH: 10000, CODECS: 'foo;bar', NAME: '1', ['FRAME-RATE']: 30, ['PROGRAM-ID']: 1, RESOLUTION: { height: 600, width: 800 } }, endList: true, mediaSequence: 0, discontinuitySequence: 0, discontinuityStarts: [], timelineStarts: [{ start: 0, timeline: 0 }], targetDuration: 0, resolvedUri: '', segments: [], timeline: 0, uri: '' }], segments: [], uri: '' }; assert.deepEqual(toM3u8({ dashPlaylists, eventStream }), expected); });