Skip to content

Commit

Permalink
Catching up with the edge in live streams
Browse files Browse the repository at this point in the history
  • Loading branch information
aescarcha committed Jun 7, 2018
1 parent f9386c5 commit 7c9cba3
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 2 deletions.
2 changes: 2 additions & 0 deletions index.d.ts
Expand Up @@ -269,6 +269,8 @@ declare namespace dashjs {
METRIC_UPDATED: 'metricUpdated';
PERIOD_SWITCH_COMPLETED: 'periodSwitchCompleted';
PERIOD_SWITCH_STARTED: 'periodSwitchStarted';
PLAYBACK_CATCHUP_END: 'playbackCatchupEnd';
PLAYBACK_CATCHUP_START: 'playbackCatchupStart';
PLAYBACK_ENDED: 'playbackEnded';
PLAYBACK_ERROR: 'playbackError';
PLAYBACK_METADATA_LOADED: 'playbackMetaDataLoaded';
Expand Down
26 changes: 26 additions & 0 deletions src/streaming/MediaPlayer.js
Expand Up @@ -82,6 +82,7 @@ function MediaPlayer() {
const SOURCE_NOT_ATTACHED_ERROR = 'You must first call attachSource() with a valid source before calling this method';
const MEDIA_PLAYER_NOT_INITIALIZED_ERROR = 'MediaPlayer not initialized!';
const MEDIA_PLAYER_BAD_ARGUMENT_ERROR = 'MediaPlayer Invalid Arguments!';
const PLAYBACK_CATCHUP_RATE_BAD_ARGUMENT_ERROR = 'Playback catchup rate invalid argument! Use a number from 1 to 1.2';

const context = this.context;
const eventBus = EventBus(context).getInstance();
Expand Down Expand Up @@ -482,6 +483,29 @@ function MediaPlayer() {
return getVideoElement().playbackRate;
}

/**
* Use this method to set the playback rate that will be used when catching up with live stream. Set 1 to disable the feature.
* @param {number} value
* @memberof module:MediaPlayer
* @instance
*/
function setCatchUpPlaybackRate(value) {
if (isNaN(value) || value < 1 || value > 1.20) {
throw PLAYBACK_CATCHUP_RATE_BAD_ARGUMENT_ERROR;
}
playbackController.setCatchUpPlaybackRate(value);
}

/**
* Returns the current catchup playback rate.
* @returns {number}
* @memberof module:MediaPlayer
* @instance
*/
function getCatchUpPlaybackRate() {
return playbackController.getCatchUpPlaybackRate();
}

/**
* Use this method to set the native Video Element's muted state. Takes a Boolean that determines whether audio is muted. true if the audio is muted and false otherwise.
* @param {boolean} value
Expand Down Expand Up @@ -2831,6 +2855,8 @@ function MediaPlayer() {
seek: seek,
setPlaybackRate: setPlaybackRate,
getPlaybackRate: getPlaybackRate,
setCatchUpPlaybackRate: setCatchUpPlaybackRate,
getCatchUpPlaybackRate: getCatchUpPlaybackRate,
setMute: setMute,
isMuted: isMuted,
setVolume: setVolume,
Expand Down
13 changes: 13 additions & 0 deletions src/streaming/MediaPlayerEvents.js
Expand Up @@ -214,6 +214,19 @@ class MediaPlayerEvents extends EventsBase {
*/
this.CAN_PLAY = 'canPlay';


/**
* Sent live catch up stops and playback rate goes back to normal
* @event MediaPlayerEvents#PLAYBACK_CATCHUP_END
*/
this.PLAYBACK_CATCHUP_END = 'playbackCatchupEnd';

/**
* Sent when playing live and buffer is too long, the player starts catching up by accelerating.
* @event MediaPlayerEvents#PLAYBACK_CATCHUP_START
*/
this.PLAYBACK_CATCHUP_START = 'playbackCatchupStart';

/**
* Sent when playback completes.
* @event MediaPlayerEvents#PLAYBACK_ENDED
Expand Down
64 changes: 64 additions & 0 deletions src/streaming/controllers/PlaybackController.js
Expand Up @@ -37,6 +37,8 @@ import FactoryMaker from '../../core/FactoryMaker';
import Debug from '../../core/Debug';

const LIVE_UPDATE_PLAYBACK_TIME_INTERVAL_MS = 500;
const DEFAULT_CATCHUP_PLAYBACK_RATE = 1.05;
const LIVE_CATCHUP_THRESHOLD = 0.5;

function PlaybackController() {

Expand All @@ -63,8 +65,12 @@ function PlaybackController() {
mediaPlayerModel,
playOnceInitialized,
lastLivePlaybackTime,
originalPlaybackRate,
catchingUp,
availabilityStartTime;

let catchUpPlaybackRate = DEFAULT_CATCHUP_PLAYBACK_RATE;

function setup() {
logger = Debug(context).getInstance().getLogger(instance);
reset();
Expand All @@ -79,6 +85,10 @@ function PlaybackController() {
eventBus.on(Events.BYTES_APPENDED_END_FRAGMENT, onBytesAppended, this);
eventBus.on(Events.BUFFER_LEVEL_STATE_CHANGED, onBufferLevelStateChanged, this);
eventBus.on(Events.PERIOD_SWITCH_STARTED, onPeriodSwitchStarted, this);
eventBus.on(Events.PLAYBACK_CATCHUP_END, onPlaybackCatchUpEnd, this);
eventBus.on(Events.PLAYBACK_CATCHUP_START, onPlaybackCatchUpStart, this);
eventBus.on(Events.PLAYBACK_PROGRESS, onPlaybackProgression, this);
eventBus.on(Events.PLAYBACK_TIME_UPDATED, onPlaybackProgression, this);

if (playOnceInitialized) {
playOnceInitialized = false;
Expand Down Expand Up @@ -161,6 +171,14 @@ function PlaybackController() {
return liveStartTime;
}

function setCatchUpPlaybackRate(value) {
catchUpPlaybackRate = value;
}

function getCatchUpPlaybackRate() {
return catchUpPlaybackRate;
}

/**
* Computes the desirable delay for the live edge to avoid a risk of getting 404 when playing at the bleeding edge
* @param {number} fragmentDuration - seconds?
Expand Down Expand Up @@ -220,19 +238,39 @@ function PlaybackController() {
return ((Math.round(new Date().getTime() - (currentTime * 1000 + availabilityStartTime))) / 1000).toFixed(3);
}

function onPlaybackCatchUpStart() {
if (videoModel) {
logger.info('onPlaybackCatchUpStart setting playback rate to', getCatchUpPlaybackRate());
originalPlaybackRate = originalPlaybackRate || getPlaybackRate();
videoModel.getElement().playbackRate = getCatchUpPlaybackRate();
}
}

function onPlaybackCatchUpEnd() {
if (videoModel) {
logger.info('onPlaybackCatchUpEnd setting playback rate to', originalPlaybackRate || 1);
videoModel.getElement().playbackRate = originalPlaybackRate || 1;
}
}

function reset() {
currentTime = 0;
liveStartTime = NaN;
playOnceInitialized = false;
commonEarliestTime = {};
liveDelay = 0;
availabilityStartTime = 0;
catchUpPlaybackRate = DEFAULT_CATCHUP_PLAYBACK_RATE;
bufferedRange = {};
if (videoModel) {
eventBus.off(Events.DATA_UPDATE_COMPLETED, onDataUpdateCompleted, this);
eventBus.off(Events.BUFFER_LEVEL_STATE_CHANGED, onBufferLevelStateChanged, this);
eventBus.off(Events.BYTES_APPENDED_END_FRAGMENT, onBytesAppended, this);
eventBus.off(Events.PERIOD_SWITCH_STARTED, onPeriodSwitchStarted, this);
eventBus.off(Events.PLAYBACK_CATCHUP_END, onPlaybackCatchUpEnd, this);
eventBus.off(Events.PLAYBACK_CATCHUP_START, onPlaybackCatchUpStart, this);
eventBus.off(Events.PLAYBACK_PROGRESS, onPlaybackProgression, this);
eventBus.off(Events.PLAYBACK_TIME_UPDATED, onPlaybackProgression, this);
stopUpdatingWallclockTime();
removeAllListeners();
}
Expand Down Expand Up @@ -510,6 +548,31 @@ function PlaybackController() {
return false;
}

function onPlaybackProgression() {
if (needToCatchUp() && !catchingUp) {
catchingUp = true;
eventBus.trigger(Events.PLAYBACK_CATCHUP_START, { sender: instance });
} else if (stopCatchingUp()) {
catchingUp = false;
eventBus.trigger(Events.PLAYBACK_CATCHUP_END, { sender: instance });
}
}

function needToCatchUp() {
if (getIsDynamic() && getCatchUpPlaybackRate() !== 1) {
return getCurrentLiveLatency() > (mediaPlayerModel.getLiveDelay() * (1 + LIVE_CATCHUP_THRESHOLD));
}
return false;
}

function stopCatchingUp() {
if (catchingUp && getIsDynamic()) {
return getCurrentLiveLatency() <= (mediaPlayerModel.getLiveDelay() + LIVE_CATCHUP_THRESHOLD );
}

return false;
}

function onBytesAppended(e) {
let earliestTime,
initialStartTime;
Expand Down Expand Up @@ -625,6 +688,7 @@ function PlaybackController() {
getEnded: getEnded,
getIsDynamic: getIsDynamic,
getStreamController: getStreamController,
setCatchUpPlaybackRate: setCatchUpPlaybackRate,
setLiveStartTime: setLiveStartTime,
getLiveStartTime: getLiveStartTime,
computeLiveDelay: computeLiveDelay,
Expand Down
1 change: 0 additions & 1 deletion src/streaming/rules/scheduling/BufferLevelRule.js
Expand Up @@ -69,7 +69,6 @@ function BufferLevelRule(config) {
bufferTarget = mediaPlayerModel.getStableBufferTime();
}
}

return bufferTarget;
}

Expand Down
9 changes: 9 additions & 0 deletions test/unit/mocks/PlaybackControllerMock.js
Expand Up @@ -9,6 +9,7 @@ class PlaybackControllerMock {
this.playing = false;
this.seeking = false;
this.isDynamic = false;
this.catchUpPlaybackRate = 1.05;
}

initialize() {}
Expand Down Expand Up @@ -97,6 +98,14 @@ class PlaybackControllerMock {
getStreamStartTime() {
return 0;
}

setCatchUpPlaybackRate(value) {
this.catchUpPlaybackRate = value;
}

getCatchUpPlaybackRate() {
return this.catchUpPlaybackRate;
}
}

export default PlaybackControllerMock;
21 changes: 20 additions & 1 deletion test/unit/streaming.MediaPlayerSpec.js
Expand Up @@ -247,7 +247,7 @@ describe('MediaPlayer', function () {
expect(playbackRate).to.equal(newPlaybackRate);
});

it('Method setPlaybackRate should return video element playback rate', function () {
it('Method getPlaybackRate should return video element playback rate', function () {
const elementPlayBackRate = videoElementMock.playbackRate;
const playerPlayBackRate = player.getPlaybackRate();
expect(playerPlayBackRate).to.equal(elementPlayBackRate);
Expand Down Expand Up @@ -330,6 +330,25 @@ describe('MediaPlayer', function () {
duration = player.duration();
expect(duration).to.equal(4);
});

it('Method setCatchUpPlaybackRate should change catchUpPlaybackRate', function () {
let rate = player.getCatchUpPlaybackRate();
expect(rate).to.equal(1.05);

player.setCatchUpPlaybackRate(1.2);
rate = player.getCatchUpPlaybackRate();
expect(rate).to.equal(1.2);

player.setCatchUpPlaybackRate(1);
rate = player.getCatchUpPlaybackRate();
expect(rate).to.equal(1);
});

it('Method setCatchUpPlaybackRate should throw an exception if given bad values', function () {
expect(() => {player.setCatchUpPlaybackRate(0.9);}).to.throw(MediaPlayer.PLAYBACK_CATCHUP_RATE_BAD_ARGUMENT_ERROR);
expect(() => {player.setCatchUpPlaybackRate(13);}).to.throw(MediaPlayer.PLAYBACK_CATCHUP_RATE_BAD_ARGUMENT_ERROR);
expect(() => {player.setCatchUpPlaybackRate('string');}).to.throw(MediaPlayer.PLAYBACK_CATCHUP_RATE_BAD_ARGUMENT_ERROR);
});
});
});

Expand Down

0 comments on commit 7c9cba3

Please sign in to comment.