mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-20 16:57:25 +03:00
7e6a0f38ff
This corrects/normalizes license headers in misc. files, such as config files, docs, build tools, tests, and externs. This does not affect the compiled output, and is only done for consistency. Issue #2638 Change-Id: I9d8da2de55243b08d7df2b743aac73c6f15e858a
1300 lines
40 KiB
JavaScript
1300 lines
40 KiB
JavaScript
/*! @license
|
|
* Shaka Player
|
|
* Copyright 2016 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
|
|
/**
|
|
* @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.
|
|
*/
|
|
let 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.
|
|
*/
|
|
let PlayingTestInfo;
|
|
|
|
|
|
/**
|
|
* @typedef {{
|
|
* buffered: !Array.<TimeRange>,
|
|
* newBuffered: (!Array.<TimeRange>|undefined),
|
|
* start: number,
|
|
* seekTo: 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 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.
|
|
* @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.
|
|
*/
|
|
let SeekTestInfo;
|
|
|
|
|
|
describe('Playhead', () => {
|
|
const Util = shaka.test.Util;
|
|
|
|
/** @type {!shaka.test.FakeVideo} */
|
|
let video;
|
|
/** @type {!shaka.test.FakePresentationTimeline} */
|
|
let timeline;
|
|
/** @type {shaka.extern.Manifest} */
|
|
let manifest;
|
|
/** @type {!shaka.media.Playhead} */
|
|
let playhead;
|
|
/** @type {shaka.extern.StreamingConfiguration} */
|
|
let config;
|
|
|
|
// Callback to us from Playhead when a valid 'seeking' event occurs.
|
|
/** @type {!jasmine.Spy} */
|
|
let onSeek;
|
|
|
|
// Callback to us from Playhead when an event should be sent to the app.
|
|
/** @type {!jasmine.Spy} */
|
|
let onEvent;
|
|
|
|
beforeAll(() => {
|
|
jasmine.clock().install();
|
|
});
|
|
|
|
afterAll(() => {
|
|
jasmine.clock().uninstall();
|
|
});
|
|
|
|
beforeEach(() => {
|
|
video = new shaka.test.FakeVideo();
|
|
timeline = new shaka.test.FakePresentationTimeline();
|
|
|
|
onSeek = jasmine.createSpy('onSeek');
|
|
onEvent = jasmine.createSpy('onEvent');
|
|
|
|
timeline.isLive.and.returnValue(false);
|
|
timeline.getSeekRangeStart.and.returnValue(5);
|
|
timeline.getSeekRangeEnd.and.returnValue(60);
|
|
timeline.getDuration.and.returnValue(60);
|
|
|
|
// These tests should not cause these methods to be invoked.
|
|
timeline.getSegmentAvailabilityStart.and.throwError(new Error());
|
|
timeline.getSegmentAvailabilityEnd.and.throwError(new Error());
|
|
timeline.setDuration.and.throwError(new Error());
|
|
|
|
manifest = {
|
|
variants: [],
|
|
textStreams: [],
|
|
presentationTimeline: timeline,
|
|
minBufferTime: 10,
|
|
offlineSessionIds: [],
|
|
};
|
|
|
|
config = shaka.util.PlayerConfiguration.createDefault().streaming;
|
|
});
|
|
|
|
afterEach(() => {
|
|
playhead.release();
|
|
});
|
|
|
|
function setMockDate(seconds) {
|
|
const minutes = Math.floor(seconds / 60);
|
|
seconds %= 60;
|
|
const mockDate = new Date(2013, 9, 23, 7, minutes, seconds);
|
|
jasmine.clock().mockDate(mockDate);
|
|
}
|
|
|
|
describe('getTime', () => {
|
|
it('returns current time when the video is paused', () => {
|
|
video.readyState = HTMLMediaElement.HAVE_METADATA;
|
|
playhead = new shaka.media.MediaSourcePlayhead(
|
|
video,
|
|
manifest,
|
|
config,
|
|
/* startTime= */ 5,
|
|
Util.spyFunc(onSeek),
|
|
Util.spyFunc(onEvent));
|
|
|
|
expect(video.currentTime).toBe(5);
|
|
expect(playhead.getTime()).toBe(5);
|
|
|
|
// Simulate pausing.
|
|
video.paused = true;
|
|
timeline.getSeekRangeStart.and.returnValue(10);
|
|
timeline.getSeekRangeEnd.and.returnValue(70);
|
|
|
|
expect(video.currentTime).toBe(5);
|
|
expect(playhead.getTime()).toBe(5);
|
|
});
|
|
|
|
it('returns the correct time when readyState starts at 0', () => {
|
|
playhead = new shaka.media.MediaSourcePlayhead(
|
|
video,
|
|
manifest,
|
|
config,
|
|
/* startTime= */ 5,
|
|
Util.spyFunc(onSeek),
|
|
Util.spyFunc(onEvent));
|
|
|
|
expect(video.addEventListener).toHaveBeenCalledWith(
|
|
'loadedmetadata', jasmine.any(Function), jasmine.anything());
|
|
expect(video.addEventListener).not.toHaveBeenCalledWith(
|
|
'seeking', jasmine.any(Function), jasmine.anything());
|
|
|
|
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), jasmine.anything());
|
|
video.on['seeking']();
|
|
|
|
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', () => {
|
|
video.readyState = HTMLMediaElement.HAVE_METADATA;
|
|
|
|
playhead = new shaka.media.MediaSourcePlayhead(
|
|
video,
|
|
manifest,
|
|
config,
|
|
/* startTime= */ 5,
|
|
Util.spyFunc(onSeek),
|
|
Util.spyFunc(onEvent));
|
|
|
|
video.on['seeking']();
|
|
expect(playhead.getTime()).toBe(5);
|
|
expect(video.currentTime).toBe(5);
|
|
|
|
video.currentTime = 6;
|
|
expect(playhead.getTime()).toBe(6);
|
|
});
|
|
|
|
it('allows using startTime of 0', () => {
|
|
video.readyState = HTMLMediaElement.HAVE_METADATA;
|
|
timeline.isLive.and.returnValue(true);
|
|
timeline.getDuration.and.returnValue(Infinity);
|
|
timeline.getSeekRangeStart.and.returnValue(0);
|
|
timeline.getSeekRangeEnd.and.returnValue(60);
|
|
|
|
playhead = new shaka.media.MediaSourcePlayhead(
|
|
video, manifest, config, /* startTime= */ 0, Util.spyFunc(onSeek),
|
|
Util.spyFunc(onEvent));
|
|
|
|
expect(playhead.getTime()).toBe(0);
|
|
});
|
|
|
|
it('bumps startTime back from duration', () => {
|
|
video.readyState = HTMLMediaElement.HAVE_METADATA;
|
|
timeline.isLive.and.returnValue(false);
|
|
timeline.getSeekRangeStart.and.returnValue(0);
|
|
timeline.getSeekRangeEnd.and.returnValue(60);
|
|
timeline.getDuration.and.returnValue(60);
|
|
|
|
playhead = new shaka.media.MediaSourcePlayhead(
|
|
video, manifest, config, /* startTime= */ 60, Util.spyFunc(onSeek),
|
|
Util.spyFunc(onEvent));
|
|
|
|
expect(playhead.getTime()).toBe(59); // duration - durationBackoff
|
|
expect(video.currentTime).toBe(59); // duration - durationBackoff
|
|
});
|
|
|
|
it('playback from a certain offset from live edge for live', () => {
|
|
video.readyState = HTMLMediaElement.HAVE_METADATA;
|
|
timeline.isLive.and.returnValue(true);
|
|
timeline.getDuration.and.returnValue(Infinity);
|
|
timeline.getSeekRangeStart.and.returnValue(0);
|
|
timeline.getSeekRangeEnd.and.returnValue(60);
|
|
|
|
playhead = new shaka.media.MediaSourcePlayhead(
|
|
video, manifest, config, /* startTime= */ -15, Util.spyFunc(onSeek),
|
|
Util.spyFunc(onEvent));
|
|
|
|
expect(playhead.getTime()).toBe(45);
|
|
});
|
|
|
|
it('playback from segment seek range start time', () => {
|
|
video.readyState = HTMLMediaElement.HAVE_METADATA;
|
|
timeline.isLive.and.returnValue(true);
|
|
timeline.getDuration.and.returnValue(Infinity);
|
|
timeline.getSeekRangeStart.and.returnValue(30);
|
|
timeline.getSeekRangeEnd.and.returnValue(60);
|
|
// If the live stream's playback offset time is not available, start
|
|
// playing from the seek range start time.
|
|
playhead = new shaka.media.MediaSourcePlayhead(
|
|
video, manifest, config, /* startTime= */ -40, Util.spyFunc(onSeek),
|
|
Util.spyFunc(onEvent));
|
|
|
|
expect(playhead.getTime()).toBe(30);
|
|
});
|
|
|
|
it('does not change currentTime if it\'s not 0', () => {
|
|
playhead = new shaka.media.MediaSourcePlayhead(
|
|
video,
|
|
manifest,
|
|
config,
|
|
/* startTime= */ 5,
|
|
Util.spyFunc(onSeek),
|
|
Util.spyFunc(onEvent));
|
|
|
|
expect(video.addEventListener).toHaveBeenCalledWith(
|
|
'loadedmetadata', jasmine.any(Function), jasmine.anything());
|
|
|
|
expect(playhead.getTime()).toBe(5);
|
|
expect(video.currentTime).toBe(0);
|
|
|
|
video.currentTime = 8;
|
|
video.readyState = HTMLMediaElement.HAVE_METADATA;
|
|
video.on['loadedmetadata']();
|
|
|
|
// Delay to let Playhead batch up changes to currentTime and observe.
|
|
jasmine.clock().tick(1000);
|
|
expect(video.currentTime).toBe(8);
|
|
});
|
|
|
|
// 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', () => {
|
|
timeline.isLive.and.returnValue(true);
|
|
timeline.getDuration.and.returnValue(Infinity);
|
|
timeline.getSeekRangeStart.and.returnValue(0);
|
|
timeline.getSeekRangeEnd.and.returnValue(60);
|
|
timeline.getSeekRangeEnd.and.returnValue(60);
|
|
|
|
playhead = new shaka.media.MediaSourcePlayhead(
|
|
video,
|
|
manifest,
|
|
config,
|
|
/* startTime= */ null,
|
|
Util.spyFunc(onSeek),
|
|
Util.spyFunc(onEvent));
|
|
|
|
expect(video.addEventListener).toHaveBeenCalledWith(
|
|
'loadedmetadata', jasmine.any(Function), jasmine.anything());
|
|
|
|
expect(playhead.getTime()).toBe(60);
|
|
expect(video.currentTime).toBe(0);
|
|
|
|
// Simulate time passing and the live edge changing.
|
|
timeline.getSeekRangeStart.and.returnValue(10);
|
|
timeline.getSeekRangeEnd.and.returnValue(70);
|
|
timeline.getSeekRangeEnd.and.returnValue(70);
|
|
|
|
expect(playhead.getTime()).toBe(60);
|
|
});
|
|
}); // getTime
|
|
|
|
it('clamps playhead after seeking for live', () => {
|
|
video.readyState = HTMLMediaElement.HAVE_METADATA;
|
|
|
|
video.buffered = createFakeBuffered([{start: 25, end: 55}]);
|
|
|
|
timeline.isLive.and.returnValue(true);
|
|
timeline.getDuration.and.returnValue(Infinity);
|
|
timeline.getSeekRangeStart.and.returnValue(5);
|
|
timeline.getSeekRangeEnd.and.returnValue(60);
|
|
|
|
playhead = new shaka.media.MediaSourcePlayhead(
|
|
video,
|
|
manifest,
|
|
config,
|
|
/* startTime= */ 5,
|
|
Util.spyFunc(onSeek),
|
|
Util.spyFunc(onEvent));
|
|
|
|
// This has to periodically increment the mock date to allow the onSeeking_
|
|
// handler to seek, if appropriate.
|
|
|
|
// 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'](),
|
|
|
|
setMockDate(0);
|
|
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.
|
|
setMockDate(10);
|
|
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.
|
|
setMockDate(20);
|
|
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.
|
|
setMockDate(30);
|
|
video.currentTime = 1;
|
|
video.on['seeking']();
|
|
expect(video.currentTime).toBe(20);
|
|
expect(playhead.getTime()).toBe(20);
|
|
expect(onSeek).not.toHaveBeenCalled();
|
|
setMockDate(40);
|
|
video.on['seeking']();
|
|
expect(onSeek).toHaveBeenCalled();
|
|
|
|
onSeek.calls.reset();
|
|
|
|
video.buffered = createFakeBuffered([{start: 10, end: 40}]);
|
|
|
|
// Seek outside safe region & in buffered region.
|
|
setMockDate(50);
|
|
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.
|
|
setMockDate(60);
|
|
video.currentTime = 9;
|
|
video.on['seeking']();
|
|
expect(video.currentTime).toBe(20);
|
|
expect(playhead.getTime()).toBe(20);
|
|
expect(onSeek).not.toHaveBeenCalled();
|
|
setMockDate(70);
|
|
video.on['seeking']();
|
|
expect(onSeek).toHaveBeenCalled();
|
|
|
|
onSeek.calls.reset();
|
|
|
|
// Seek past end.
|
|
setMockDate(80);
|
|
video.currentTime = 120;
|
|
video.on['seeking']();
|
|
expect(video.currentTime).toBe(60);
|
|
expect(playhead.getTime()).toBe(60);
|
|
expect(onSeek).not.toHaveBeenCalled();
|
|
setMockDate(90);
|
|
video.on['seeking']();
|
|
expect(onSeek).toHaveBeenCalled();
|
|
|
|
onSeek.calls.reset();
|
|
|
|
// Seek before start, start is buffered.
|
|
setMockDate(100);
|
|
video.currentTime = 1;
|
|
video.on['seeking']();
|
|
expect(video.currentTime).toBe(10);
|
|
expect(playhead.getTime()).toBe(10);
|
|
expect(onSeek).not.toHaveBeenCalled();
|
|
setMockDate(110);
|
|
video.on['seeking']();
|
|
expect(onSeek).toHaveBeenCalled();
|
|
|
|
onSeek.calls.reset();
|
|
|
|
// Seek with safe == end
|
|
timeline.getSeekRangeEnd.and.returnValue(12);
|
|
timeline.getSafeSeekRangeStart.and.returnValue(12);
|
|
|
|
// Seek before start
|
|
setMockDate(120);
|
|
video.currentTime = 4;
|
|
video.on['seeking']();
|
|
expect(video.currentTime).toBe(12);
|
|
expect(playhead.getTime()).toBe(12);
|
|
expect(onSeek).not.toHaveBeenCalled();
|
|
setMockDate(130);
|
|
video.on['seeking']();
|
|
expect(onSeek).toHaveBeenCalled();
|
|
|
|
onSeek.calls.reset();
|
|
|
|
// Seek in window.
|
|
setMockDate(140);
|
|
video.currentTime = 8;
|
|
video.on['seeking']();
|
|
expect(video.currentTime).toBe(12);
|
|
expect(playhead.getTime()).toBe(12);
|
|
expect(onSeek).not.toHaveBeenCalled();
|
|
setMockDate(150);
|
|
video.on['seeking']();
|
|
expect(onSeek).toHaveBeenCalled();
|
|
|
|
onSeek.calls.reset();
|
|
|
|
// Seek past end.
|
|
setMockDate(160);
|
|
video.currentTime = 13;
|
|
video.on['seeking']();
|
|
expect(video.currentTime).toBe(12);
|
|
expect(playhead.getTime()).toBe(12);
|
|
expect(onSeek).not.toHaveBeenCalled();
|
|
setMockDate(170);
|
|
video.on['seeking']();
|
|
expect(onSeek).toHaveBeenCalled();
|
|
}); // clamps playhead after seeking for live
|
|
|
|
it('clamps playhead after seeking for VOD', () => {
|
|
video.readyState = HTMLMediaElement.HAVE_METADATA;
|
|
|
|
video.buffered = createFakeBuffered([{start: 25, end: 55}]);
|
|
|
|
timeline.isLive.and.returnValue(false);
|
|
timeline.getSeekRangeStart.and.returnValue(5);
|
|
timeline.getSafeSeekRangeStart.and.returnValue(5);
|
|
timeline.getSeekRangeEnd.and.returnValue(60);
|
|
timeline.getDuration.and.returnValue(60);
|
|
|
|
playhead = new shaka.media.MediaSourcePlayhead(
|
|
video,
|
|
manifest,
|
|
config,
|
|
/* startTime= */ 5,
|
|
Util.spyFunc(onSeek),
|
|
Util.spyFunc(onEvent));
|
|
|
|
setMockDate(0);
|
|
video.on['seeking']();
|
|
expect(video.currentTime).toBe(5);
|
|
expect(playhead.getTime()).toBe(5);
|
|
|
|
// Seek past end.
|
|
setMockDate(10);
|
|
video.currentTime = 120;
|
|
video.on['seeking']();
|
|
expect(video.currentTime).toBe(59); // duration - durationBackoff
|
|
expect(playhead.getTime()).toBe(59); // duration - durationBackoff
|
|
expect(onSeek).not.toHaveBeenCalled();
|
|
setMockDate(20);
|
|
video.on['seeking']();
|
|
expect(onSeek).toHaveBeenCalled();
|
|
|
|
onSeek.calls.reset();
|
|
|
|
// Seek before start.
|
|
setMockDate(30);
|
|
video.currentTime = 1;
|
|
video.on['seeking']();
|
|
expect(video.currentTime).toBe(5);
|
|
expect(playhead.getTime()).toBe(5);
|
|
expect(onSeek).not.toHaveBeenCalled();
|
|
setMockDate(40);
|
|
video.on['seeking']();
|
|
expect(onSeek).toHaveBeenCalled();
|
|
}); // clamps playhead after seeking for VOD
|
|
|
|
it('doesn\'t repeatedly re-seek', () => {
|
|
video.readyState = HTMLMediaElement.HAVE_METADATA;
|
|
|
|
video.buffered = createFakeBuffered([{start: 25, end: 55}]);
|
|
|
|
timeline.isLive.and.returnValue(false);
|
|
timeline.getSeekRangeStart.and.returnValue(5);
|
|
timeline.getSafeSeekRangeStart.and.returnValue(5);
|
|
timeline.getSeekRangeEnd.and.returnValue(60);
|
|
timeline.getDuration.and.returnValue(60);
|
|
|
|
playhead = new shaka.media.MediaSourcePlayhead(
|
|
video,
|
|
manifest,
|
|
config,
|
|
/* startTime= */ 5,
|
|
Util.spyFunc(onSeek),
|
|
Util.spyFunc(onEvent));
|
|
|
|
// First, seek to start time.
|
|
video.currentTime = 0;
|
|
video.on['seeking']();
|
|
|
|
// The first time, it should re-seek without issue.
|
|
setMockDate(0);
|
|
video.currentTime = 0;
|
|
video.on['seeking']();
|
|
expect(video.currentTime).toBe(5);
|
|
expect(playhead.getTime()).toBe(5);
|
|
expect(onSeek).not.toHaveBeenCalled();
|
|
|
|
onSeek.calls.reset();
|
|
|
|
// No time passes, so it shouldn't be willing to re-seek.
|
|
video.currentTime = 0;
|
|
video.on['seeking']();
|
|
expect(video.currentTime).toBe(0);
|
|
// The playhead wanted to seek ahead to 5, but was unable to.
|
|
expect(playhead.getTime()).toBe(5);
|
|
expect(onSeek).toHaveBeenCalled();
|
|
|
|
onSeek.calls.reset();
|
|
|
|
// Wait 10 seconds, so it should be willing to re-seek.
|
|
setMockDate(10);
|
|
video.currentTime = 0;
|
|
video.on['seeking']();
|
|
expect(video.currentTime).toBe(5);
|
|
expect(playhead.getTime()).toBe(5);
|
|
expect(onSeek).not.toHaveBeenCalled();
|
|
}); // doesn't repeatedly re-seek
|
|
|
|
it('handles live manifests with no seek range', () => {
|
|
video.buffered = createFakeBuffered([{start: 1000, end: 1030}]);
|
|
video.readyState = HTMLMediaElement.HAVE_METADATA;
|
|
|
|
timeline.isLive.and.returnValue(true);
|
|
timeline.getDuration.and.returnValue(Infinity);
|
|
timeline.getSeekRangeStart.and.returnValue(1000);
|
|
timeline.getSeekRangeEnd.and.returnValue(1000);
|
|
|
|
let currentTime = 0;
|
|
let seekCount = 0;
|
|
Object.defineProperty(video, 'currentTime', {
|
|
get: () => currentTime,
|
|
set: (val) => {
|
|
currentTime = val;
|
|
seekCount++;
|
|
setTimeout(video.on['seeking'], 5);
|
|
},
|
|
});
|
|
|
|
playhead = new shaka.media.MediaSourcePlayhead(
|
|
video,
|
|
manifest,
|
|
config,
|
|
/* startTime= */ 5,
|
|
Util.spyFunc(onSeek),
|
|
Util.spyFunc(onEvent));
|
|
expect(currentTime).toBe(1000);
|
|
seekCount = 0;
|
|
|
|
// The availability window slips ahead.
|
|
timeline.getSeekRangeStart.and.returnValue(1030);
|
|
timeline.getSeekRangeEnd.and.returnValue(1030);
|
|
video.on['waiting']();
|
|
jasmine.clock().tick(500);
|
|
expect(currentTime).toBe(1030);
|
|
expect(seekCount).toBe(1);
|
|
|
|
// It should allow a small buffer around the seek range.
|
|
seekCount = 0;
|
|
currentTime = 1030.062441;
|
|
jasmine.clock().tick(500);
|
|
currentTime = 1027.9233;
|
|
jasmine.clock().tick(500);
|
|
expect(seekCount).toBe(0);
|
|
|
|
// Got too far away.
|
|
currentTime = 1026;
|
|
jasmine.clock().tick(500);
|
|
expect(currentTime).toBe(1030);
|
|
expect(seekCount).toBe(1);
|
|
}); // handles live manifests with no seek range
|
|
|
|
describe('clamps playhead after resuming', () => {
|
|
beforeEach(() => {
|
|
video.readyState = HTMLMediaElement.HAVE_METADATA;
|
|
|
|
video.buffered = createFakeBuffered([{start: 5, end: 35}]);
|
|
});
|
|
|
|
it('(live case)', () => {
|
|
timeline.isLive.and.returnValue(true);
|
|
timeline.getDuration.and.returnValue(Infinity);
|
|
timeline.getSeekRangeStart.and.returnValue(5);
|
|
timeline.getSeekRangeEnd.and.returnValue(60);
|
|
|
|
playhead = new shaka.media.MediaSourcePlayhead(
|
|
video,
|
|
manifest,
|
|
config,
|
|
/* startTime= */ 5,
|
|
Util.spyFunc(onSeek),
|
|
Util.spyFunc(onEvent));
|
|
|
|
video.on['seeking']();
|
|
expect(video.currentTime).toBe(5);
|
|
expect(playhead.getTime()).toBe(5);
|
|
|
|
// Simulate pausing.
|
|
timeline.getSeekRangeStart.and.returnValue(10);
|
|
timeline.getSeekRangeEnd.and.returnValue(70);
|
|
|
|
// Because this is buffered, the playhead should move to (start + 5),
|
|
// which will cause a 'seeking' event.
|
|
jasmine.clock().tick(500);
|
|
expect(video.currentTime).toBe(15);
|
|
video.on['seeking']();
|
|
expect(playhead.getTime()).toBe(15);
|
|
expect(onSeek).toHaveBeenCalled();
|
|
});
|
|
|
|
it('(VOD case)', () => {
|
|
timeline.isLive.and.returnValue(false);
|
|
timeline.getSeekRangeStart.and.returnValue(5);
|
|
timeline.getSafeSeekRangeStart.and.returnValue(5);
|
|
timeline.getSeekRangeEnd.and.returnValue(60);
|
|
timeline.getDuration.and.returnValue(60);
|
|
|
|
playhead = new shaka.media.MediaSourcePlayhead(
|
|
video,
|
|
manifest,
|
|
config,
|
|
/* startTime= */ 5,
|
|
Util.spyFunc(onSeek),
|
|
Util.spyFunc(onEvent));
|
|
|
|
video.on['seeking']();
|
|
expect(video.currentTime).toBe(5);
|
|
expect(playhead.getTime()).toBe(5);
|
|
|
|
// Simulate pausing.
|
|
timeline.getSeekRangeStart.and.returnValue(10);
|
|
timeline.getSafeSeekRangeStart.and.returnValue(10);
|
|
timeline.getSeekRangeEnd.and.returnValue(70);
|
|
|
|
jasmine.clock().tick(500);
|
|
expect(video.currentTime).toBe(10);
|
|
video.on['seeking']();
|
|
expect(playhead.getTime()).toBe(10);
|
|
expect(onSeek).toHaveBeenCalled();
|
|
});
|
|
}); // clamps playhead after resuming
|
|
|
|
it('clamps playhead even before seeking completes', () => {
|
|
video.readyState = HTMLMediaElement.HAVE_METADATA;
|
|
|
|
video.buffered = createFakeBuffered([{start: 25, end: 55}]);
|
|
|
|
timeline.isLive.and.returnValue(true);
|
|
timeline.getDuration.and.returnValue(Infinity);
|
|
timeline.getSeekRangeStart.and.returnValue(5);
|
|
timeline.getSeekRangeEnd.and.returnValue(60);
|
|
|
|
playhead = new shaka.media.MediaSourcePlayhead(
|
|
video,
|
|
manifest,
|
|
config,
|
|
/* startTime= */ 30,
|
|
Util.spyFunc(onSeek),
|
|
Util.spyFunc(onEvent));
|
|
|
|
video.currentTime = 0;
|
|
video.seeking = true;
|
|
// "video.seeking" stays true until the buffered range intersects with
|
|
// "video.currentTime". Playhead should correct anyway.
|
|
video.on['seeking']();
|
|
|
|
// Let the Playhead poll the seek range and push us back inside it.
|
|
jasmine.clock().tick(1000);
|
|
|
|
// There is some "safety zone" at the front of the range, so we may seek to
|
|
// anything >= the seek range start (5).
|
|
expect(video.currentTime).not.toBeLessThan(5);
|
|
expect(playhead.getTime()).not.toBeLessThan(5);
|
|
}); // clamps playhead even before seeking completes
|
|
|
|
describe('gap jumping', () => {
|
|
beforeEach(() => {
|
|
timeline.isLive.and.returnValue(false);
|
|
timeline.getSafeSeekRangeStart.and.returnValue(0);
|
|
timeline.getSeekRangeStart.and.returnValue(0);
|
|
timeline.getSeekRangeEnd.and.returnValue(60);
|
|
timeline.getDuration.and.returnValue(60);
|
|
|
|
config.smallGapLimit = 1;
|
|
});
|
|
|
|
describe('when playing', () => {
|
|
describe('with small gaps', () => {
|
|
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', () => {
|
|
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, () => {
|
|
video.buffered = createFakeBuffered(data.buffered);
|
|
video.currentTime = data.start;
|
|
video.readyState = HTMLMediaElement.HAVE_ENOUGH_DATA;
|
|
|
|
onEvent.and.callFake((event) => {
|
|
if (data.preventDefault) {
|
|
event.preventDefault();
|
|
}
|
|
});
|
|
|
|
config.jumpLargeGaps = !!data.jumpLargeGaps;
|
|
playhead = new shaka.media.MediaSourcePlayhead(
|
|
video,
|
|
manifest,
|
|
config,
|
|
/* startTime= */ data.start,
|
|
Util.spyFunc(onSeek),
|
|
Util.spyFunc(onEvent));
|
|
|
|
jasmine.clock().tick(500);
|
|
for (let time = data.start; time < data.waitingAt; time++) {
|
|
// We don't want to run tick() for 1 second because it will trigger
|
|
// the stall-detection, which will move the playhead; on the other
|
|
// hand, we don't want to be 0.5 seconds from the gap because on
|
|
// IE/Edge/Tizen, gap jumping will treat that as in the gap.
|
|
// See shaka.media.TimeRangesUtils.getGapIndex.
|
|
|
|
video.currentTime = time;
|
|
jasmine.clock().tick(400);
|
|
// Make sure Playhead didn't adjust the time yet.
|
|
expect(video.currentTime).toBe(time);
|
|
|
|
video.currentTime = time + 0.4;
|
|
jasmine.clock().tick(600);
|
|
// Make sure Playhead didn't adjust the time yet.
|
|
expect(video.currentTime).toBe(time + 0.4);
|
|
}
|
|
|
|
expect(onEvent).not.toHaveBeenCalled();
|
|
|
|
video.currentTime = data.waitingAt;
|
|
video.readyState = HTMLMediaElement.HAVE_CURRENT_DATA;
|
|
video.on['waiting']();
|
|
jasmine.clock().tick(500);
|
|
|
|
expect(onEvent).toHaveBeenCalledTimes(data.expectEvent ? 1 : 0);
|
|
expect(video.currentTime).toBe(data.expectedEndTime);
|
|
});
|
|
}
|
|
}); // when playing
|
|
|
|
describe('with buffered seeks', () => {
|
|
describe('with small gaps', () => {
|
|
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', () => {
|
|
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', () => {
|
|
describe('with small gaps', () => {
|
|
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', () => {
|
|
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
|
|
|
|
it('doesn\'t gap jump if the seeking event is late', () => {
|
|
const buffered = [{start: 10, end: 20}];
|
|
video.buffered = createFakeBuffered(buffered);
|
|
video.currentTime = 12;
|
|
video.readyState = HTMLMediaElement.HAVE_ENOUGH_DATA;
|
|
|
|
config.jumpLargeGaps = true;
|
|
playhead = new shaka.media.MediaSourcePlayhead(
|
|
video,
|
|
manifest,
|
|
config,
|
|
/* startTime= */ 12,
|
|
Util.spyFunc(onSeek),
|
|
Util.spyFunc(onEvent));
|
|
|
|
jasmine.clock().tick(500);
|
|
expect(onEvent).not.toHaveBeenCalled();
|
|
|
|
// Append a segment before seeking.
|
|
playhead.notifyOfBufferingChange();
|
|
|
|
// Seek backwards but wait briefly to fire the seeking event.
|
|
video.currentTime = 3;
|
|
video.readyState = HTMLMediaElement.HAVE_METADATA;
|
|
video.seeking = true;
|
|
jasmine.clock().tick(500);
|
|
video.on['seeking']();
|
|
|
|
// There should NOT have been a gap jump.
|
|
expect(video.currentTime).toBe(3);
|
|
});
|
|
|
|
it('works with rounding errors when seeking', () => {
|
|
// If the browser sets the time to slightly before where we seek to, we
|
|
// shouldn't get stuck in an infinite loop trying to jump the tiny gap.
|
|
// https://github.com/google/shaka-player/issues/1309
|
|
const buffered = [{start: 10, end: 20}];
|
|
video.buffered = createFakeBuffered(buffered);
|
|
video.readyState = HTMLMediaElement.HAVE_METADATA;
|
|
|
|
// Track the number of times we seeked.
|
|
let seekCount = 0;
|
|
let currentTime = 0;
|
|
Object.defineProperty(video, 'currentTime', {
|
|
get: () => currentTime - 0.00001,
|
|
set: (time) => {
|
|
seekCount++;
|
|
currentTime = time;
|
|
setTimeout(() => {
|
|
video.on['seeking']();
|
|
playhead.notifyOfBufferingChange();
|
|
}, 5);
|
|
},
|
|
});
|
|
|
|
config.jumpLargeGaps = true;
|
|
playhead = new shaka.media.MediaSourcePlayhead(
|
|
video,
|
|
manifest,
|
|
config,
|
|
/* startTime= */ 0,
|
|
Util.spyFunc(onSeek),
|
|
Util.spyFunc(onEvent));
|
|
|
|
playhead.notifyOfBufferingChange();
|
|
jasmine.clock().tick(500);
|
|
|
|
expect(seekCount).toBe(1);
|
|
expect(currentTime).toBe(10);
|
|
});
|
|
|
|
/**
|
|
* @param {string} name
|
|
* @param {SeekTestInfo} data
|
|
*/
|
|
function seekTest(name, data) {
|
|
it(name, () => {
|
|
video.buffered = createFakeBuffered(data.buffered);
|
|
video.currentTime = data.start;
|
|
video.readyState = HTMLMediaElement.HAVE_ENOUGH_DATA;
|
|
|
|
onEvent.and.callFake((event) => {
|
|
if (data.preventDefault) {
|
|
event.preventDefault();
|
|
}
|
|
});
|
|
|
|
config.jumpLargeGaps = !!data.jumpLargeGaps;
|
|
playhead = new shaka.media.MediaSourcePlayhead(
|
|
video,
|
|
manifest,
|
|
config,
|
|
/* startTime= */ data.start,
|
|
Util.spyFunc(onSeek),
|
|
Util.spyFunc(onEvent));
|
|
|
|
jasmine.clock().tick(500);
|
|
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(500);
|
|
|
|
if (data.newBuffered) {
|
|
// If we have a new buffer, first clear the buffer.
|
|
video.buffered = createFakeBuffered([]);
|
|
video.readyState = HTMLMediaElement.HAVE_METADATA;
|
|
jasmine.clock().tick(250);
|
|
|
|
// 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.notifyOfBufferingChange();
|
|
jasmine.clock().tick(250);
|
|
}
|
|
|
|
expect(onEvent).toHaveBeenCalledTimes(data.expectEvent ? 1 : 0);
|
|
expect(video.currentTime).toBe(data.expectedEndTime);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param {!Array.<{start: number, end: number}>} buffers
|
|
* @param {number} time
|
|
* @return {number}
|
|
*/
|
|
function calculateReadyState(buffers, time) {
|
|
// See: https://bit.ly/2JYh8WX
|
|
for (const buffer of buffers) {
|
|
if (time >= buffer.start) {
|
|
if (time == buffer.end) {
|
|
// The video has the current frame, but no data in the future.
|
|
return HTMLMediaElement.HAVE_CURRENT_DATA;
|
|
} else if (time < buffer.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
|
|
});
|