Skip to content

Commit

Permalink
Merge pull request #2605 from aescarcha/lowLatencyCatchUp
Browse files Browse the repository at this point in the history
Low latency catch up
  • Loading branch information
epiclabsDASH committed Jun 7, 2018
2 parents 640da28 + f14e6ff commit 9b69ae2
Show file tree
Hide file tree
Showing 9 changed files with 164 additions and 6 deletions.
2 changes: 1 addition & 1 deletion build/typings/index.d.ts
Expand Up @@ -123,7 +123,7 @@ declare namespace dashjs {
isMuted(): boolean;
setVolume(value: number): void;
getVolume(): number;
time(streamId: string | undefined): number;
time(streamId?: string): number;
duration(): number;
timeAsUTC(): number;
durationAsUTC(): number;
Expand Down
4 changes: 3 additions & 1 deletion index.d.ts
Expand Up @@ -123,7 +123,7 @@ declare namespace dashjs {
isMuted(): boolean;
setVolume(value: number): void;
getVolume(): number;
time(streamId: string | undefined): number;
time(streamId?: string): number;
duration(): number;
timeAsUTC(): number;
durationAsUTC(): number;
Expand Down 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
4 changes: 2 additions & 2 deletions samples/dash-if-reference-player/app/sources.json
Expand Up @@ -74,15 +74,15 @@
"name": "Low Latency (Single-Rate)",
"bufferConfig" : {
"lowLatencyMode": true,
"liveDelay": 3
"liveDelay": 3.5
}
},
{
"url": "https://vm2.dashif.org/livesim-chunked/chunkdur_1/ato_7/testpic4_8s/Manifest.mpd",
"name": "Low Latency (Multi-Rate)",
"bufferConfig" : {
"lowLatencyMode": true,
"liveDelay": 3
"liveDelay": 3.5
}
},
{
Expand Down
36 changes: 36 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,39 @@ function MediaPlayer() {
return getVideoElement().playbackRate;
}

/**
* Use this method to set the catch up rate, as a percentage, for low latency live streams. In low latency mode,
* when measured latency is higher than the target one ({@link module:MediaPlayer#setLiveDelay setLiveDelay()}),
* dash.js increases playback rate the percentage defined with this method until target is reached.
*
* Valid values for catch up rate are in range 0-20%. Set it to 0% to turn off live catch up feature.
*
* Note: Catch-up mechanism is only applied when playing low latency live streams.
*
* @param {number} value Percentage in which playback rate is increased when live catch up mechanism is activated.
* @memberof module:MediaPlayer
* @see {@link module:MediaPlayer#setLiveDelay setLiveDelay()}
* @default {number} 0.05
* @instance
*/
function setCatchUpPlaybackRate(value) {
if (isNaN(value) || value < 0.0 || value > 0.20) {
throw PLAYBACK_CATCHUP_RATE_BAD_ARGUMENT_ERROR;
}
playbackController.setCatchUpPlaybackRate(value);
}

/**
* Returns the current catchup playback rate.
* @returns {number}
* @see {@link module:MediaPlayer#setCatchUpPlaybackRate setCatchUpPlaybackRate()}
* @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 +2865,8 @@ function MediaPlayer() {
seek: seek,
setPlaybackRate: setPlaybackRate,
getPlaybackRate: getPlaybackRate,
setCatchUpPlaybackRate: setCatchUpPlaybackRate,
getCatchUpPlaybackRate: getCatchUpPlaybackRate,
setMute: setMute,
isMuted: isMuted,
setVolume: setVolume,
Expand Down
17 changes: 17 additions & 0 deletions src/streaming/MediaPlayerEvents.js
Expand Up @@ -214,6 +214,23 @@ class MediaPlayerEvents extends EventsBase {
*/
this.CAN_PLAY = 'canPlay';

/**
* Sent when live catch mechanism has been activated, which implies the measured latency of the low latency
* stream that is been played has gone beyond the target one.
* @see {@link module:MediaPlayer#setCatchUpPlaybackRate setCatchUpPlaybackRate()}
* @see {@link module:MediaPlayer#setLiveDelay setLiveDelay()}
* @event MediaPlayerEvents#PLAYBACK_CATCHUP_START
*/
this.PLAYBACK_CATCHUP_START = 'playbackCatchupStart';

/**
* Sent live catch up mechanism has been deactivated.
* @see {@link module:MediaPlayer#setCatchUpPlaybackRate setCatchUpPlaybackRate()}
* @see {@link module:MediaPlayer#setLiveDelay setLiveDelay()}
* @event MediaPlayerEvents#PLAYBACK_CATCHUP_END
*/
this.PLAYBACK_CATCHUP_END = 'playbackCatchupEnd';

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

const LIVE_UPDATE_PLAYBACK_TIME_INTERVAL_MS = 500;
const DEFAULT_CATCHUP_PLAYBACK_RATE = 0.05;

// Start catching up mechanism for low latency live streaming
// when latency goes beyong targetDelay * (1 + LIVE_CATCHUP_START_THRESHOLD)
const LIVE_CATCHUP_START_THRESHOLD = 0.35;

function PlaybackController() {

Expand All @@ -63,8 +68,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 +88,8 @@ 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_PROGRESS, onPlaybackProgression, this);
eventBus.on(Events.PLAYBACK_TIME_UPDATED, onPlaybackProgression, this);

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

function setCatchUpPlaybackRate(value) {
catchUpPlaybackRate = value;

// If value == 0.0, deactivate catchup mechanism
if (value === 0.0 && getPlaybackRate() > 1.0) {
stopPlaybackCatchUp();
}
}

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 +244,52 @@ function PlaybackController() {
return ((Math.round(new Date().getTime() - (currentTime * 1000 + availabilityStartTime))) / 1000).toFixed(3);
}

function startPlaybackCatchUp() {
if (videoModel) {
const playbackRate = 1 + getCatchUpPlaybackRate();
const currentRate = getPlaybackRate();
if (playbackRate !== currentRate) {
catchingUp = true;

logger.info('Starting live catchup mechanism. Setting playback rate to', playbackRate);
originalPlaybackRate = currentRate;
videoModel.getElement().playbackRate = playbackRate;

eventBus.trigger(Events.PLAYBACK_CATCHUP_START, { sender: instance });
}
}
}

function stopPlaybackCatchUp() {
if (videoModel) {
const playbackRate = originalPlaybackRate || 1;
if (playbackRate !== getPlaybackRate()) {
catchingUp = false;

logger.info('Stopping live catchup mechanism. Setting playback rate to', playbackRate);
videoModel.getElement().playbackRate = playbackRate;

eventBus.trigger(Events.PLAYBACK_CATCHUP_END, { sender: instance });
}
}
}

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_PROGRESS, onPlaybackProgression, this);
eventBus.off(Events.PLAYBACK_TIME_UPDATED, onPlaybackProgression, this);
stopUpdatingWallclockTime();
removeAllListeners();
}
Expand Down Expand Up @@ -510,6 +567,24 @@ function PlaybackController() {
return false;
}

function onPlaybackProgression() {
if (isDynamic && mediaPlayerModel.getLowLatencyEnabled() && getCatchUpPlaybackRate() > 0.0) {
if (!catchingUp && needToCatchUp()) {
startPlaybackCatchUp();
} else if (stopCatchingUp()) {
stopPlaybackCatchUp();
}
}
}

function needToCatchUp() {
return getCurrentLiveLatency() > (mediaPlayerModel.getLiveDelay() * (1 + LIVE_CATCHUP_START_THRESHOLD));
}

function stopCatchingUp() {
return getCurrentLiveLatency() <= (mediaPlayerModel.getLiveDelay() );
}

function onBytesAppended(e) {
let earliestTime,
initialStartTime;
Expand Down Expand Up @@ -625,6 +700,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 = 0.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(0.05);

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

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

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 9b69ae2

Please sign in to comment.