UNPKG

shaka-player

Version:
1,164 lines (991 loc) 37.4 kB
/** * @license * Copyright 2016 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * @typedef {{start: number, end: number}} * * @property {number} start * The start time of the range, in seconds. * @property {number} end * The end time of the range, in seconds. */ var TimeRange; /** * @typedef {{ * buffered: !Array.<TimeRange>, * start: number, * waitingAt: number, * expectedEndTime: number, * expectEvent: boolean, * jumpLargeGaps: (boolean|undefined), * preventDefault: (boolean|undefined) * }} * * @description * Parameters for a test where we start playing inside a buffered range and play * until the end of the buffer. Then, if we expect it, Playhead should jump * to the expected time. Also, if the gap is large, we should get a 'largegap' * event from the Playhead, which we may optionally suppress. * * @property {!Array.<TimeRange>} buffered * The buffered ranges for the test. * @property {number} start * The time to start playing at. * @property {number} waitingAt * The time to pause at and fire a 'waiting' event. * @property {number} expectedEndTime * The expected time at the end of the test. * @property {boolean} expectEvent * If true, expect the 'largegap' event to be fired. * @property {(boolean|undefined)} jumpLargeGaps * If given, set this field of the Playhead configuration. * @property {(boolean|undefined)} preventDefault * If true, call preventDefault() on the 'largegap' event. */ var PlayingTestInfo; /** * @typedef {{ * buffered: !Array.<TimeRange>, * newBuffered: (!Array.<TimeRange>|undefined), * start: number, * seekTo: number, * expectedEndTime: number, * expectEvent: boolean * }} * * @description * Parameters for a test where we start playing inside a buffered range and seek * to a given time, which may have different buffered ranges. If we are in a * gap, Playhead should jump the gap to the expected time. Also, if the gap is * large, we should get a 'largegap' event from the Playhead, which we may * optionally suppress. * * @property {!Array.<TimeRange>} buffered * The buffered ranges for the test. * @property {(!Array.<TimeRange>|undefined)} newBuffered * Used in the unbuffered seek tests. Represents the buffered ranges to * use after the seek. * @property {number} start * The time to start playing at. * @property {number} seekTo * The time to seek to. * @property {number} expectedEndTime * The expected time at the end of the test. * @property {boolean} expectEvent * If true, expect the 'largegap' event to be fired. */ var SeekTestInfo; describe('Playhead', function() { /** @const */ var Util = shaka.test.Util; /** @type {!shaka.test.FakeVideo} */ var video; /** @type {!shaka.test.FakePresentationTimeline} */ var timeline; /** @type {shakaExtern.Manifest} */ var manifest; /** @type {!shaka.media.Playhead} */ var playhead; /** @type {shakaExtern.StreamingConfiguration} */ var config; // Callback to us from Playhead when a valid 'seeking' event occurs. /** @type {!jasmine.Spy} */ var onSeek; // Callback to us from Playhead when an event should be sent to the app. /** @type {!jasmine.Spy} */ var onEvent; beforeAll(function() { jasmine.clock().install(); }); afterAll(function() { jasmine.clock().uninstall(); }); beforeEach(function() { video = new shaka.test.FakeVideo(); timeline = new shaka.test.FakePresentationTimeline(); onSeek = jasmine.createSpy('onSeek'); onEvent = jasmine.createSpy('onEvent'); timeline.isLive.and.returnValue(false); timeline.getSegmentAvailabilityStart.and.returnValue(5); timeline.getSegmentAvailabilityEnd.and.returnValue(60); timeline.getDuration.and.returnValue(60); // These tests should not cause these methods to be invoked. timeline.getSegmentAvailabilityDuration.and.throwError(new Error()); timeline.setDuration.and.throwError(new Error()); manifest = { periods: [], presentationTimeline: timeline, minBufferTime: 10, offlineSessionIds: [] }; config = { rebufferingGoal: 10, bufferingGoal: 5, retryParameters: shaka.net.NetworkingEngine.defaultRetryParameters(), failureCallback: function() {}, bufferBehind: 15, ignoreTextStreamFailures: false, useRelativeCueTimestamps: false, startAtSegmentBoundary: false, smallGapLimit: 0.5, jumpLargeGaps: false, durationBackoff: 1 }; }); afterEach(function(done) { playhead.destroy().then(done); }); describe('getTime', function() { it('returns current time when the video is paused', function() { video.readyState = HTMLMediaElement.HAVE_METADATA; playhead = new shaka.media.Playhead( video, manifest, config, 5 /* startTime */, Util.spyFunc(onSeek), Util.spyFunc(onEvent)); expect(video.currentTime).toBe(5); expect(playhead.getTime()).toBe(5); // Simulate pausing. video.paused = true; timeline.getSegmentAvailabilityStart.and.returnValue(10); timeline.getSegmentAvailabilityEnd.and.returnValue(70); expect(video.currentTime).toBe(5); expect(playhead.getTime()).toBe(5); }); it('returns the correct time when readyState starts at 0', function() { playhead = new shaka.media.Playhead( video, manifest, config, 5 /* startTime */, Util.spyFunc(onSeek), Util.spyFunc(onEvent)); expect(video.addEventListener).toHaveBeenCalledWith( 'loadedmetadata', jasmine.any(Function), false); expect(video.addEventListener).not.toHaveBeenCalledWith( 'seeking', jasmine.any(Function), jasmine.any(Boolean)); expect(playhead.getTime()).toBe(5); expect(video.currentTime).toBe(0); video.readyState = HTMLMediaElement.HAVE_METADATA; video.on['loadedmetadata'](); expect(video.addEventListener).toHaveBeenCalledWith( 'seeking', jasmine.any(Function), false); expect(playhead.getTime()).toBe(5); expect(video.currentTime).toBe(5); video.currentTime = 6; expect(playhead.getTime()).toBe(6); // getTime() should always clamp the time even if the video element // doesn't dispatch 'seeking' events. video.currentTime = 120; expect(playhead.getTime()).toBe(60); video.currentTime = 0; expect(playhead.getTime()).toBe(5); }); it('returns the correct time when readyState starts at 1', function() { video.readyState = HTMLMediaElement.HAVE_METADATA; playhead = new shaka.media.Playhead( video, manifest, config, 5 /* startTime */, Util.spyFunc(onSeek), Util.spyFunc(onEvent)); expect(playhead.getTime()).toBe(5); expect(video.currentTime).toBe(5); video.currentTime = 6; expect(playhead.getTime()).toBe(6); }); it('allows using startTime of 0', function() { video.readyState = HTMLMediaElement.HAVE_METADATA; timeline.isLive.and.returnValue(true); timeline.getDuration.and.returnValue(Infinity); timeline.getSegmentAvailabilityStart.and.returnValue(0); timeline.getSegmentAvailabilityEnd.and.returnValue(60); timeline.getSeekRangeEnd.and.returnValue(60); playhead = new shaka.media.Playhead( video, manifest, config, 0 /* startTime */, Util.spyFunc(onSeek), Util.spyFunc(onEvent)); expect(playhead.getTime()).toBe(0); }); it('bumps startTime back from duration', function() { video.readyState = HTMLMediaElement.HAVE_METADATA; timeline.isLive.and.returnValue(false); timeline.getSegmentAvailabilityStart.and.returnValue(0); timeline.getSegmentAvailabilityEnd.and.returnValue(60); timeline.getSeekRangeEnd.and.returnValue(60); timeline.getDuration.and.returnValue(60); playhead = new shaka.media.Playhead( video, manifest, config, 60 /* startTime */, Util.spyFunc(onSeek), Util.spyFunc(onEvent)); expect(playhead.getTime()).toBe(59); // duration - durationBackoff expect(video.currentTime).toBe(59); // duration - durationBackoff }); it('respects a seek before metadata is loaded', function() { playhead = new shaka.media.Playhead( video, manifest, config, 5 /* startTime */, Util.spyFunc(onSeek), Util.spyFunc(onEvent)); expect(video.addEventListener).toHaveBeenCalledWith( 'loadedmetadata', jasmine.any(Function), false); expect(playhead.getTime()).toBe(5); expect(video.currentTime).toBe(0); // Realism: Chrome fires timeupdate before currentTime changes. video.on['timeupdate'](); video.currentTime = 20; video.on['timeupdate'](); video.currentTime = 30; // This hasn't changed yet, because Playhead delays observing currentTime. expect(playhead.getTime()).toBe(5); // Delay to let Playhead batch up changes to currentTime and observe. jasmine.clock().tick(1000); expect(playhead.getTime()).toBe(30); }); it('does not treat timeupdate as seek close to metadata load', function() { playhead = new shaka.media.Playhead( video, manifest, config, 5 /* startTime */, Util.spyFunc(onSeek), Util.spyFunc(onEvent)); expect(video.addEventListener).toHaveBeenCalledWith( 'loadedmetadata', jasmine.any(Function), false); expect(playhead.getTime()).toBe(5); expect(video.currentTime).toBe(0); // Realism: Edge fires "timeupdate" right before "loadedmetadata". // This was causing a failed assertion in onEarlySeek_(); video.currentTime = 5.001; video.on['timeupdate'](); video.readyState = HTMLMediaElement.HAVE_METADATA; video.on['loadedmetadata'](); // Delay to let Playhead batch up changes to currentTime and observe. jasmine.clock().tick(1000); }); // This is important for recovering from drift. // See: https://github.com/google/shaka-player/issues/1105 // TODO: Re-evaluate after https://github.com/google/shaka-player/issues/999 it('does not change once the initial position is set', function() { timeline.isLive.and.returnValue(true); timeline.getDuration.and.returnValue(Infinity); timeline.getSegmentAvailabilityStart.and.returnValue(0); timeline.getSegmentAvailabilityEnd.and.returnValue(60); timeline.getSeekRangeEnd.and.returnValue(60); playhead = new shaka.media.Playhead( video, manifest, config, null /* startTime */, Util.spyFunc(onSeek), Util.spyFunc(onEvent)); expect(video.addEventListener).toHaveBeenCalledWith( 'loadedmetadata', jasmine.any(Function), false); expect(playhead.getTime()).toBe(60); expect(video.currentTime).toBe(0); // Simulate time passing and the live edge changing. timeline.getSegmentAvailabilityStart.and.returnValue(10); timeline.getSegmentAvailabilityEnd.and.returnValue(70); timeline.getSeekRangeEnd.and.returnValue(70); expect(playhead.getTime()).toBe(60); }); }); // getTime it('clamps playhead after seeking for live', function() { video.readyState = HTMLMediaElement.HAVE_METADATA; video.buffered = createFakeBuffered([{start: 25, end: 55}]); timeline.isLive.and.returnValue(true); timeline.getDuration.and.returnValue(Infinity); timeline.getSegmentAvailabilityStart.and.returnValue(5); timeline.getSegmentAvailabilityEnd.and.returnValue(60); timeline.getSegmentAvailabilityDuration.and.returnValue(30); playhead = new shaka.media.Playhead( video, manifest, config, 5 /* startTime */, Util.spyFunc(onSeek), Util.spyFunc(onEvent)); // Calling on['seeking']() is like dispatching a 'seeking' event. So, each // time we change the video's current time or Playhead changes the video's // current time we must call on['seeking'](), video.on['seeking'](); expect(video.currentTime).toBe(5); expect(playhead.getTime()).toBe(5); // safe = start + rebufferingGoal = 5 + 10 = 15 // safeSeek = safeSeek + 5 = 15 + 5 = 20 // Seek in safe region & in buffered region. video.currentTime = 26; video.on['seeking'](); expect(video.currentTime).toBe(26); expect(playhead.getTime()).toBe(26); expect(onSeek).toHaveBeenCalled(); onSeek.calls.reset(); // Seek in safe region & in unbuffered region. video.currentTime = 24; video.on['seeking'](); expect(video.currentTime).toBe(24); expect(playhead.getTime()).toBe(24); expect(onSeek).toHaveBeenCalled(); onSeek.calls.reset(); // Seek before start, start is unbuffered. video.currentTime = 1; video.on['seeking'](); expect(video.currentTime).toBe(20); expect(playhead.getTime()).toBe(20); expect(onSeek).not.toHaveBeenCalled(); video.on['seeking'](); expect(onSeek).toHaveBeenCalled(); onSeek.calls.reset(); video.buffered = createFakeBuffered([{start: 10, end: 40}]); // Seek outside safe region & in buffered region. video.currentTime = 11; video.on['seeking'](); expect(video.currentTime).toBe(11); expect(playhead.getTime()).toBe(11); expect(onSeek).toHaveBeenCalled(); onSeek.calls.reset(); // Seek outside safe region & in unbuffered region. video.currentTime = 9; video.on['seeking'](); expect(video.currentTime).toBe(20); expect(playhead.getTime()).toBe(20); expect(onSeek).not.toHaveBeenCalled(); video.on['seeking'](); expect(onSeek).toHaveBeenCalled(); onSeek.calls.reset(); // Seek past end. video.currentTime = 120; video.on['seeking'](); expect(video.currentTime).toBe(60); expect(playhead.getTime()).toBe(60); expect(onSeek).not.toHaveBeenCalled(); video.on['seeking'](); expect(onSeek).toHaveBeenCalled(); onSeek.calls.reset(); // Seek before start, start is buffered. video.currentTime = 1; video.on['seeking'](); expect(video.currentTime).toBe(10); expect(playhead.getTime()).toBe(10); expect(onSeek).not.toHaveBeenCalled(); video.on['seeking'](); expect(onSeek).toHaveBeenCalled(); onSeek.calls.reset(); // Seek with safe == end timeline.getSegmentAvailabilityEnd.and.returnValue(12); timeline.getSafeAvailabilityStart.and.returnValue(12); // Seek before start video.currentTime = 4; video.on['seeking'](); expect(video.currentTime).toBe(12); expect(playhead.getTime()).toBe(12); expect(onSeek).not.toHaveBeenCalled(); video.on['seeking'](); expect(onSeek).toHaveBeenCalled(); onSeek.calls.reset(); // Seek in window. video.currentTime = 8; video.on['seeking'](); expect(video.currentTime).toBe(12); expect(playhead.getTime()).toBe(12); expect(onSeek).not.toHaveBeenCalled(); video.on['seeking'](); expect(onSeek).toHaveBeenCalled(); onSeek.calls.reset(); // Seek past end. video.currentTime = 13; video.on['seeking'](); expect(video.currentTime).toBe(12); expect(playhead.getTime()).toBe(12); expect(onSeek).not.toHaveBeenCalled(); video.on['seeking'](); expect(onSeek).toHaveBeenCalled(); }); it('clamps playhead after seeking for VOD', function() { video.readyState = HTMLMediaElement.HAVE_METADATA; video.buffered = createFakeBuffered([{start: 25, end: 55}]); timeline.isLive.and.returnValue(false); timeline.getSegmentAvailabilityStart.and.returnValue(5); timeline.getSafeAvailabilityStart.and.returnValue(5); timeline.getSegmentAvailabilityEnd.and.returnValue(60); timeline.getDuration.and.returnValue(60); timeline.getSegmentAvailabilityDuration.and.returnValue(null); playhead = new shaka.media.Playhead( video, manifest, config, 5 /* startTime */, Util.spyFunc(onSeek), Util.spyFunc(onEvent)); video.on['seeking'](); expect(video.currentTime).toBe(5); expect(playhead.getTime()).toBe(5); // Seek past end. video.currentTime = 120; video.on['seeking'](); expect(video.currentTime).toBe(59); // duration - durationBackoff expect(playhead.getTime()).toBe(59); // duration - durationBackoff expect(onSeek).not.toHaveBeenCalled(); video.on['seeking'](); expect(onSeek).toHaveBeenCalled(); onSeek.calls.reset(); // Seek before start. video.currentTime = 1; video.on['seeking'](); expect(video.currentTime).toBe(5); expect(playhead.getTime()).toBe(5); expect(onSeek).not.toHaveBeenCalled(); video.on['seeking'](); expect(onSeek).toHaveBeenCalled(); }); it('handles live manifests with no seek range', function() { video.buffered = createFakeBuffered([{start: 1000, end: 1030}]); video.readyState = HTMLMediaElement.HAVE_METADATA; timeline.isLive.and.returnValue(true); timeline.getDuration.and.returnValue(Infinity); timeline.getSegmentAvailabilityStart.and.returnValue(1000); timeline.getSegmentAvailabilityEnd.and.returnValue(1000); timeline.getSegmentAvailabilityDuration.and.returnValue(1000); playhead = new shaka.media.Playhead( video, manifest, config, 5 /* startTime */, Util.spyFunc(onSeek), Util.spyFunc(onEvent)); expect(video.currentTime).toBe(1000); video.on['seeking'](); // The availability window slips ahead. timeline.getSegmentAvailabilityStart.and.returnValue(1030); timeline.getSegmentAvailabilityEnd.and.returnValue(1030); video.on['waiting'](); // We expect this to move to 15 seconds ahead of the start of the // availability window, due to the rebuffering goal (10s) and the 5s // for the Chromecast. expect(video.currentTime).toBe(1045); }); describe('clamps playhead after resuming', function() { beforeEach(function() { video.readyState = HTMLMediaElement.HAVE_METADATA; video.buffered = createFakeBuffered([{start: 5, end: 35}]); }); it('(live case)', function() { timeline.isLive.and.returnValue(true); timeline.getDuration.and.returnValue(Infinity); timeline.getSegmentAvailabilityStart.and.returnValue(5); timeline.getSegmentAvailabilityEnd.and.returnValue(60); timeline.getSegmentAvailabilityDuration.and.returnValue(30); playhead = new shaka.media.Playhead( video, manifest, config, 5 /* startTime */, Util.spyFunc(onSeek), Util.spyFunc(onEvent)); video.on['seeking'](); expect(video.currentTime).toBe(5); expect(playhead.getTime()).toBe(5); // Simulate pausing. timeline.getSegmentAvailabilityStart.and.returnValue(10); timeline.getSegmentAvailabilityEnd.and.returnValue(70); timeline.getSegmentAvailabilityDuration.and.returnValue(30); // Because this is buffered, the playhead should move to (start + 5), // which will cause a 'seeking' event. video.on['playing'](); expect(video.currentTime).toBe(15); video.on['seeking'](); expect(playhead.getTime()).toBe(15); expect(onSeek).toHaveBeenCalled(); }); it('(VOD case)', function() { timeline.isLive.and.returnValue(false); timeline.getSegmentAvailabilityStart.and.returnValue(5); timeline.getSafeAvailabilityStart.and.returnValue(5); timeline.getSegmentAvailabilityEnd.and.returnValue(60); timeline.getDuration.and.returnValue(60); timeline.getSegmentAvailabilityDuration.and.returnValue(30); playhead = new shaka.media.Playhead( video, manifest, config, 5 /* startTime */, Util.spyFunc(onSeek), Util.spyFunc(onEvent)); video.on['seeking'](); expect(video.currentTime).toBe(5); expect(playhead.getTime()).toBe(5); // Simulate pausing. timeline.getSegmentAvailabilityStart.and.returnValue(10); timeline.getSafeAvailabilityStart.and.returnValue(10); timeline.getSegmentAvailabilityEnd.and.returnValue(70); timeline.getSegmentAvailabilityDuration.and.returnValue(30); video.on['playing'](); expect(video.currentTime).toBe(10); video.on['seeking'](); expect(playhead.getTime()).toBe(10); expect(onSeek).toHaveBeenCalled(); }); }); // clamps playhead after resuming describe('gap jumping', function() { beforeEach(function() { timeline.isLive.and.returnValue(false); timeline.getSafeAvailabilityStart.and.returnValue(0); timeline.getSegmentAvailabilityStart.and.returnValue(0); timeline.getSegmentAvailabilityEnd.and.returnValue(60); timeline.getDuration.and.returnValue(60); config.smallGapLimit = 1; }); describe('when playing', function() { describe('with small gaps', function() { playingTest('won\'t jump at end of single region', { buffered: [{start: 0, end: 10}], start: 3, waitingAt: 10, expectEvent: false, expectedEndTime: 10 }); playingTest('won\'t jump at end of multiple regions', { buffered: [{start: 0, end: 10}, {start: 20, end: 30}], start: 24, waitingAt: 30, expectEvent: false, expectedEndTime: 30 }); playingTest('will jump small gap', { buffered: [{start: 0, end: 10}, {start: 11, end: 20}], start: 5, waitingAt: 10, expectEvent: false, expectedEndTime: 11 }); playingTest('won\'t skip a buffered range', { buffered: [{start: 0, end: 10}, {start: 11, end: 20}, {start: 21, end: 30}], start: 5, waitingAt: 10, expectEvent: false, expectedEndTime: 11 }); playingTest('will jump gap into last buffer', { buffered: [{start: 0, end: 10}, {start: 11, end: 20}, {start: 21, end: 30}], start: 15, waitingAt: 20, expectEvent: false, expectedEndTime: 21 }); }); // with small gaps describe('with large gaps', function() { playingTest('will fire an event', { buffered: [{start: 0, end: 10}, {start: 30, end: 40}], start: 5, waitingAt: 10, expectEvent: true, expectedEndTime: 10 }); playingTest('will jump large gaps if set', { buffered: [{start: 0, end: 10}, {start: 30, end: 40}], start: 5, waitingAt: 10, jumpLargeGaps: true, expectEvent: true, expectedEndTime: 30 }); playingTest('will only jump one buffer', { buffered: [{start: 0, end: 10}, {start: 30, end: 40}, {start: 50, end: 60}], start: 5, waitingAt: 10, jumpLargeGaps: true, expectEvent: true, expectedEndTime: 30 }); playingTest('will jump into last buffer', { buffered: [{start: 0, end: 10}, {start: 20, end: 30}, {start: 50, end: 60}], start: 24, waitingAt: 30, jumpLargeGaps: true, expectEvent: true, expectedEndTime: 50 }); playingTest('won\'t jump gaps when preventDefault() is called', { buffered: [{start: 0, end: 10}, {start: 30, end: 40}], start: 5, waitingAt: 10, jumpLargeGaps: true, preventDefault: true, expectEvent: true, expectedEndTime: 10 }); }); // with large gaps /** * @param {string} name * @param {PlayingTestInfo} data */ function playingTest(name, data) { it(name, function() { video.buffered = createFakeBuffered(data.buffered); video.currentTime = data.start; video.readyState = HTMLMediaElement.HAVE_ENOUGH_DATA; onEvent.and.callFake(function(event) { if (data.preventDefault) event.preventDefault(); }); config.jumpLargeGaps = !!data.jumpLargeGaps; playhead = new shaka.media.Playhead( video, manifest, config, data.start /* startTime */, Util.spyFunc(onSeek), Util.spyFunc(onEvent)); jasmine.clock().tick(1000); for (var time = data.start; time < data.waitingAt; time++) { video.currentTime = time; jasmine.clock().tick(1000); // Make sure Playhead didn't adjust the time yet. expect(video.currentTime).toBe(time); } expect(onEvent).not.toHaveBeenCalled(); video.currentTime = data.waitingAt; video.readyState = HTMLMediaElement.HAVE_CURRENT_DATA; video.on['waiting'](); jasmine.clock().tick(1000); expect(onEvent).toHaveBeenCalledTimes(data.expectEvent ? 1 : 0); expect(video.currentTime).toBe(data.expectedEndTime); }); } }); // when playing describe('with buffered seeks', function() { describe('with small gaps', function() { seekTest('won\'t seek when past the end', { buffered: [{start: 0, end: 10}], start: 4, seekTo: 14, expectedEndTime: 14, expectEvent: false }); seekTest('will jump when seeking into gap', { buffered: [{start: 0, end: 10}, {start: 11, end: 20}], start: 3, seekTo: 10.4, expectedEndTime: 11, expectEvent: false }); seekTest('won\'t jump multiple buffers', { buffered: [{start: 0, end: 10}, {start: 11, end: 20}, {start: 21, end: 30}], start: 3, seekTo: 10.4, expectedEndTime: 11, expectEvent: false }); seekTest('will jump into last range with seeking', { buffered: [{start: 0, end: 10}, {start: 11, end: 20}, {start: 21, end: 30}], start: 3, seekTo: 20.5, expectedEndTime: 21, expectEvent: false }); seekTest('treats large gaps as small if playhead near end', { buffered: [{start: 0, end: 10}, {start: 30, end: 40}], start: 3, seekTo: 29.2, expectedEndTime: 30, expectEvent: false }); }); // with small gaps describe('with large gaps', function() { seekTest('will raise event', { buffered: [{start: 0, end: 10}, {start: 30, end: 40}], start: 5, seekTo: 12, expectedEndTime: 12, expectEvent: true }); seekTest('will jump large gaps', { buffered: [{start: 0, end: 10}, {start: 30, end: 40}], start: 5, seekTo: 12, jumpLargeGaps: true, expectedEndTime: 30, expectEvent: true }); seekTest('won\'t jump if preventDefault() is called', { buffered: [{start: 0, end: 10}, {start: 30, end: 40}], start: 5, seekTo: 12, jumpLargeGaps: true, preventDefault: true, expectedEndTime: 12, expectEvent: true }); }); // with large gaps }); // with buffered seeks describe('with unbuffered seeks', function() { describe('with small gaps', function() { seekTest('won\'t jump when seeking into buffered range', { // [0-10], [20-30], [31-40] buffered: [{start: 0, end: 10}], newBuffered: [{start: 20, end: 30}, {start: 31, end: 40}], start: 3, seekTo: 22, expectedEndTime: 22, expectEvent: false }); // Seeking to the beginning is considered an unbuffered seek even if // there is a gap. seekTest('will jump a small gap at the beginning', { buffered: [{start: 0.2, end: 10}], newBuffered: [{start: 0.2, end: 10}], start: 4, seekTo: 0, expectedEndTime: 0.2, expectEvent: false }); seekTest('will jump when seeking into gap', { // [0-10], [20-30], [31-40] buffered: [{start: 0, end: 10}], newBuffered: [{start: 20, end: 30}, {start: 31, end: 40}], start: 3, seekTo: 30.2, expectedEndTime: 31, expectEvent: false }); seekTest('will jump when seeking to the end of a range', { // [0-10], [20-30], [31-40] buffered: [{start: 0, end: 10}], newBuffered: [{start: 20, end: 30}, {start: 31, end: 40}], start: 3, seekTo: 30, expectedEndTime: 31, expectEvent: false }); seekTest('won\'t jump when past end', { // [0-10], [20-30] buffered: [{start: 0, end: 10}], newBuffered: [{start: 20, end: 30}], start: 3, seekTo: 34, expectedEndTime: 34, expectEvent: false }); seekTest('won\'t jump when seeking backwards into buffered range', { // [0-10], [20-30] buffered: [{start: 20, end: 30}], newBuffered: [{start: 0, end: 10}], start: 24, seekTo: 4, expectedEndTime: 4, expectEvent: false }); seekTest('will wait to jump when seeking backwards', { // [20-30] buffered: [{start: 20, end: 30}], // The lack of newBuffered means we won't append any segments, so we // should still be waiting. start: 24, seekTo: 4, expectedEndTime: 4, expectEvent: false }); seekTest('will jump when seeking backwards into gap', { // [2-10], [20-30] buffered: [{start: 20, end: 30}], newBuffered: [{start: 2, end: 10}], start: 24, seekTo: 1.6, expectedEndTime: 2, expectEvent: false }); }); // with small gaps describe('with large gaps', function() { seekTest('will jump large gap at beginning', { buffered: [{start: 20, end: 30}], newBuffered: [{start: 20, end: 30}], start: 25, seekTo: 0, jumpLargeGaps: true, expectedEndTime: 20, expectEvent: true }); seekTest('will raise event', { // [0-10], [20-30], [40-50] buffered: [{start: 0, end: 10}], newBuffered: [{start: 20, end: 30}, {start: 40, end: 50}], start: 3, seekTo: 32, expectedEndTime: 32, expectEvent: true }); seekTest('will jump large gaps', { // [0-10], [20-30], [40-50] buffered: [{start: 0, end: 10}], newBuffered: [{start: 20, end: 30}, {start: 40, end: 50}], start: 3, seekTo: 32, expectedEndTime: 40, jumpLargeGaps: true, expectEvent: true }); seekTest('will jump large gaps', { // [0-10], [20-30], [40-50] buffered: [{start: 0, end: 10}], newBuffered: [{start: 20, end: 30}, {start: 40, end: 50}], start: 3, seekTo: 32, expectedEndTime: 32, jumpLargeGaps: true, preventDefault: true, expectEvent: true }); }); // with large gaps }); // with unbuffered seeks /** * @param {string} name * @param {SeekTestInfo} data */ function seekTest(name, data) { it(name, function() { video.buffered = createFakeBuffered(data.buffered); video.currentTime = data.start; video.readyState = HTMLMediaElement.HAVE_ENOUGH_DATA; onEvent.and.callFake(function(event) { if (data.preventDefault) event.preventDefault(); }); config.jumpLargeGaps = !!data.jumpLargeGaps; playhead = new shaka.media.Playhead( video, manifest, config, data.start /* startTime */, Util.spyFunc(onSeek), Util.spyFunc(onEvent)); jasmine.clock().tick(1000); expect(onEvent).not.toHaveBeenCalled(); // Seek to the given position and update ready state. video.currentTime = data.seekTo; video.readyState = calculateReadyState(data.buffered, data.seekTo); video.seeking = true; video.on['seeking'](); if (video.readyState < HTMLMediaElement.HAVE_ENOUGH_DATA) video.on['waiting'](); else video.seeking = false; jasmine.clock().tick(1000); if (data.newBuffered) { // If we have a new buffer, first clear the buffer. video.buffered = createFakeBuffered([]); video.readyState = HTMLMediaElement.HAVE_METADATA; jasmine.clock().tick(1000); // Now StreamingEngine will buffer the new content and tell playhead // about it. expect(video.currentTime).toBe(data.seekTo); video.buffered = createFakeBuffered(data.newBuffered); video.readyState = calculateReadyState(data.newBuffered, data.seekTo); if (video.readyState >= HTMLMediaElement.HAVE_ENOUGH_DATA) video.seeking = false; playhead.onSegmentAppended(); jasmine.clock().tick(1000); } expect(onEvent).toHaveBeenCalledTimes(data.expectEvent ? 1 : 0); expect(video.currentTime).toBe(data.expectedEndTime); }); } /** * @param {!Array.<{start: number, end: number}>} b * @param {number} time * @return {number} */ function calculateReadyState(b, time) { // See: https://goo.gl/L8qxfD for (var i = 0; i < b.length; i++) { if (time >= b[i].start) { if (time == b[i].end) { // The video has the current frame, but no data in the future. return HTMLMediaElement.HAVE_CURRENT_DATA; } else if (time < b[i].end) { // The video has enough data to play forward. return HTMLMediaElement.HAVE_ENOUGH_DATA; } } } // The video doesn't have any video data. return HTMLMediaElement.HAVE_METADATA; } }); // gap jumping describe('rate changes', function() { beforeEach(function() { playhead = new shaka.media.Playhead( video, manifest, config, 0 /* startTime */, Util.spyFunc(onSeek), Util.spyFunc(onEvent)); }); it('notices video rate changes', function() { expect(playhead.getPlaybackRate()).toBe(1); video.playbackRate = 2; video.on['ratechange'](); expect(playhead.getPlaybackRate()).toBe(2); }); it('controls video rate with setPlaybackRate', function() { expect(playhead.getPlaybackRate()).toBe(1); playhead.setPlaybackRate(2); expect(playhead.getPlaybackRate()).toBe(2); expect(video.playbackRate).toBe(2); }); it('sets video rate to 0 when buffering', function() { expect(video.playbackRate).toBe(1); playhead.setBuffering(true); expect(video.playbackRate).toBe(0); }); it('remembers previous rate while buffering', function() { playhead.setPlaybackRate(5); expect(video.playbackRate).toBe(5); expect(playhead.getPlaybackRate()).toBe(5); playhead.setBuffering(true); expect(video.playbackRate).toBe(0); expect(playhead.getPlaybackRate()).toBe(5); playhead.setBuffering(false); expect(video.playbackRate).toBe(5); expect(playhead.getPlaybackRate()).toBe(5); }); it('ignores a rate change to 0', function() { // Regression test for https://github.com/google/shaka-player/issues/951 expect(video.playbackRate).toBe(1); expect(playhead.getPlaybackRate()).toBe(1); // With native controls on Edge, a rate change to 0 occurs when the user // seeks. This seems to happen before setBuffering(true). video.playbackRate = 0; video.on['ratechange'](); expect(playhead.getPlaybackRate()).toBe(1); playhead.setBuffering(true); expect(video.playbackRate).toBe(0); expect(playhead.getPlaybackRate()).toBe(1); playhead.setBuffering(false); expect(video.playbackRate).toBe(1); expect(playhead.getPlaybackRate()).toBe(1); }); }); // rate changes });