From 491d19403db816956969e31e6947ca1b580a9388 Mon Sep 17 00:00:00 2001 From: Brandon Casey <2381475+brandonocasey@users.noreply.github.com> Date: Tue, 16 Jun 2020 13:12:20 -0400 Subject: [PATCH] refactor: Move caption parser to webworker, saving 5732b offloading work (#863) --- package-lock.json | 6 +- package.json | 2 +- src/dash-playlist-loader.js | 4 +- src/media-segment-request.js | 67 ++++++++++++--------- src/segment-loader.js | 37 ++++++------ src/transmuxer-worker.js | 34 +++++++++++ test/media-segment-request.test.js | 97 ++++++++++++------------------ test/segment-loader.test.js | 34 ++++++++--- 8 files changed, 161 insertions(+), 120 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2a54f12ee..7bdfb3ff5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6852,9 +6852,9 @@ "dev": true }, "mux.js": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/mux.js/-/mux.js-5.6.2.tgz", - "integrity": "sha512-66+0HevBv12gJHmoMTbkQssPyiCOJNnm2XarYgUC6nj1DJ8WG0wLdinzMFq5vrbBmDxNNYYh+03Rp/QVzGiSQg==" + "version": "5.6.5", + "resolved": "https://registry.npmjs.org/mux.js/-/mux.js-5.6.5.tgz", + "integrity": "sha512-9lcApL+FquDTpvBR8TIzouMirpzAf8tiOIp4hhogyE0U374sWQP0RTqFDXsU7TeubQy/GZu1UzvXrzcCuNyEww==" }, "nanomatch": { "version": "1.2.13", diff --git a/package.json b/package.json index 5c6c55610..6c90b0a08 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "global": "^4.3.2", "m3u8-parser": "4.4.2", "mpd-parser": "0.10.1", - "mux.js": "5.6.2", + "mux.js": "5.6.5", "video.js": "^6 || ^7" }, "devDependencies": { diff --git a/src/dash-playlist-loader.js b/src/dash-playlist-loader.js index e3b7520b9..325d75900 100644 --- a/src/dash-playlist-loader.js +++ b/src/dash-playlist-loader.js @@ -8,7 +8,7 @@ import { updateMaster as updatePlaylist } from './playlist-loader'; import { resolveUrl, resolveManifestRedirect } from './resolve-url'; -import mp4Inspector from 'mux.js/lib/tools/mp4-inspector'; +import parseSidx from 'mux.js/lib/tools/parse-sidx'; import { segmentXhrHeaders } from './xhr'; import window from 'global/window'; import { @@ -345,7 +345,7 @@ export default class DashPlaylistLoader extends EventTarget { } const bytes = toUint8(request.response); - const sidx = mp4Inspector.parseSidx(bytes.subarray(8)); + const sidx = parseSidx(bytes.subarray(8)); return doneFn(master, sidx); }; diff --git a/src/media-segment-request.js b/src/media-segment-request.js index b5c8ad195..8dc121a55 100644 --- a/src/media-segment-request.js +++ b/src/media-segment-request.js @@ -148,7 +148,7 @@ const handleKeyResponse = (segment, finishProcessingFn) => (error, request) => { * this request */ const handleInitSegmentResponse = -({segment, captionParser, finishProcessingFn}) => (error, request) => { +({segment, finishProcessingFn}) => (error, request) => { const response = request.response; const errorObj = handleErrors(error, request); @@ -368,7 +368,6 @@ const handleSegmentBytes = ({ segment, bytes, isPartial, - captionParser, trackInfoFn, timingInfoFn, videoSegmentTimingInfoFn, @@ -430,30 +429,52 @@ const handleSegmentBytes = ({ timingInfoFn(segment, 'video', 'start', timingInfo); } - // 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. - dataFn(segment, {data: bytes, type: trackInfo.hasAudio ? 'audio' : 'video'}); + 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 ? '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 (captionParser && tracks.video) { - if (!captionParser.isInitialized()) { - captionParser.init(); + 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 parsed = captionParser.parse( - segment.bytes, - [tracks.video.id], - segment.map.timescales - ); + const data = event.data.data; - if (parsed && parsed.captions && parsed.captions.length > 0) { - captionsFn(segment, parsed.captions); - } - } + // transfer ownership of bytes back to us. + segment.bytes = bytes = new Uint8Array(data, data.byteOffset || 0, data.byteLength); - doneFn(null, segment, {}); + finishLoading(event.data.captions); + }; + + segment.transmuxer.addEventListener('message', listenForCaptions); + + // 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 ]); return; } @@ -493,7 +514,6 @@ const handleSegmentBytes = ({ const decryptSegment = ({ decryptionWorker, segment, - captionParser, trackInfoFn, timingInfoFn, videoSegmentTimingInfoFn, @@ -517,7 +537,6 @@ const decryptSegment = ({ segment, bytes: segment.bytes, isPartial: false, - captionParser, trackInfoFn, timingInfoFn, videoSegmentTimingInfoFn, @@ -573,7 +592,6 @@ const decryptSegment = ({ const waitForCompletion = ({ activeXhrs, decryptionWorker, - captionParser, trackInfoFn, timingInfoFn, videoSegmentTimingInfoFn, @@ -619,7 +637,6 @@ const waitForCompletion = ({ return decryptSegment({ decryptionWorker, segment, - captionParser, trackInfoFn, timingInfoFn, videoSegmentTimingInfoFn, @@ -634,7 +651,6 @@ const waitForCompletion = ({ segment, bytes: segment.bytes, isPartial: false, - captionParser, trackInfoFn, timingInfoFn, videoSegmentTimingInfoFn, @@ -795,7 +811,6 @@ export const mediaSegmentRequest = ({ xhr, xhrOptions, decryptionWorker, - captionParser, segment, abortFn, progressFn, @@ -812,7 +827,6 @@ export const mediaSegmentRequest = ({ const finishProcessingFn = waitForCompletion({ activeXhrs, decryptionWorker, - captionParser, trackInfoFn, timingInfoFn, videoSegmentTimingInfoFn, @@ -843,7 +857,6 @@ export const mediaSegmentRequest = ({ }); const initSegmentRequestCallback = handleInitSegmentResponse({ segment, - captionParser, finishProcessingFn }); const initSegmentXhr = xhr(initSegmentOptions, initSegmentRequestCallback); diff --git a/src/segment-loader.js b/src/segment-loader.js index cf5e1e16f..8fa56a6cb 100644 --- a/src/segment-loader.js +++ b/src/segment-loader.js @@ -11,7 +11,6 @@ import TransmuxWorker from 'worker!./transmuxer-worker.worker.js'; import segmentTransmuxer from './segment-transmuxer'; import { TIME_FUDGE_FACTOR, timeUntilRebuffer as timeUntilRebuffer_ } from './ranges'; import { minRebufferMaxBandwidthSelector } from './playlist-selectors'; -import CaptionParser from 'mux.js/lib/mp4/caption-parser'; import logger from './util/logger'; import { concatSegments } from './util/segment'; import { @@ -397,13 +396,6 @@ export default class SegmentLoader extends videojs.EventTarget { this.cacheEncryptionKeys_ = settings.cacheEncryptionKeys; this.keyCache_ = {}; - // Fmp4 CaptionParser - if (this.loaderType_ === 'main') { - this.captionParser_ = new CaptionParser(); - } else { - this.captionParser_ = null; - } - this.decrypter_ = settings.decrypter; // Manages the tracking and generation of sync-points, mappings @@ -522,9 +514,6 @@ export default class SegmentLoader extends videojs.EventTarget { segmentTransmuxer.dispose(); } this.resetStats_(); - if (this.captionParser_) { - this.captionParser_.reset(); - } if (this.checkBufferTimeout_) { window.clearTimeout(this.checkBufferTimeout_); @@ -928,8 +917,10 @@ export default class SegmentLoader extends videojs.EventTarget { this.remove(0, Infinity, done); // clears fmp4 captions - if (this.captionParser_) { - this.captionParser_.clearAllCaptions(); + if (this.transmuxer_) { + this.transmuxer_.postMessage({ + action: 'clearAllMp4Captions' + }); } } @@ -962,8 +953,10 @@ export default class SegmentLoader extends videojs.EventTarget { this.metadataQueue_.caption = []; this.abort(); - if (this.captionParser_) { - this.captionParser_.clearParsedCaptions(); + if (this.transmuxer_) { + this.transmuxer_.postMessage({ + action: 'clearParsedMp4Captions' + }); } } @@ -1547,8 +1540,11 @@ export default class SegmentLoader extends videojs.EventTarget { // Reset stored captions since we added parsed // captions to a text track at this point - if (this.captionParser_) { - this.captionParser_.clearParsedCaptions(); + + if (this.transmuxer_) { + this.transmuxer_.postMessage({ + action: 'clearParsedMp4Captions' + }); } } @@ -1958,8 +1954,10 @@ export default class SegmentLoader extends videojs.EventTarget { this.trimBackBuffer_(segmentInfo); if (typeof segmentInfo.timestampOffset === 'number') { - if (this.captionParser_) { - this.captionParser_.clearAllCaptions(); + if (this.transmuxer_) { + this.transmuxer_.postMessage({ + action: 'clearAllMp4Captions' + }); } } @@ -2035,7 +2033,6 @@ export default class SegmentLoader extends videojs.EventTarget { xhr: this.vhs_.xhr, xhrOptions: this.xhrOptions_, decryptionWorker: this.decrypter_, - captionParser: this.captionParser_, segment: simpleSegment, handlePartialData: this.handlePartialData_, abortFn: this.handleAbort_.bind(this), diff --git a/src/transmuxer-worker.js b/src/transmuxer-worker.js index de2fa48d4..c9d31468d 100644 --- a/src/transmuxer-worker.js +++ b/src/transmuxer-worker.js @@ -16,6 +16,7 @@ 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 { secondsToVideoTs, videoTsToSeconds @@ -134,6 +135,7 @@ const wireFullTransmuxerEvents = function(self, transmuxer) { } }); }); + }; const wirePartialTransmuxerEvents = function(self, transmuxer) { @@ -287,6 +289,38 @@ class MessageHandlers { } else { wireFullTransmuxerEvents(this.self, this.transmuxer); } + + } + + pushMp4Captions(data) { + if (!this.captionParser) { + this.captionParser = new CaptionParser(); + this.captionParser.init(); + } + const segment = new Uint8Array(data.data, data.byteOffset, data.byteLength); + const parsed = this.captionParser.parse( + segment, + data.trackIds, + data.timescales + ); + + this.self.postMessage({ + action: 'mp4Captions', + captions: parsed.captions, + data: segment.buffer + }, [segment.buffer]); + } + + clearAllMp4Captions() { + if (this.captionParser) { + this.captionParser.clearAllCaptions(); + } + } + + clearParsedMp4Captions() { + if (this.captionParser) { + this.captionParser.clearParsedCaptions(); + } } /** diff --git a/test/media-segment-request.test.js b/test/media-segment-request.test.js index 1a81d39a3..ddba9d4b0 100644 --- a/test/media-segment-request.test.js +++ b/test/media-segment-request.test.js @@ -1,4 +1,5 @@ import QUnit from 'qunit'; +import videojs from 'video.js'; import sinon from 'sinon'; import {mediaSegmentRequest, REQUEST_ERRORS} from '../src/media-segment-request'; import xhrFactory from '../src/xhr'; @@ -48,31 +49,6 @@ QUnit.module('Media Segment Request', { this.listeners = this.listeners.filter((fn)=>fn !== listener); } }; - this.mockCaptionParser = { - initialized: false, - parsed: false, - isInitialized() { - return this.initialized; - }, - init() { - this.initialized = true; - }, - parse(segment, videoTrackIds, timescales) { - this.parsed = true; - - return { - captions: [{ - startTime: 0, - endTime: 1, - text: 'test caption', - stream: 'CC1' - }], - captionStreams: { - CC1: true - } - }; - } - }; this.xhrOptions = { timeout: 1000 }; @@ -121,7 +97,6 @@ QUnit.test('cancels outstanding segment request on abort', function(assert) { xhr: this.xhr, xhrOptions: this.xhrOptions, decryptionWorker: this.noop, - captionParser: this.noop, segment: { resolvedUri: '0-test.ts' }, abortFn: () => aborts++, progressFn: this.noop, @@ -149,7 +124,6 @@ QUnit.test('cancels outstanding key requests on abort', function(assert) { xhr: this.xhr, xhrOptions: this.xhrOptions, decryptionWorker: this.noop, - captionParser: this.noop, segment: { resolvedUri: '0-test.ts', key: { @@ -189,7 +163,6 @@ QUnit.test('cancels outstanding key requests on failure', function(assert) { xhr: this.xhr, xhrOptions: this.xhrOptions, decryptionWorker: this.noop, - captionParser: this.noop, segment: { resolvedUri: '0-test.ts', key: { @@ -226,7 +199,6 @@ QUnit.test('cancels outstanding key requests on timeout', function(assert) { xhr: this.xhr, xhrOptions: this.xhrOptions, decryptionWorker: this.noop, - captionParser: this.noop, segment: { resolvedUri: '0-test.ts', key: { @@ -265,7 +237,6 @@ QUnit.test( xhr: this.xhr, xhrOptions: this.xhrOptions, decryptionWorker: this.noop, - captionParser: this.noop, segment: { resolvedUri: '0-test.ts', key: { @@ -324,7 +295,6 @@ QUnit.test('the key response is converted to the correct format', function(asser xhr: this.xhr, xhrOptions: this.xhrOptions, decryptionWorker: this.mockDecrypter, - captionParser: this.noop, segment: { resolvedUri: '0-test.ts', key: { @@ -367,7 +337,6 @@ QUnit.test('segment with key has bytes decrypted', function(assert) { xhr: this.xhr, xhrOptions: this.xhrOptions, decryptionWorker: this.realDecrypter, - captionParser: this.noop, segment: { resolvedUri: '0-test.ts', key: { @@ -417,7 +386,6 @@ QUnit.test('segment with key bytes does not request key again', function(assert) mediaSegmentRequest({xhr: this.xhr, xhrOptions: this.xhrOptions, decryptionWorker: this.realDecrypter, - captionParser: this.noop, segment: { resolvedUri: '0-test.ts', key: { @@ -465,7 +433,6 @@ QUnit.test('key 404 calls back with error', function(assert) { xhr: this.xhr, xhrOptions: this.xhrOptions, decryptionWorker: this.realDecrypter, - captionParser: this.noop, segment: { resolvedUri: '0-test.ts', key: { @@ -509,7 +476,6 @@ QUnit.test('key 500 calls back with error', function(assert) { xhr: this.xhr, xhrOptions: this.xhrOptions, decryptionWorker: this.realDecrypter, - captionParser: this.noop, segment: { resolvedUri: '0-test.ts', key: { @@ -554,7 +520,6 @@ QUnit.test( xhr: this.xhr, xhrOptions: this.xhrOptions, decryptionWorker: this.realDecrypter, - captionParser: this.mockCaptionParser, segment: { resolvedUri: '0-test.ts', key: { @@ -609,13 +574,29 @@ QUnit.test('non-TS segment will get parsed for captions', function(assert) { const done = assert.async(); let gotCaption = false; let gotData = false; + const captions = [{foo: 'bar'}]; + + const transmuxer = new videojs.EventTarget(); + + transmuxer.postMessage = (event) => { + if (event.action === 'pushMp4Captions') { + transmuxer.trigger({ + type: 'message', + data: { + action: 'mp4Captions', + data: event.data, + captions + } + }); + } + }; mediaSegmentRequest({ xhr: this.xhr, xhrOptions: this.xhrOptions, decryptionWorker: this.mockDecrypter, - captionParser: this.mockCaptionParser, segment: { + transmuxer, resolvedUri: 'mp4Video.mp4', map: { resolvedUri: 'mp4VideoInit.mp4' @@ -626,12 +607,9 @@ QUnit.test('non-TS segment will get parsed for captions', function(assert) { trackInfoFn: this.noop, timingInfoFn: this.noop, id3Fn: this.noop, - captionsFn: (segment, captions) => { + captionsFn: (segment, _captions) => { gotCaption = true; - - // verify the caption parser - assert.equal(this.mockCaptionParser.parsed, true, 'tried to parse captions'); - assert.deepEqual(captions, this.mockCaptionParser.parse().captions, 'returned the expected captions'); + assert.equal(captions, _captions, 'captions as expected'); }, dataFn: (segment, segmentData) => { gotData = true; @@ -643,6 +621,7 @@ QUnit.test('non-TS segment will get parsed for captions', function(assert) { doneFn: () => { assert.ok(gotCaption, 'received caption event'); assert.ok(gotData, 'received data event'); + transmuxer.off(); done(); }, handlePartialData: false @@ -668,7 +647,6 @@ QUnit.test('webm segment calls back with error', function(assert) { xhr: this.xhr, xhrOptions: this.xhrOptions, decryptionWorker: this.mockDecrypter, - captionParser: this.mockCaptionParser, segment: { resolvedUri: 'webmVideo.mp4', map: { @@ -717,13 +695,28 @@ QUnit.test('non-TS segment will get parsed for captions on next segment request const done = assert.async(); let gotCaption = 0; let gotData = 0; + const captions = [{foo: 'bar'}]; + const transmuxer = new videojs.EventTarget(); + + transmuxer.postMessage = (event) => { + if (event.action === 'pushMp4Captions') { + transmuxer.trigger({ + type: 'message', + data: { + action: 'mp4Captions', + data: event.data, + captions + } + }); + } + }; mediaSegmentRequest({ xhr: this.xhr, xhrOptions: this.xhrOptions, decryptionWorker: this.mockDecrypter, - captionParser: this.mockCaptionParser, segment: { + transmuxer, resolvedUri: 'mp4Video.mp4', map: { resolvedUri: 'mp4VideoInit.mp4' @@ -733,18 +726,13 @@ QUnit.test('non-TS segment will get parsed for captions on next segment request trackInfoFn: this.noop, timingInfoFn: this.noop, id3Fn: this.noop, - captionsFn: (segment, captions) => { + captionsFn: (segment, _captions) => { gotCaption++; // verify the caption parser - assert.equal( - this.mockCaptionParser.parsed, - true, - 'should have parsed captions even though init was received late' - ); assert.deepEqual( captions, - this.mockCaptionParser.parse().captions, + _captions, 'the expected captions were received' ); }, @@ -758,6 +746,7 @@ QUnit.test('non-TS segment will get parsed for captions on next segment request doneFn: () => { assert.equal(gotCaption, 1, 'received caption event'); assert.equal(gotData, 1, 'received data event'); + transmuxer.off(); done(); }, handlePartialData: false @@ -790,7 +779,6 @@ QUnit.test('callbacks fire for TS segment with partial data', function(assert) { xhr: this.xhr, xhrOptions: this.xhrOptions, decryptionWorker: this.mockDecrypter, - captionParser: this.mockCaptionParser, segment: { resolvedUri: 'muxed.ts', transmuxer: this.transmuxer @@ -833,7 +821,6 @@ QUnit.test('data callback does not fire if too little partial data', function(as xhr: this.xhr, xhrOptions: this.xhrOptions, decryptionWorker: this.mockDecrypter, - captionParser: this.mockCaptionParser, segment: { resolvedUri: 'muxed.ts', transmuxer: this.transmuxer @@ -877,7 +864,6 @@ QUnit.skip('caption callback fires for TS segment with partial data', function(a xhr: this.xhr, xhrOptions: this.xhrOptions, decryptionWorker: this.mockDecrypter, - captionParser: this.mockCaptionParser, segment: { resolvedUri: 'caption.ts', transmuxer: this.transmuxer @@ -926,7 +912,6 @@ QUnit.skip('caption callback does not fire if partial data has no captions', fun xhr: this.xhr, xhrOptions: this.xhrOptions, decryptionWorker: this.mockDecrypter, - captionParser: this.mockCaptionParser, segment: { resolvedUri: 'caption.ts', transmuxer: this.transmuxer @@ -974,7 +959,6 @@ QUnit.skip('id3 callback fires for TS segment with partial data', function(asser xhr: this.xhr, xhrOptions: this.xhrOptions, decryptionWorker: this.mockDecrypter, - captionParser: this.mockCaptionParser, segment: { resolvedUri: 'id3.ts', transmuxer: this.transmuxer @@ -1023,7 +1007,6 @@ QUnit.skip('id3 callback does not fire if partial data has no ID3 tags', functio xhr: this.xhr, xhrOptions: this.xhrOptions, decryptionWorker: this.mockDecrypter, - captionParser: this.mockCaptionParser, segment: { resolvedUri: 'id3.ts', transmuxer: this.transmuxer diff --git a/test/segment-loader.test.js b/test/segment-loader.test.js index 4f7e662b4..70fb4553e 100644 --- a/test/segment-loader.test.js +++ b/test/segment-loader.test.js @@ -3165,13 +3165,16 @@ QUnit.module('SegmentLoader: FMP4', function(hooks) { loader.dispose(); }); - QUnit.test('CaptionParser is handled as expected', function(assert) { + QUnit.test('CaptionParser messages sent as expected', function(assert) { return setupMediaSource(loader.mediaSource_, loader.sourceUpdater_).then(() => { - assert.ok(loader.captionParser_, 'there is a captions parser'); + const actions = {}; - const mockCaptionParserReset = sinon.stub(loader.captionParser_, 'reset'); - const mockCaptionParserClear = sinon.stub(loader.captionParser_, 'clearAllCaptions'); - const mockCaptionParserClearParsedCaptions = sinon.stub(loader.captionParser_, 'clearParsedCaptions'); + loader.transmuxer_.postMessage = ({action}) => { + if (/Mp4Captions/.test(action)) { + actions[action] = actions[action] || 0; + actions[action]++; + } + }; loader.load(); loader.playlist(playlistWithDuration(10, 'm4s')); @@ -3179,17 +3182,26 @@ QUnit.module('SegmentLoader: FMP4', function(hooks) { this.clock.tick(1); assert.equal(this.requests.length, 1, 'made a request'); - assert.equal(mockCaptionParserClear.callCount, 2, 'captions cleared on load and mimeType'); + assert.deepEqual(actions, { + clearParsedMp4Captions: 1, + clearAllMp4Captions: 2 + }, 'caption parser cleared as expected on load'); // Simulate a rendition switch loader.resetEverything(); - assert.equal(mockCaptionParserClear.callCount, 3, 'captions cleared on rendition switch'); + assert.deepEqual(actions, { + clearParsedMp4Captions: 2, + clearAllMp4Captions: 3 + }, 'caption parser cleared as expected on resetEverything'); // Simulate a discontinuity const originalCurrentTimeline = loader.currentTimeline_; loader.currentTimeline_ = originalCurrentTimeline + 1; - assert.equal(mockCaptionParserClear.callCount, 3, 'captions cleared on discontinuity'); + assert.deepEqual(actions, { + clearParsedMp4Captions: 2, + clearAllMp4Captions: 3 + }, 'caption parser cleared as expected after timeline change'); loader.currentTimeline_ = originalCurrentTimeline; // Add to the inband text track, then call remove @@ -3240,12 +3252,14 @@ QUnit.module('SegmentLoader: FMP4', function(hooks) { loader.fillBuffer_(); assert.ok(this.inbandTextTracks.CC1, 'text track created'); assert.equal(this.inbandTextTracks.CC1.cues.length, 1, 'cue added'); - assert.equal(mockCaptionParserClearParsedCaptions.callCount, 3, 'captions cleared after adding to text track'); + assert.deepEqual(actions, { + clearParsedMp4Captions: 3, + clearAllMp4Captions: 3 + }, 'caption parser cleared as expected after load'); loader.pendingSegment_ = originalPendingSegment; // Dispose the loader loader.dispose(); - assert.equal(mockCaptionParserReset.callCount, 1, 'CaptionParser reset'); }); }); });