diff --git a/package-lock.json b/package-lock.json index 67afa1a84..1c01baddc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7711,9 +7711,9 @@ } }, "rollup-plugin-worker-factory": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/rollup-plugin-worker-factory/-/rollup-plugin-worker-factory-0.5.4.tgz", - "integrity": "sha512-JuIwBZjiMzvqtcwZdY4gAFcS9O3+L+8hNJqWX763b95pqTY31O6T+gxWcYVWX0JuzsPOL9QKClkLN+RgBhgkQg==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/rollup-plugin-worker-factory/-/rollup-plugin-worker-factory-0.5.5.tgz", + "integrity": "sha512-kbsvueLjSntAFjBnnD7UOWTdc1zcufshG97M+Muu2s1mugtiviYpz/jR1WQyZK1u5ZA2tbSCCqRzHHe6FmpoIg==", "dev": true, "requires": { "rollup": "^2.34.2" diff --git a/package.json b/package.json index c7eced59c..be5c0362a 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "lodash-compat": "^3.10.0", "nomnoml": "^0.3.0", "rollup": "^2.36.1", - "rollup-plugin-worker-factory": "0.5.4", + "rollup-plugin-worker-factory": "0.5.5", "shelljs": "^0.8.4", "sinon": "^8.1.1", "url-toolkit": "^2.2.1", diff --git a/src/media-segment-request.js b/src/media-segment-request.js index 7f3284437..7bcefd03f 100644 --- a/src/media-segment-request.js +++ b/src/media-segment-request.js @@ -2,9 +2,8 @@ import videojs from 'video.js'; import { createTransferableMessage } from './bin-utils'; import { stringToArrayBuffer } from './util/string-to-array-buffer'; import { transmux } from './segment-transmuxer'; -import { probeTsSegment } from './util/segment'; -import mp4probe from 'mux.js/lib/mp4/probe'; import { segmentXhrHeaders } from './xhr'; +import {workerCallback} from './util/worker-callback.js'; import { detectContainerForBytes, isLikelyFmp4MediaSegment @@ -182,25 +181,34 @@ const handleInitSegmentResponse = }, segment); } - const tracks = mp4probe.tracks(segment.map.bytes); + workerCallback({ + action: 'probeMp4Tracks', + data: segment.map.bytes, + transmuxer: segment.transmuxer, + callback: ({tracks, data}) => { + // transfer bytes back to us + segment.map.bytes = data; - tracks.forEach(function(track) { - segment.map.tracks = segment.map.tracks || {}; + tracks.forEach(function(track) { + segment.map.tracks = segment.map.tracks || {}; - // only support one track of each type for now - if (segment.map.tracks[track.type]) { - return; - } + // only support one track of each type for now + if (segment.map.tracks[track.type]) { + return; + } - segment.map.tracks[track.type] = track; + segment.map.tracks[track.type] = track; + + if (typeof track.id === 'number' && track.timescale) { + segment.map.timescales = segment.map.timescales || {}; + segment.map.timescales[track.id] = track.timescale; + } + + }); - if (typeof track.id === 'number' && track.timescale) { - segment.map.timescales = segment.map.timescales || {}; - segment.map.timescales[track.id] = track.timescale; + return finishProcessingFn(null, segment); } }); - - return finishProcessingFn(null, segment); }; /** @@ -282,34 +290,7 @@ const transmuxAndNotify = ({ let videoStartFn = timingInfoFn.bind(null, segment, 'video', 'start'); const videoEndFn = timingInfoFn.bind(null, segment, 'video', 'end'); - // Check to see if we are appending a full segment. - if (!isPartial && !segment.lastReachedChar) { - // In the full segment transmuxer, we don't yet have the ability to extract a "proper" - // start time. Meaning cached frame data may corrupt our notion of where this segment - // really starts. To get around this, full segment appends should probe for the info - // needed. - const probeResult = probeTsSegment(bytes, segment.baseStartTime); - - if (probeResult) { - trackInfoFn(segment, { - hasAudio: probeResult.hasAudio, - hasVideo: probeResult.hasVideo, - isMuxed - }); - trackInfoFn = null; - - if (probeResult.hasAudio && !isMuxed) { - audioStartFn(probeResult.audioStart); - } - if (probeResult.hasVideo) { - videoStartFn(probeResult.videoStart); - } - audioStartFn = null; - videoStartFn = null; - } - } - - transmux({ + const finish = () => transmux({ bytes, transmuxer: segment.transmuxer, audioAppendStart: segment.audioAppendStart, @@ -379,6 +360,47 @@ const transmuxAndNotify = ({ doneFn(null, segment, result); } }); + + // Check to see if we are appending a full segment. + if (!isPartial && !segment.lastReachedChar) { + // In the full segment transmuxer, we don't yet have the ability to extract a "proper" + // start time. Meaning cached frame data may corrupt our notion of where this segment + // really starts. To get around this, full segment appends should probe for the info + // needed. + workerCallback({ + action: 'probeTs', + transmuxer: segment.transmuxer, + data: bytes, + baseStartTime: segment.baseStartTime, + callback: (data) => { + segment.bytes = bytes = data.data; + + const probeResult = data.result; + + if (probeResult) { + trackInfoFn(segment, { + hasAudio: probeResult.hasAudio, + hasVideo: probeResult.hasVideo, + isMuxed + }); + trackInfoFn = null; + + if (probeResult.hasAudio && !isMuxed) { + audioStartFn(probeResult.audioStart); + } + if (probeResult.hasVideo) { + videoStartFn(probeResult.videoStart); + } + audioStartFn = null; + videoStartFn = null; + } + + finish(); + } + }); + } else { + finish(); + } }; const handleSegmentBytes = ({ @@ -396,7 +418,7 @@ const handleSegmentBytes = ({ dataFn, doneFn }) => { - const bytesAsUint8Array = new Uint8Array(bytes); + let bytesAsUint8Array = new Uint8Array(bytes); // TODO: // We should have a handler that fetches the number of bytes required @@ -438,62 +460,62 @@ const handleSegmentBytes = ({ // Note that the start time returned by the probe reflects the baseMediaDecodeTime, as // that is the true start of the segment (where the playback engine should begin // decoding). - const timingInfo = mp4probe.startTime(segment.map.timescales, bytesAsUint8Array); - - if (trackInfo.hasAudio && !trackInfo.isMuxed) { - timingInfoFn(segment, 'audio', 'start', timingInfo); - } - - if (trackInfo.hasVideo) { - timingInfoFn(segment, 'video', 'start', timingInfo); - } - const finishLoading = (captions) => { // if the track still has audio at this point it is only possible // for it to be audio only. See `tracks.video && tracks.audio` if statement // above. // we make sure to use segment.bytes here as that - dataFn(segment, {data: bytes, type: trackInfo.hasAudio && !trackInfo.isMuxed ? 'audio' : 'video'}); + dataFn(segment, { + data: bytesAsUint8Array, + type: trackInfo.hasAudio && !trackInfo.isMuxed ? 'audio' : 'video' + }); if (captions && captions.length) { captionsFn(segment, captions); } doneFn(null, segment, {}); }; - // Run through the CaptionParser in case there are captions. - // Initialize CaptionParser if it hasn't been yet - if (!tracks.video || !bytes.byteLength || !segment.transmuxer) { - finishLoading(); - return; - } - - const buffer = bytes instanceof ArrayBuffer ? bytes : bytes.buffer; - const byteOffset = bytes instanceof ArrayBuffer ? 0 : bytes.byteOffset; - const listenForCaptions = (event) => { - if (event.data.action !== 'mp4Captions') { - return; - } - segment.transmuxer.removeEventListener('message', listenForCaptions); - - const data = event.data.data; - - // transfer ownership of bytes back to us. - segment.bytes = bytes = new Uint8Array(data, data.byteOffset || 0, data.byteLength); + workerCallback({ + action: 'probeMp4StartTime', + timescales: segment.map.timescales, + data: bytesAsUint8Array, + transmuxer: segment.transmuxer, + callback: ({data, startTime}) => { + // transfer bytes back to us + bytes = data.buffer; + segment.bytes = bytesAsUint8Array = data; + + if (trackInfo.hasAudio && !trackInfo.isMuxed) { + timingInfoFn(segment, 'audio', 'start', startTime); + } - finishLoading(event.data.captions); - }; + if (trackInfo.hasVideo) { + timingInfoFn(segment, 'video', 'start', startTime); + } - segment.transmuxer.addEventListener('message', listenForCaptions); + // Run through the CaptionParser in case there are captions. + // Initialize CaptionParser if it hasn't been yet + if (!tracks.video || !data.byteLength || !segment.transmuxer) { + finishLoading(); + return; + } - // transfer ownership of bytes to worker. - segment.transmuxer.postMessage({ - action: 'pushMp4Captions', - timescales: segment.map.timescales, - trackIds: [tracks.video.id], - data: buffer, - byteOffset, - byteLength: bytes.byteLength - }, [ buffer ]); + workerCallback({ + action: 'pushMp4Captions', + endAction: 'mp4Captions', + transmuxer: segment.transmuxer, + data: bytesAsUint8Array, + timescales: segment.map.timescales, + trackIds: [tracks.video.id], + callback: (message) => { + // transfer bytes back to us + bytes = message.data.buffer; + segment.bytes = bytesAsUint8Array = message.data; + finishLoading(message.captions); + } + }); + } + }); return; } diff --git a/src/transmuxer-worker.js b/src/transmuxer-worker.js index b483cdadb..9d293a81c 100644 --- a/src/transmuxer-worker.js +++ b/src/transmuxer-worker.js @@ -17,7 +17,10 @@ import {Transmuxer as FullMux} from 'mux.js/lib/mp4/transmuxer'; import PartialMux from 'mux.js/lib/partial/transmuxer'; import CaptionParser from 'mux.js/lib/mp4/caption-parser'; +import mp4probe from 'mux.js/lib/mp4/probe'; +import tsInspector from 'mux.js/lib/tools/ts-inspector.js'; import { + ONE_SECOND_IN_TS, secondsToVideoTs, videoTsToSeconds } from 'mux.js/lib/utils/clock'; @@ -335,6 +338,68 @@ class MessageHandlers { }, [segment.buffer]); } + probeMp4StartTime({timescales, data}) { + const startTime = mp4probe.startTime(timescales, data); + + this.self.postMessage({ + action: 'probeMp4StartTime', + startTime, + data + }, [data.buffer]); + } + + probeMp4Tracks({data}) { + const tracks = mp4probe.tracks(data); + + this.self.postMessage({ + action: 'probeMp4Tracks', + tracks, + data + }, [data.buffer]); + } + + /** + * Probe an mpeg2-ts segment to determine the start time of the segment in it's + * internal "media time," as well as whether it contains video and/or audio. + * + * @private + * @param {Uint8Array} bytes - segment bytes + * @param {number} baseStartTime + * Relative reference timestamp used when adjusting frame timestamps for rollover. + * This value should be in seconds, as it's converted to a 90khz clock within the + * function body. + * @return {Object} The start time of the current segment in "media time" as well as + * whether it contains video and/or audio + */ + probeTs({data, baseStartTime}) { + const tsStartTime = (typeof baseStartTime === 'number' && !isNaN(baseStartTime)) ? + (baseStartTime * ONE_SECOND_IN_TS) : + void 0; + const timeInfo = tsInspector.inspect(data, tsStartTime); + let result = null; + + if (timeInfo) { + result = { + // each type's time info comes back as an array of 2 times, start and end + hasVideo: timeInfo.video && timeInfo.video.length === 2 || false, + hasAudio: timeInfo.audio && timeInfo.audio.length === 2 || false + }; + + if (result.hasVideo) { + result.videoStart = timeInfo.video[0].ptsTime; + } + if (result.hasAudio) { + result.audioStart = timeInfo.audio[0].ptsTime; + } + } + + this.self.postMessage({ + action: 'probeTs', + result, + data + }, [data.buffer]); + } + clearAllMp4Captions() { if (this.captionParser) { this.captionParser.clearAllCaptions(); diff --git a/src/util/segment.js b/src/util/segment.js index a4fed7486..c9adbe942 100644 --- a/src/util/segment.js +++ b/src/util/segment.js @@ -1,45 +1,3 @@ -import tsInspector from 'mux.js/lib/tools/ts-inspector.js'; -import { ONE_SECOND_IN_TS } from 'mux.js/lib/utils/clock'; - -/** - * Probe an mpeg2-ts segment to determine the start time of the segment in it's - * internal "media time," as well as whether it contains video and/or audio. - * - * @private - * @param {Uint8Array} bytes - segment bytes - * @param {number} baseStartTime - * Relative reference timestamp used when adjusting frame timestamps for rollover. - * This value should be in seconds, as it's converted to a 90khz clock within the - * function body. - * @return {Object} The start time of the current segment in "media time" as well as - * whether it contains video and/or audio - */ -export const probeTsSegment = (bytes, baseStartTime) => { - const tsStartTime = (typeof baseStartTime === 'number' && !isNaN(baseStartTime)) ? - (baseStartTime * ONE_SECOND_IN_TS) : - void 0; - const timeInfo = tsInspector.inspect(bytes, tsStartTime); - - if (!timeInfo) { - return null; - } - - const result = { - // each type's time info comes back as an array of 2 times, start and end - hasVideo: timeInfo.video && timeInfo.video.length === 2 || false, - hasAudio: timeInfo.audio && timeInfo.audio.length === 2 || false - }; - - if (result.hasVideo) { - result.videoStart = timeInfo.video[0].ptsTime; - } - if (result.hasAudio) { - result.audioStart = timeInfo.audio[0].ptsTime; - } - - return result; -}; - /** * Combine all segments into a single Uint8Array * diff --git a/src/util/worker-callback.js b/src/util/worker-callback.js new file mode 100644 index 000000000..974754720 --- /dev/null +++ b/src/util/worker-callback.js @@ -0,0 +1,42 @@ +export const workerCallback = function(options) { + const transmuxer = options.transmuxer; + const endAction = options.endAction || options.action; + const callback = options.callback; + const message = Object.assign({}, options, {endAction: null, transmuxer: null, callback: null}); + + const listenForEndEvent = (event) => { + if (event.data.action !== endAction) { + return; + } + transmuxer.removeEventListener('message', listenForEndEvent); + + // transfer ownership of bytes back to us. + if (event.data.data) { + event.data.data = new Uint8Array( + event.data.data, + options.byteOffset || 0, + options.byteLength || event.data.data.byteLength + ); + if (options.data) { + options.data = event.data.data; + } + } + + callback(event.data); + }; + + transmuxer.addEventListener('message', listenForEndEvent); + + if (options.data) { + const isArrayBuffer = options.data instanceof ArrayBuffer; + + message.byteOffset = isArrayBuffer ? 0 : options.data.byteOffset; + message.byteLength = options.data.byteLength; + + const transfers = [isArrayBuffer ? options.data : options.data.buffer]; + + transmuxer.postMessage(message, transfers); + } else { + transmuxer.postMessage(message); + } +}; diff --git a/test/media-segment-request.test.js b/test/media-segment-request.test.js index 3044c38b9..23b59c069 100644 --- a/test/media-segment-request.test.js +++ b/test/media-segment-request.test.js @@ -906,6 +906,8 @@ QUnit.test( function(assert) { const done = assert.async(); + this.transmuxer = this.createTransmuxer(); + assert.expect(10); mediaSegmentRequest({ xhr: this.xhr, @@ -921,9 +923,11 @@ QUnit.test( }, map: { resolvedUri: '0-init.dat' - } + }, + transmuxer: this.transmuxer }, progressFn: this.noop, + trackInfoFn: this.noop, doneFn: (error, segmentData) => { assert.notOk(error, 'there are no errors'); assert.ok(segmentData.bytes, 'decrypted bytes in segment'); @@ -980,6 +984,28 @@ QUnit.test('non-TS segment will get parsed for captions', function(assert) { } }); } + + if (event.action === 'probeMp4StartTime') { + transmuxer.trigger({ + type: 'message', + data: { + action: 'probeMp4StartTime', + data: event.data, + timingInfo: {} + } + }); + } + + if (event.action === 'probeMp4Tracks') { + transmuxer.trigger({ + type: 'message', + data: { + action: 'probeMp4Tracks', + data: event.data, + tracks: [{type: 'video', codec: 'avc1.4d400d'}] + } + }); + } }; mediaSegmentRequest({ @@ -1100,6 +1126,28 @@ QUnit.test('non-TS segment will get parsed for captions on next segment request } }); } + + if (event.action === 'probeMp4StartTime') { + transmuxer.trigger({ + type: 'message', + data: { + action: 'probeMp4StartTime', + data: event.data, + timingInfo: {} + } + }); + } + + if (event.action === 'probeMp4Tracks') { + transmuxer.trigger({ + type: 'message', + data: { + action: 'probeMp4Tracks', + data: event.data, + tracks: [{type: 'video', codec: 'avc1.4d400d'}] + } + }); + } }; mediaSegmentRequest({ diff --git a/test/segment-loader.test.js b/test/segment-loader.test.js index ba36f9d10..f15b7385f 100644 --- a/test/segment-loader.test.js +++ b/test/segment-loader.test.js @@ -50,6 +50,21 @@ import { import sinon from 'sinon'; import { timeRangesEqual } from './custom-assertions.js'; import { QUOTA_EXCEEDED_ERR } from '../src/error-codes'; +import window from 'global/window'; +import document from 'global/document'; + +const newEvent = function(name) { + let event; + + if (typeof window.Event === 'function') { + event = new window.Event(name); + } else { + event = document.createEvent('Event'); + event.initEvent(name, true, true); + } + + return event; +}; /* TODO // noop addSegmentMetadataCue_ since most test segments dont have real timing information @@ -801,29 +816,39 @@ QUnit.module('SegmentLoader', function(hooks) { QUnit.test('updates timestamps when segments do not start at zero', function(assert) { const playlist = playlistWithDuration(10); + const ogPost = loader.transmuxer_.postMessage; - return setupMediaSource(loader.mediaSource_, loader.sourceUpdater_, {isVideoOnly: true}).then(() => { + loader.transmuxer_.postMessage = (message) => { + if (message.action === 'probeMp4StartTime') { + const evt = newEvent('message'); - playlist.segments.forEach((segment) => { - segment.map = { - resolvedUri: 'init.mp4', - byterange: { length: Infinity, offset: 0 } - }; - }); - loader.playlist(playlist); - loader.load(); + evt.data = {action: 'probeMp4StartTime', startTime: 11, data: message.data}; - this.startTime.returns(11); - - this.clock.tick(100); - // init - standardXHRResponse(this.requests.shift(), mp4VideoInitSegment()); - // segment - standardXHRResponse(this.requests.shift(), mp4VideoSegment()); + loader.transmuxer_.dispatchEvent(evt); + return; + } + return ogPost.call(loader.transmuxer_, message); + }; + return setupMediaSource(loader.mediaSource_, loader.sourceUpdater_, {isVideoOnly: true}).then(() => { return new Promise((resolve, reject) => { loader.one('appended', resolve); loader.one('error', reject); + + playlist.segments.forEach((segment) => { + segment.map = { + resolvedUri: 'init.mp4', + byterange: { length: Infinity, offset: 0 } + }; + }); + loader.playlist(playlist); + loader.load(); + + this.clock.tick(100); + // init + standardXHRResponse(this.requests.shift(), mp4VideoInitSegment()); + // segment + standardXHRResponse(this.requests.shift(), mp4VideoSegment()); }); }).then(() => { @@ -913,6 +938,8 @@ QUnit.module('SegmentLoader', function(hooks) { // decryption tick for syncWorker this.clock.tick(1); + // tick for web worker segment probe + this.clock.tick(1); }); }).then(() => { assert.deepEqual(loader.keyCache_['0-key.php'], { @@ -2286,43 +2313,42 @@ QUnit.module('SegmentLoader', function(hooks) { }); QUnit.test('errors when trying to switch from audio and video to audio only', function(assert) { - const errors = []; return setupMediaSource(loader.mediaSource_, loader.sourceUpdater_).then(() => { return new Promise((resolve, reject) => { loader.one('appended', resolve); + loader.one('error', reject); const playlist = playlistWithDuration(40); - loader.on('error', () => errors.push(loader.error())); - loader.playlist(playlist); loader.load(); this.clock.tick(1); standardXHRResponse(this.requests.shift(), muxedSegment()); + this.clock.tick(1); }); - }).then(() => { + }).then(() => new Promise((resolve, reject) => { this.clock.tick(1); - - assert.equal(errors.length, 0, 'no errors'); + loader.one('error', () => { + const error = loader.error(); + + assert.equal( + error.message, + 'Only audio found in segment when we expected video.' + + ' We can\'t switch to audio only from a stream that had video.' + + ' To get rid of this message, please add codec information to the' + + ' manifest.', + 'correct error message' + ); + resolve(); + }); standardXHRResponse(this.requests.shift(), audioSegment()); - - assert.equal(errors.length, 1, 'one error'); - assert.equal( - errors[0].message, - 'Only audio found in segment when we expected video.' + - ' We can\'t switch to audio only from a stream that had video.' + - ' To get rid of this message, please add codec information to the' + - ' manifest.', - 'correct error message' - ); - }); + })); }); QUnit.test('errors when trying to switch from audio only to audio and video', function(assert) { - const errors = []; return setupMediaSource(loader.mediaSource_, loader.sourceUpdater_).then(() => { return new Promise((resolve, reject) => { @@ -2332,8 +2358,6 @@ QUnit.module('SegmentLoader', function(hooks) { const playlist = playlistWithDuration(40); - loader.on('error', () => errors.push(loader.error())); - loader.playlist(playlist); loader.load(); this.clock.tick(1); @@ -2341,34 +2365,36 @@ QUnit.module('SegmentLoader', function(hooks) { standardXHRResponse(this.requests.shift(), audioSegment()); }); - }).then(() => { + }).then(() => new Promise((resolve, reject) => { this.clock.tick(1); - assert.equal(errors.length, 0, 'no errors'); + loader.one('error', function() { + const error = loader.error(); - standardXHRResponse(this.requests.shift(), muxedSegment()); + assert.equal( + error.message, + 'Video found in segment when we expected only audio.' + + ' We can\'t switch to a stream with video from an audio only stream.' + + ' To get rid of this message, please add codec information to the' + + ' manifest.', + 'correct error message' + ); + resolve(); + }); - assert.equal(errors.length, 1, 'one error'); - assert.equal( - errors[0].message, - 'Video found in segment when we expected only audio.' + - ' We can\'t switch to a stream with video from an audio only stream.' + - ' To get rid of this message, please add codec information to the' + - ' manifest.', - 'correct error message' - ); - }); + standardXHRResponse(this.requests.shift(), muxedSegment()); + })); }); QUnit.test('no error when not switching from audio and video', function(assert) { const errors = []; + loader.on('error', () => errors.push(loader.error())); + return setupMediaSource(loader.mediaSource_, loader.sourceUpdater_).then(() => { const playlist = playlistWithDuration(40); - loader.on('error', () => errors.push(loader.error())); - loader.playlist(playlist); loader.load(); this.clock.tick(1); @@ -3051,41 +3077,41 @@ QUnit.module('SegmentLoader', function(hooks) { const appends = []; return setupMediaSource(loader.mediaSource_, loader.sourceUpdater_, {isVideoOnly: true}).then(() => { - const origAppendToSourceBuffer = loader.appendToSourceBuffer_.bind(loader); + return new Promise((resolve, reject) => { + loader.one('appended', resolve); + loader.one('error', reject); + const origAppendToSourceBuffer = loader.appendToSourceBuffer_.bind(loader); - loader.appendToSourceBuffer_ = (config) => { - appends.push(config); - origAppendToSourceBuffer(config); - }; + loader.appendToSourceBuffer_ = (config) => { + appends.push(config); + origAppendToSourceBuffer(config); + }; - const playlist = playlistWithDuration(30); + const playlist = playlistWithDuration(30); - playlist.segments[0].map = { - resolvedUri: 'init.mp4', - byterange: { length: Infinity, offset: 0 } - }; - // change the map tag as we won't re-append the init segment if it hasn't changed - playlist.segments[1].map = { - resolvedUri: 'init2.mp4', - byterange: { length: 100, offset: 10 } - }; - // reuse the initial map to see if it was cached - playlist.segments[2].map = { - resolvedUri: 'init.mp4', - byterange: { length: Infinity, offset: 0 } - }; + playlist.segments[0].map = { + resolvedUri: 'init.mp4', + byterange: { length: Infinity, offset: 0 } + }; + // change the map tag as we won't re-append the init segment if it hasn't changed + playlist.segments[1].map = { + resolvedUri: 'init2.mp4', + byterange: { length: 100, offset: 10 } + }; + // reuse the initial map to see if it was cached + playlist.segments[2].map = { + resolvedUri: 'init.mp4', + byterange: { length: Infinity, offset: 0 } + }; - loader.playlist(playlist); - loader.load(); - this.clock.tick(1); + loader.playlist(playlist); + loader.load(); + this.clock.tick(1); - // init - standardXHRResponse(this.requests.shift(), mp4VideoInitSegment()); - // segment - standardXHRResponse(this.requests.shift(), mp4VideoSegment()); - return new Promise((resolve, reject) => { - loader.one('appended', resolve); - loader.one('error', reject); + // init + standardXHRResponse(this.requests.shift(), mp4VideoInitSegment()); + // segment + standardXHRResponse(this.requests.shift(), mp4VideoSegment()); }); }).then(() => { this.clock.tick(1);