From 47fa3093e1462d0bcca87238dc4886b9e2c1f8f4 Mon Sep 17 00:00:00 2001 From: Joey Parrish Date: Thu, 14 Jul 2022 16:28:33 -0700 Subject: [PATCH] fix: Debug buffer placement (#4345) Issue an error with debug info, including the URL, if the placement of a segment in buffer is very different from expectations. b/233075535 --- lib/media/media_source_engine.js | 81 ++++++++++++----- lib/media/streaming_engine.js | 7 +- lib/media/time_ranges_utils.js | 46 ++++++++++ test/media/drm_engine_integration.js | 32 +++++-- test/media/media_source_engine_integration.js | 34 +++++-- test/media/media_source_engine_unit.js | 91 +++++++++++-------- test/media/streaming_engine_unit.js | 55 +++++------ test/test/util/fake_media_source_engine.js | 19 ++-- 8 files changed, 245 insertions(+), 120 deletions(-) diff --git a/lib/media/media_source_engine.js b/lib/media/media_source_engine.js index 4325bdecd7..7768abed2d 100644 --- a/lib/media/media_source_engine.js +++ b/lib/media/media_source_engine.js @@ -10,6 +10,7 @@ goog.require('goog.asserts'); goog.require('shaka.log'); goog.require('shaka.media.ContentWorkarounds'); goog.require('shaka.media.IClosedCaptionParser'); +goog.require('shaka.media.SegmentReference'); goog.require('shaka.media.TimeRangesUtils'); goog.require('shaka.media.Transmuxer'); goog.require('shaka.text.TextEngine'); @@ -466,13 +467,13 @@ shaka.media.MediaSourceEngine = class { */ getBufferedInfo() { const ContentType = shaka.util.ManifestParserUtils.ContentType; - const TimeRangeUtils = shaka.media.TimeRangesUtils; + const TimeRangesUtils = shaka.media.TimeRangesUtils; const info = { - total: TimeRangeUtils.getBufferedInfo(this.video_.buffered), - audio: TimeRangeUtils.getBufferedInfo( + total: TimeRangesUtils.getBufferedInfo(this.video_.buffered), + audio: TimeRangesUtils.getBufferedInfo( this.getBuffered_(ContentType.AUDIO)), - video: TimeRangeUtils.getBufferedInfo( + video: TimeRangesUtils.getBufferedInfo( this.getBuffered_(ContentType.VIDEO)), text: [], }; @@ -516,15 +517,14 @@ shaka.media.MediaSourceEngine = class { * * @param {shaka.util.ManifestParserUtils.ContentType} contentType * @param {!BufferSource} data - * @param {?number} startTime relative to the start of the presentation - * @param {?number} endTime relative to the start of the presentation + * @param {?shaka.media.SegmentReference} reference The segment reference + * we are appending, or null for init segments * @param {?boolean} hasClosedCaptions True if the buffer contains CEA closed - * captions + * captions * @param {boolean=} seeked True if we just seeked * @return {!Promise} */ - async appendBuffer( - contentType, data, startTime, endTime, hasClosedCaptions, seeked) { + async appendBuffer(contentType, data, reference, hasClosedCaptions, seeked) { const ContentType = shaka.util.ManifestParserUtils.ContentType; if (contentType == ContentType.TEXT) { @@ -533,7 +533,10 @@ shaka.media.MediaSourceEngine = class { const offset = await this.textSequenceModeOffset_; this.textEngine_.setTimestampOffset(offset); } - await this.textEngine_.appendBuffer(data, startTime, endTime); + await this.textEngine_.appendBuffer( + data, + reference ? reference.startTime : null, + reference ? reference.endTime : null); return; } @@ -549,7 +552,10 @@ shaka.media.MediaSourceEngine = class { if (transmuxedData.metadata) { const timestampOffset = this.sourceBuffers_[contentType].timestampOffset; - this.onMetadata_(transmuxedData.metadata, timestampOffset, endTime); + this.onMetadata_( + transmuxedData.metadata, + timestampOffset, + reference ? reference.endTime : null); } // This doesn't work for native TS support (ex. Edge/Chromecast), // since no transmuxing is needed for native TS. @@ -559,7 +565,10 @@ shaka.media.MediaSourceEngine = class { const closedCaptions = this.textEngine_ .convertMuxjsCaptionsToShakaCaptions(transmuxedData.captions); this.textEngine_.storeAndAppendClosedCaptions( - closedCaptions, startTime, endTime, videoOffset); + closedCaptions, + reference ? reference.startTime : null, + reference ? reference.endTime : null, + videoOffset); } data = transmuxedData.data; @@ -569,7 +578,7 @@ shaka.media.MediaSourceEngine = class { } // If it is the init segment for closed captions, initialize the closed // caption parser. - if (startTime == null && endTime == null) { + if (!reference) { this.captionParser_.init(data); } else { const closedCaptions = this.captionParser_.parseFrom(data); @@ -577,18 +586,21 @@ shaka.media.MediaSourceEngine = class { const videoOffset = this.sourceBuffers_[ContentType.VIDEO].timestampOffset; this.textEngine_.storeAndAppendClosedCaptions( - closedCaptions, startTime, endTime, videoOffset); + closedCaptions, + reference.startTime, + reference.endTime, + videoOffset); } } } - data = this.workAroundBrokenPlatforms_(data, startTime, contentType); + data = this.workAroundBrokenPlatforms_( + data, reference ? reference.startTime : null, contentType); const sourceBuffer = this.sourceBuffers_[contentType]; const SEQUENCE = shaka.media.MediaSourceEngine.SourceBufferMode_.SEQUENCE; - if (this.sequenceMode_ && sourceBuffer.mode != SEQUENCE && - startTime != null) { + if (this.sequenceMode_ && sourceBuffer.mode != SEQUENCE && reference) { // This is the first media segment to be appended to a SourceBuffer in // sequence mode. We set the mode late so that we can trick MediaSource // into extracting a timestamp for us to align text segments in sequence @@ -623,7 +635,7 @@ shaka.media.MediaSourceEngine = class { // segments. const mediaStartTime = shaka.media.TimeRangesUtils.bufferStart( this.getBuffered_(contentType)); - const textOffset = (startTime || 0) - (mediaStartTime || 0); + const textOffset = (reference.startTime || 0) - (mediaStartTime || 0); this.textSequenceModeOffset_.resolve(textOffset); // Finally, clear the buffer. @@ -636,22 +648,43 @@ shaka.media.MediaSourceEngine = class { sourceBuffer.mode = SEQUENCE; } - if (startTime != null && this.sequenceMode_ && - contentType != ContentType.TEXT) { + if (reference && this.sequenceMode_ && contentType != ContentType.TEXT) { // In sequence mode, for non-text streams, if we just cleared the buffer // and are performing an unbuffered seek, we need to set a new // timestampOffset on the sourceBuffer. if (seeked) { - const timestampOffset = /** @type {number} */ (startTime); + const timestampOffset = reference.startTime; this.enqueueOperation_( contentType, () => this.setTimestampOffset_(contentType, timestampOffset)); } } - await this.enqueueOperation_( - contentType, - () => this.append_(contentType, data)); + let bufferedBefore = null; + + await this.enqueueOperation_(contentType, () => { + if (goog.DEBUG && reference) { + bufferedBefore = this.getBuffered_(contentType); + } + this.append_(contentType, data); + }); + + if (goog.DEBUG && reference) { + const bufferedAfter = this.getBuffered_(contentType); + const newBuffered = shaka.media.TimeRangesUtils.computeAddedRange( + bufferedBefore, bufferedAfter); + if (newBuffered) { + const segmentDuration = reference.endTime - reference.startTime; + if (Math.abs(newBuffered.start - reference.startTime) > + segmentDuration / 2) { + shaka.log.error('Possible encoding problem detected!', + 'Unexpected buffered range for reference', reference, + 'from URIs', reference.getUris(), + 'should be', {start: reference.startTime, end: reference.endTime}, + 'but got', newBuffered); + } + } + } } /** diff --git a/lib/media/streaming_engine.js b/lib/media/streaming_engine.js index 2882b2d59f..f491acb389 100644 --- a/lib/media/streaming_engine.js +++ b/lib/media/streaming_engine.js @@ -1555,8 +1555,8 @@ shaka.media.StreamingEngine = class { const hasClosedCaptions = mediaState.stream.closedCaptions && mediaState.stream.closedCaptions.size > 0; await this.playerInterface_.mediaSourceEngine.appendBuffer( - mediaState.type, initSegment, /* startTime= */ null, - /* endTime= */ null, hasClosedCaptions); + mediaState.type, initSegment, /* reference= */ null, + hasClosedCaptions); } catch (error) { mediaState.lastInitSegmentReference = null; throw error; @@ -1609,8 +1609,7 @@ shaka.media.StreamingEngine = class { await this.playerInterface_.mediaSourceEngine.appendBuffer( mediaState.type, segment, - reference.startTime, - reference.endTime, + reference, hasClosedCaptions, seeked); this.destroyer_.ensureNotDestroyed(); diff --git a/lib/media/time_ranges_utils.js b/lib/media/time_ranges_utils.js index 79e3d84351..e095caabcb 100644 --- a/lib/media/time_ranges_utils.js +++ b/lib/media/time_ranges_utils.js @@ -155,4 +155,50 @@ shaka.media.TimeRangesUtils = class { } return ret; } + + /** + * This operation can be potentially EXPENSIVE and should only be done in + * debug builds for debugging purposes. + * + * @param {TimeRanges} oldRanges + * @param {TimeRanges} newRanges + * @return {?shaka.extern.BufferedRange} The last added range, + * chronologically by presentation time. + */ + static computeAddedRange(oldRanges, newRanges) { + const TimeRangesUtils = shaka.media.TimeRangesUtils; + + if (!oldRanges || !oldRanges.length) { + return null; + } + if (!newRanges || !newRanges.length) { + return TimeRangesUtils.getBufferedInfo(newRanges).pop(); + } + + const newRangesReversed = + TimeRangesUtils.getBufferedInfo(newRanges).reverse(); + const oldRangesReversed = + TimeRangesUtils.getBufferedInfo(oldRanges).reverse(); + for (const newRange of newRangesReversed) { + let foundOverlap = false; + + for (const oldRange of oldRangesReversed) { + if (oldRange.end >= newRange.start && oldRange.end <= newRange.end) { + foundOverlap = true; + + // If the new range goes beyond the corresponding old one, the + // difference is newly-added. + if (newRange.end > oldRange.end) { + return {start: oldRange.end, end: newRange.end}; + } + } + } + + if (!foundOverlap) { + return newRange; + } + } + + return null; + } }; diff --git a/test/media/drm_engine_integration.js b/test/media/drm_engine_integration.js index 168f7ab641..8b3f587af9 100644 --- a/test/media/drm_engine_integration.js +++ b/test/media/drm_engine_integration.js @@ -209,10 +209,10 @@ describe('DrmEngine', () => { await drmEngine.initForPlayback(variants, manifest.offlineSessionIds); await drmEngine.attach(video); await mediaSourceEngine.appendBuffer( - ContentType.VIDEO, videoInitSegment, null, null, + ContentType.VIDEO, videoInitSegment, null, /* hasClosedCaptions= */ false); await mediaSourceEngine.appendBuffer( - ContentType.AUDIO, audioInitSegment, null, null, + ContentType.AUDIO, audioInitSegment, null, /* hasClosedCaptions= */ false); await encryptedEventSeen; // With PlayReady, a persistent license policy can cause a different @@ -245,11 +245,13 @@ describe('DrmEngine', () => { } } + const reference = dummyReference(0, 10); + await mediaSourceEngine.appendBuffer( - ContentType.VIDEO, videoSegment, 0, 10, + ContentType.VIDEO, videoSegment, reference, /* hasClosedCaptions= */ false); await mediaSourceEngine.appendBuffer( - ContentType.AUDIO, audioSegment, 0, 10, + ContentType.AUDIO, audioSegment, reference, /* hasClosedCaptions= */ false); expect(video.buffered.end(0)).toBeGreaterThan(0); @@ -305,10 +307,10 @@ describe('DrmEngine', () => { await drmEngine.initForPlayback(variants, manifest.offlineSessionIds); await drmEngine.attach(video); await mediaSourceEngine.appendBuffer( - ContentType.VIDEO, videoInitSegment, null, null, + ContentType.VIDEO, videoInitSegment, null, /* hasClosedCaptions= */ false); await mediaSourceEngine.appendBuffer( - ContentType.AUDIO, audioInitSegment, null, null, + ContentType.AUDIO, audioInitSegment, null, /* hasClosedCaptions= */ false); await encryptedEventSeen; @@ -326,11 +328,13 @@ describe('DrmEngine', () => { } } + const reference = dummyReference(0, 10); + await mediaSourceEngine.appendBuffer( - ContentType.VIDEO, videoSegment, 0, 10, + ContentType.VIDEO, videoSegment, reference, /* hasClosedCaptions= */ false); await mediaSourceEngine.appendBuffer( - ContentType.AUDIO, audioSegment, 0, 10, + ContentType.AUDIO, audioSegment, reference, /* hasClosedCaptions= */ false); expect(video.buffered.end(0)).toBeGreaterThan(0); @@ -345,4 +349,16 @@ describe('DrmEngine', () => { expect(video.currentTime).toBeGreaterThan(0); }); }); // describe('ClearKey') + + function dummyReference(startTime, endTime) { + return new shaka.media.SegmentReference( + startTime, endTime, + /* uris= */ () => ['foo://bar'], + /* startByte= */ 0, + /* endByte= */ null, + /* initSegmentReference= */ null, + /* timestampOffset= */ 0, + /* appendWindowStart= */ 0, + /* appendWindowEnd= */ Infinity); + } }); diff --git a/test/media/media_source_engine_integration.js b/test/media/media_source_engine_integration.js index 9d384c150d..ebbd49a370 100644 --- a/test/media/media_source_engine_integration.js +++ b/test/media/media_source_engine_integration.js @@ -57,32 +57,32 @@ describe('MediaSourceEngine', () => { function appendInit(type) { const segment = generators[type].getInitSegment(Date.now() / 1000); + const reference = null; return mediaSourceEngine.appendBuffer( - type, segment, null, null, /* hasClosedCaptions= */ false); + type, segment, reference, /* hasClosedCaptions= */ false); } function append(type, segmentNumber) { const segment = generators[type] .getSegment(segmentNumber, Date.now() / 1000); + const reference = dummyReference(type, segmentNumber); return mediaSourceEngine.appendBuffer( - type, segment, null, null, /* hasClosedCaptions= */ false); + type, segment, reference, /* hasClosedCaptions= */ false); } - // The start time and end time should be null for init segment with closed - // captions. function appendInitWithClosedCaptions(type) { const segment = generators[type].getInitSegment(Date.now() / 1000); - return mediaSourceEngine.appendBuffer(type, segment, /* startTime= */ null, - /* endTime= */ null, /* hasClosedCaptions= */ true); + const reference = null; + return mediaSourceEngine.appendBuffer( + type, segment, reference, /* hasClosedCaptions= */ true); } - // The start time and end time should be valid for the segments with closed - // captions. function appendWithClosedCaptions(type, segmentNumber) { const segment = generators[type] .getSegment(segmentNumber, Date.now() / 1000); - return mediaSourceEngine.appendBuffer(type, segment, /* startTime= */ 0, - /* endTime= */ 2, /* hasClosedCaptions= */ true); + const reference = dummyReference(type, segmentNumber); + return mediaSourceEngine.appendBuffer( + type, segment, reference, /* hasClosedCaptions= */ true); } function buffered(type, time) { @@ -93,6 +93,20 @@ describe('MediaSourceEngine', () => { return mediaSourceEngine.bufferStart(type); } + function dummyReference(type, segmentNumber) { + const start = segmentNumber * metadata[type].segmentDuration; + const end = (segmentNumber + 1) * metadata[type].segmentDuration; + return new shaka.media.SegmentReference( + start, end, + /* uris= */ () => ['foo://bar'], + /* startByte= */ 0, + /* endByte= */ null, + /* initSegmentReference= */ null, + /* timestampOffset= */ 0, + /* appendWindowStart= */ 0, + /* appendWindowEnd= */ Infinity); + } + function remove(type, segmentNumber) { const start = segmentNumber * metadata[type].segmentDuration; const end = (segmentNumber + 1) * metadata[type].segmentDuration; diff --git a/test/media/media_source_engine_unit.js b/test/media/media_source_engine_unit.js index 7e58af6770..a7a37f02c2 100644 --- a/test/media/media_source_engine_unit.js +++ b/test/media/media_source_engine_unit.js @@ -346,7 +346,7 @@ describe('MediaSourceEngine', () => { it('appends the given data', async () => { const p = mediaSourceEngine.appendBuffer( - ContentType.AUDIO, buffer, null, null, + ContentType.AUDIO, buffer, null, /* hasClosedCaptions= */ false); expect(audioSourceBuffer.appendBuffer).toHaveBeenCalledWith(buffer); audioSourceBuffer.updateend(); @@ -363,7 +363,7 @@ describe('MediaSourceEngine', () => { jasmine.objectContaining({message: 'fail!'}))); await expectAsync( mediaSourceEngine.appendBuffer( - ContentType.AUDIO, buffer, null, null, + ContentType.AUDIO, buffer, null, /* hasClosedCaptions= */ false)) .toBeRejectedWith(expected); expect(audioSourceBuffer.appendBuffer).toHaveBeenCalledWith(buffer); @@ -382,7 +382,7 @@ describe('MediaSourceEngine', () => { ContentType.AUDIO)); await expectAsync( mediaSourceEngine.appendBuffer( - ContentType.AUDIO, buffer, null, null, + ContentType.AUDIO, buffer, null, /* hasClosedCaptions= */ false)) .toBeRejectedWith(expected); expect(audioSourceBuffer.appendBuffer).toHaveBeenCalledWith(buffer); @@ -403,10 +403,10 @@ describe('MediaSourceEngine', () => { ContentType.AUDIO)); const p1 = mediaSourceEngine.appendBuffer( - ContentType.AUDIO, buffer, null, null, + ContentType.AUDIO, buffer, null, /* hasClosedCaptions= */ false); const p2 = mediaSourceEngine.appendBuffer( - ContentType.AUDIO, buffer, null, null, + ContentType.AUDIO, buffer, null, /* hasClosedCaptions= */ false); audioSourceBuffer.updateend(); await expectAsync(p1).toBeResolved(); @@ -416,7 +416,7 @@ describe('MediaSourceEngine', () => { it('rejects the promise if this operation fails async', async () => { mockVideo.error = {code: 5}; const p = mediaSourceEngine.appendBuffer( - ContentType.AUDIO, buffer, null, null, + ContentType.AUDIO, buffer, null, /* hasClosedCaptions= */ false); audioSourceBuffer.error(); audioSourceBuffer.updateend(); @@ -433,11 +433,11 @@ describe('MediaSourceEngine', () => { it('queues operations on a single SourceBuffer', async () => { /** @type {!shaka.test.StatusPromise} */ const p1 = new shaka.test.StatusPromise(mediaSourceEngine.appendBuffer( - ContentType.AUDIO, buffer, null, null, + ContentType.AUDIO, buffer, null, /* hasClosedCaptions= */ false)); /** @type {!shaka.test.StatusPromise} */ const p2 = new shaka.test.StatusPromise(mediaSourceEngine.appendBuffer( - ContentType.AUDIO, buffer2, null, null, + ContentType.AUDIO, buffer2, null, /* hasClosedCaptions= */ false)); expect(audioSourceBuffer.appendBuffer).toHaveBeenCalledWith(buffer); @@ -456,15 +456,15 @@ describe('MediaSourceEngine', () => { it('queues operations independently for different types', async () => { /** @type {!shaka.test.StatusPromise} */ const p1 = new shaka.test.StatusPromise(mediaSourceEngine.appendBuffer( - ContentType.AUDIO, buffer, null, null, + ContentType.AUDIO, buffer, null, /* hasClosedCaptions= */ false)); /** @type {!shaka.test.StatusPromise} */ const p2 = new shaka.test.StatusPromise(mediaSourceEngine.appendBuffer( - ContentType.AUDIO, buffer2, null, null, + ContentType.AUDIO, buffer2, null, /* hasClosedCaptions= */ false)); /** @type {!shaka.test.StatusPromise} */ const p3 = new shaka.test.StatusPromise(mediaSourceEngine.appendBuffer( - ContentType.VIDEO, buffer3, null, null, + ContentType.VIDEO, buffer3, null, /* hasClosedCaptions= */ false)); expect(audioSourceBuffer.appendBuffer).toHaveBeenCalledWith(buffer); @@ -499,13 +499,13 @@ describe('MediaSourceEngine', () => { }); const p1 = mediaSourceEngine.appendBuffer( - ContentType.AUDIO, buffer, null, null, + ContentType.AUDIO, buffer, null, /* hasClosedCaptions= */ false); const p2 = mediaSourceEngine.appendBuffer( - ContentType.AUDIO, buffer2, null, null, + ContentType.AUDIO, buffer2, null, /* hasClosedCaptions= */ false); const p3 = mediaSourceEngine.appendBuffer( - ContentType.AUDIO, buffer3, null, null, + ContentType.AUDIO, buffer3, null, /* hasClosedCaptions= */ false); await expectAsync(p1).toBeResolved(); @@ -519,8 +519,9 @@ describe('MediaSourceEngine', () => { it('forwards to TextEngine', async () => { const data = new ArrayBuffer(0); expect(mockTextEngine.appendBuffer).not.toHaveBeenCalled(); + const reference = dummyReference(0, 10); await mediaSourceEngine.appendBuffer( - ContentType.TEXT, data, 0, 10, /* hasClosedCaptions= */ false); + ContentType.TEXT, data, reference, /* hasClosedCaptions= */ false); expect(mockTextEngine.appendBuffer).toHaveBeenCalledWith( data, 0, 10); }); @@ -538,7 +539,7 @@ describe('MediaSourceEngine', () => { const init = async () => { await mediaSourceEngine.init(initObject, false); await mediaSourceEngine.appendBuffer( - ContentType.VIDEO, buffer, null, null, + ContentType.VIDEO, buffer, null, /* hasClosedCaptions= */ false); expect(mockTextEngine.storeAndAppendClosedCaptions).toHaveBeenCalled(); expect(videoSourceBuffer.appendBuffer).toHaveBeenCalled(); @@ -568,7 +569,7 @@ describe('MediaSourceEngine', () => { const init = async () => { await mediaSourceEngine.init(initObject, false); await mediaSourceEngine.appendBuffer( - ContentType.VIDEO, buffer, null, null, + ContentType.VIDEO, buffer, null, /* hasClosedCaptions= */ false); expect(mockTextEngine.storeAndAppendClosedCaptions) .not.toHaveBeenCalled(); @@ -599,7 +600,8 @@ describe('MediaSourceEngine', () => { // Initialize the closed caption parser. const appendInit = mediaSourceEngine.appendBuffer( - ContentType.VIDEO, buffer, null, null, true); + ContentType.VIDEO, buffer, null, + /* hasClosedCaptions= */ true); // In MediaSourceEngine, appendBuffer() is async and Promise-based, but // at the browser level, it's event-based. // MediaSourceEngine waits for the 'updateend' event from the @@ -612,8 +614,9 @@ describe('MediaSourceEngine', () => { expect(mockTextEngine.storeAndAppendClosedCaptions).not .toHaveBeenCalled(); // Parse and append the closed captions embedded in video stream. + const reference = dummyReference(0, 1000); const appendVideo = mediaSourceEngine.appendBuffer( - ContentType.VIDEO, buffer, 0, Infinity, true); + ContentType.VIDEO, buffer, reference, true); videoSourceBuffer.updateend(); await appendVideo; @@ -837,11 +840,11 @@ describe('MediaSourceEngine', () => { it('waits for all previous operations to complete', async () => { /** @type {!shaka.test.StatusPromise} */ const p1 = new shaka.test.StatusPromise(mediaSourceEngine.appendBuffer( - ContentType.AUDIO, buffer, null, null, + ContentType.AUDIO, buffer, null, /* hasClosedCaptions= */ false)); /** @type {!shaka.test.StatusPromise} */ const p2 = new shaka.test.StatusPromise(mediaSourceEngine.appendBuffer( - ContentType.VIDEO, buffer, null, null, + ContentType.VIDEO, buffer, null, /* hasClosedCaptions= */ false)); /** @type {!shaka.test.StatusPromise} */ const p3 = new shaka.test.StatusPromise(mediaSourceEngine.endOfStream()); @@ -864,11 +867,11 @@ describe('MediaSourceEngine', () => { it('makes subsequent operations wait', async () => { /** @type {!Promise} */ const p1 = mediaSourceEngine.endOfStream(); - mediaSourceEngine.appendBuffer(ContentType.AUDIO, buffer, null, null, + mediaSourceEngine.appendBuffer(ContentType.AUDIO, buffer, null, /* hasClosedCaptions= */ false); - mediaSourceEngine.appendBuffer(ContentType.VIDEO, buffer, null, null, + mediaSourceEngine.appendBuffer(ContentType.VIDEO, buffer, null, /* hasClosedCaptions= */ false); - mediaSourceEngine.appendBuffer(ContentType.VIDEO, buffer2, null, null, + mediaSourceEngine.appendBuffer(ContentType.VIDEO, buffer2, null, /* hasClosedCaptions= */ false); // endOfStream hasn't been called yet because blocking multiple queues @@ -896,7 +899,7 @@ describe('MediaSourceEngine', () => { mockMediaSource.endOfStream.and.throwError(new Error()); /** @type {!Promise} */ const p1 = mediaSourceEngine.endOfStream(); - mediaSourceEngine.appendBuffer(ContentType.AUDIO, buffer, null, null, + mediaSourceEngine.appendBuffer(ContentType.AUDIO, buffer, null, /* hasClosedCaptions= */ false); expect(audioSourceBuffer.appendBuffer).not.toHaveBeenCalled(); @@ -928,11 +931,11 @@ describe('MediaSourceEngine', () => { it('waits for all previous operations to complete', async () => { /** @type {!shaka.test.StatusPromise} */ const p1 = new shaka.test.StatusPromise(mediaSourceEngine.appendBuffer( - ContentType.AUDIO, buffer, null, null, + ContentType.AUDIO, buffer, null, /* hasClosedCaptions= */ false)); /** @type {!shaka.test.StatusPromise} */ const p2 = new shaka.test.StatusPromise(mediaSourceEngine.appendBuffer( - ContentType.VIDEO, buffer, null, null, + ContentType.VIDEO, buffer, null, /* hasClosedCaptions= */ false)); /** @type {!shaka.test.StatusPromise} */ const p3 = @@ -956,11 +959,11 @@ describe('MediaSourceEngine', () => { it('makes subsequent operations wait', async () => { /** @type {!Promise} */ const p1 = mediaSourceEngine.setDuration(100); - mediaSourceEngine.appendBuffer(ContentType.AUDIO, buffer, null, null, + mediaSourceEngine.appendBuffer(ContentType.AUDIO, buffer, null, /* hasClosedCaptions= */ false); - mediaSourceEngine.appendBuffer(ContentType.VIDEO, buffer, null, null, + mediaSourceEngine.appendBuffer(ContentType.VIDEO, buffer, null, /* hasClosedCaptions= */ false); - mediaSourceEngine.appendBuffer(ContentType.VIDEO, buffer2, null, null, + mediaSourceEngine.appendBuffer(ContentType.VIDEO, buffer2, null, /* hasClosedCaptions= */ false); // The setter hasn't been called yet because blocking multiple queues @@ -989,7 +992,7 @@ describe('MediaSourceEngine', () => { mockMediaSource.durationSetter.and.throwError(new Error()); /** @type {!Promise} */ const p1 = mediaSourceEngine.setDuration(100); - mediaSourceEngine.appendBuffer(ContentType.AUDIO, buffer, null, null, + mediaSourceEngine.appendBuffer(ContentType.AUDIO, buffer, null, /* hasClosedCaptions= */ false); expect(audioSourceBuffer.appendBuffer).not.toHaveBeenCalled(); @@ -1013,9 +1016,9 @@ describe('MediaSourceEngine', () => { }); it('waits for all operations to complete', async () => { - mediaSourceEngine.appendBuffer(ContentType.AUDIO, buffer, null, null, + mediaSourceEngine.appendBuffer(ContentType.AUDIO, buffer, null, /* hasClosedCaptions= */ false); - mediaSourceEngine.appendBuffer(ContentType.VIDEO, buffer, null, null, + mediaSourceEngine.appendBuffer(ContentType.VIDEO, buffer, null, /* hasClosedCaptions= */ false); /** @type {!shaka.test.StatusPromise} */ @@ -1033,7 +1036,7 @@ describe('MediaSourceEngine', () => { it('resolves even when a pending operation fails', async () => { const p = mediaSourceEngine.appendBuffer( - ContentType.AUDIO, buffer, null, null, + ContentType.AUDIO, buffer, null, /* hasClosedCaptions= */ false); const d = mediaSourceEngine.destroy(); @@ -1058,10 +1061,10 @@ describe('MediaSourceEngine', () => { it('cancels operations that have not yet started', async () => { mediaSourceEngine.appendBuffer( - ContentType.AUDIO, buffer, null, null, + ContentType.AUDIO, buffer, null, /* hasClosedCaptions= */ false); const rejected = mediaSourceEngine.appendBuffer( - ContentType.AUDIO, buffer2, null, null, + ContentType.AUDIO, buffer2, null, /* hasClosedCaptions= */ false); // Create the expectation first so we don't get unhandled rejection errors const expected = expectAsync(rejected).toBeRejected(); @@ -1085,7 +1088,7 @@ describe('MediaSourceEngine', () => { it('cancels blocking operations that have not yet started', async () => { const p1 = mediaSourceEngine.appendBuffer( - ContentType.AUDIO, buffer, null, null, + ContentType.AUDIO, buffer, null, /* hasClosedCaptions= */ false); const p2 = mediaSourceEngine.endOfStream(); const d = mediaSourceEngine.destroy(); @@ -1100,7 +1103,7 @@ describe('MediaSourceEngine', () => { const d = mediaSourceEngine.destroy(); await expectAsync( mediaSourceEngine.appendBuffer( - ContentType.AUDIO, buffer, null, null, + ContentType.AUDIO, buffer, null, /* hasClosedCaptions= */ false)) .toBeRejected(); await d; @@ -1198,4 +1201,16 @@ describe('MediaSourceEngine', () => { } }); } + + function dummyReference(startTime, endTime) { + return new shaka.media.SegmentReference( + startTime, endTime, + /* uris= */ () => ['foo://bar'], + /* startByte= */ 0, + /* endByte= */ null, + /* initSegmentReference= */ null, + /* timestampOffset= */ 0, + /* appendWindowStart= */ 0, + /* appendWindowEnd= */ Infinity); + } }); diff --git a/test/media/streaming_engine_unit.js b/test/media/streaming_engine_unit.js index 079d310ccf..2a5e1beefb 100644 --- a/test/media/streaming_engine_unit.js +++ b/test/media/streaming_engine_unit.js @@ -868,9 +868,9 @@ describe('StreamingEngine', () => { netEngineDelays.audio = 1.0; netEngineDelays.video = 5.0; // Need init segment and media segment - mediaSourceEngine.appendBuffer.and.callFake((type, data, start, end) => { + mediaSourceEngine.appendBuffer.and.callFake((type, data, reference) => { // Call to the underlying implementation. - const p = mediaSourceEngine.appendBufferImpl(type, data, start, end); + const p = mediaSourceEngine.appendBufferImpl(type, data, reference); // Validate that no one media type got ahead of any other. let minBuffered = Infinity; @@ -1012,8 +1012,8 @@ describe('StreamingEngine', () => { const bufferEnd = {audio: 0, video: 0, text: 0}; mediaSourceEngine.appendBuffer.and.callFake( - (type, data, start, end) => { - bufferEnd[type] = end; + (type, data, reference) => { + bufferEnd[type] = reference && reference.endTime; return Promise.resolve(); }); mediaSourceEngine.bufferEnd.and.callFake((type) => { @@ -1080,9 +1080,9 @@ describe('StreamingEngine', () => { // Replace the whole spy since we want to call the original. mediaSourceEngine.appendBuffer = jasmine.createSpy('appendBuffer') - .and.callFake(async (type, data, start, end) => { + .and.callFake(async (type, data, reference) => { await p; - return Util.invokeSpy(old, type, data, start, end); + return Util.invokeSpy(old, type, data, reference); }); await streamingEngine.start(); @@ -1107,9 +1107,9 @@ describe('StreamingEngine', () => { // Replace the whole spy since we want to call the original. mediaSourceEngine.appendBuffer = jasmine.createSpy('appendBuffer') - .and.callFake(async (type, data, start, end) => { + .and.callFake(async (type, data, reference) => { await p; - return Util.invokeSpy(old, type, data, start, end); + return Util.invokeSpy(old, type, data, reference); }); await streamingEngine.start(); @@ -1750,9 +1750,9 @@ describe('StreamingEngine', () => { // eslint-disable-next-line no-restricted-syntax shaka.test.FakeMediaSourceEngine.prototype.appendBufferImpl; mediaSourceEngine.appendBuffer.and.callFake( - (type, data, startTime, endTime) => { + (type, data, reference) => { expect(presentationTimeInSeconds).toBe(125); - if (startTime >= 100) { + if (reference && reference.startTime >= 100) { // Ignore a possible call for the first Period. expect(Util.invokeSpy(timeline.getSegmentAvailabilityStart)) .toBe(100); @@ -1765,7 +1765,7 @@ describe('StreamingEngine', () => { // eslint-disable-next-line no-restricted-syntax return originalAppendBuffer.call( - mediaSourceEngine, type, data, startTime, endTime); + mediaSourceEngine, type, data, reference); }); await runTest(slideSegmentAvailabilityWindow); @@ -1862,14 +1862,14 @@ describe('StreamingEngine', () => { // eslint-disable-next-line no-restricted-syntax shaka.test.FakeMediaSourceEngine.prototype.appendBufferImpl; mediaSourceEngine.appendBuffer.and.callFake( - (type, data, startTime, endTime) => { + (type, data, reference) => { // Reject the first video init segment. if (data == segmentData[ContentType.VIDEO].initSegments[0]) { return Promise.reject(expectedError); } else { // eslint-disable-next-line no-restricted-syntax return originalAppendBuffer.call( - mediaSourceEngine, type, data, startTime, endTime); + mediaSourceEngine, type, data, reference); } }); @@ -1896,14 +1896,14 @@ describe('StreamingEngine', () => { // eslint-disable-next-line no-restricted-syntax shaka.test.FakeMediaSourceEngine.prototype.appendBufferImpl; mediaSourceEngine.appendBuffer.and.callFake( - (type, data, startTime, endTime) => { + (type, data, reference) => { // Reject the first audio segment. if (data == segmentData[ContentType.AUDIO].segments[0]) { return Promise.reject(expectedError); } else { // eslint-disable-next-line no-restricted-syntax return originalAppendBuffer.call( - mediaSourceEngine, type, data, startTime, endTime); + mediaSourceEngine, type, data, reference); } }); @@ -2187,7 +2187,7 @@ describe('StreamingEngine', () => { // Now that we're streaming, throw QuotaExceededError on every segment // to quickly trigger the quota error. const appendBufferSpy = jasmine.createSpy('appendBuffer'); - appendBufferSpy.and.callFake((type, data, startTime, endTime) => { + appendBufferSpy.and.callFake((type, data, reference) => { throw new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.MEDIA, @@ -2373,9 +2373,10 @@ describe('StreamingEngine', () => { // Throw two QuotaExceededErrors at different times. let numErrorsThrown = 0; appendBufferSpy.and.callFake( - (type, data, startTime, endTime) => { - const throwError = (numErrorsThrown == 0 && startTime == 10) || - (numErrorsThrown == 1 && startTime == 20); + (type, data, reference) => { + const throwError = reference && + ((numErrorsThrown == 0 && reference.startTime == 10) || + (numErrorsThrown == 1 && reference.startTime == 20)); if (throwError) { numErrorsThrown++; throw new shaka.util.Error( @@ -2386,7 +2387,7 @@ describe('StreamingEngine', () => { } else { // eslint-disable-next-line no-restricted-syntax const p = originalAppendBuffer.call( - mediaSourceEngine, type, data, startTime, endTime); + mediaSourceEngine, type, data, reference); return p; } }); @@ -2435,8 +2436,8 @@ describe('StreamingEngine', () => { // Throw QuotaExceededError multiple times after at least one segment of // each type has been appended. appendBufferSpy.and.callFake( - (type, data, startTime, endTime) => { - if (startTime >= 10) { + (type, data, reference) => { + if (reference && reference.startTime >= 10) { throw new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.MEDIA, @@ -2445,7 +2446,7 @@ describe('StreamingEngine', () => { } else { // eslint-disable-next-line no-restricted-syntax const p = originalAppendBuffer.call( - mediaSourceEngine, type, data, startTime, endTime); + mediaSourceEngine, type, data, reference); return p; } }); @@ -2986,8 +2987,8 @@ describe('StreamingEngine', () => { // Naive buffered range tracking that only tracks the buffer end. const bufferEnd = {audio: 0, video: 0, text: 0}; - mediaSourceEngine.appendBuffer.and.callFake((type, data, start, end) => { - bufferEnd[type] = end; + mediaSourceEngine.appendBuffer.and.callFake((type, data, reference) => { + bufferEnd[type] = reference && reference.endTime; return Promise.resolve(); }); mediaSourceEngine.bufferEnd.and.callFake((type) => bufferEnd[type]); @@ -3281,8 +3282,8 @@ describe('StreamingEngine', () => { const bufferEnd = {audio: 0, video: 0, text: 0}; mediaSourceEngine.appendBuffer.and.callFake( - (type, data, start, end) => { - bufferEnd[type] = end; + (type, data, reference) => { + bufferEnd[type] = reference && reference.endTime; return Promise.resolve(); }); mediaSourceEngine.bufferEnd.and.callFake((type) => { diff --git a/test/test/util/fake_media_source_engine.js b/test/test/util/fake_media_source_engine.js index 12efe81ee2..f7e784bc32 100644 --- a/test/test/util/fake_media_source_engine.js +++ b/test/test/util/fake_media_source_engine.js @@ -78,8 +78,8 @@ shaka.test.FakeMediaSourceEngine = class { /** @type {!jasmine.Spy} */ this.appendBuffer = jasmine.createSpy('appendBuffer') - .and.callFake((type, data, start, end) => - this.appendBufferImpl(type, data, start, end)); + .and.callFake((type, data, reference) => + this.appendBufferImpl(type, data, reference)); /** @type {!jasmine.Spy} */ this.clear = jasmine.createSpy('clear') @@ -229,11 +229,10 @@ shaka.test.FakeMediaSourceEngine = class { /** * @param {string} type * @param {!ArrayBuffer} data - * @param {?number} startTime - * @param {?number} endTime + * @param {?shaka.media.SegmentReference} reference * @return {!Promise} */ - appendBufferImpl(type, data, startTime, endTime) { + appendBufferImpl(type, data, reference) { if (!this.segments[type]) { throw new Error('unexpected type'); } @@ -257,8 +256,7 @@ shaka.test.FakeMediaSourceEngine = class { } if (i >= 0) { // Update the list of which init segment was appended last. - expect(startTime).toBe(null); - expect(endTime).toBe(null); + expect(reference).toBe(null); this.initSegments[type] = this.segmentData[type].initSegments.map((c) => false); this.initSegments[type][i] = true; @@ -289,8 +287,11 @@ shaka.test.FakeMediaSourceEngine = class { const expectedStartTime = i * segmentData.segmentDuration; const expectedEndTime = expectedStartTime + segmentData.segmentDuration; expect(appendedTime).toBe(expectedStartTime); - expect(startTime).toBe(expectedStartTime); - expect(endTime).toBe(expectedEndTime); + expect(reference).not.toBe(null); + if (reference) { + expect(reference.startTime).toBe(expectedStartTime); + expect(reference.endTime).toBe(expectedEndTime); + } this.segments[type][i] = true; return Promise.resolve();