Skip to content

Commit

Permalink
Add parameter in MediaPlayer.setQualityFor() to force quality switch (D…
Browse files Browse the repository at this point in the history
…ash-Industry-Forum#3633)

* MediaPlayer: add "replace" parameter in setQualityFor()

* Replace segments (as for track switch) when requested by quality switch

* Add option in control bar and sample to force quality switches

* Add separate functions for the different quality switch modes

* Move start of quality change handling logic to StreamController.js

Co-authored-by: Daniel Silhavy <[email protected]>
  • Loading branch information
bbert and dsilhavy authored May 11, 2021
1 parent edf54d4 commit 7ad55a2
Show file tree
Hide file tree
Showing 13 changed files with 190 additions and 76 deletions.
7 changes: 6 additions & 1 deletion contrib/akamai/controlbar/ControlBar.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ var ControlBar = function (dashjsMediaPlayer, displayUTCTimeCodes) {
var videoControllerVisibleTimeout = 0;
var liveThresholdSecs = 12;
var textTrackList = {};
var forceQuality = false;
var video,
videoContainer,
videoController,
Expand Down Expand Up @@ -845,7 +846,7 @@ var ControlBar = function (dashjsMediaPlayer, displayUTCTimeCodes) {
if (item.index > 0) {
cfg.streaming.abr.autoSwitchBitrate[item.mediaType] = false;
self.player.updateSettings(cfg);
self.player.setQualityFor(item.mediaType, item.index - 1);
self.player.setQualityFor(item.mediaType, item.index - 1, forceQuality);
} else {
cfg.streaming.abr.autoSwitchBitrate[item.mediaType] = true;
self.player.updateSettings(cfg);
Expand Down Expand Up @@ -1015,6 +1016,10 @@ var ControlBar = function (dashjsMediaPlayer, displayUTCTimeCodes) {
videoController.classList.remove('disable');
},

forceQualitySwitch: function (value) {
forceQuality = value;
},

resetSelectionMenus: function () {
if (menuHandlersList.bitrate) {
bitrateListBtn.removeEventListener('click', menuHandlersList.bitrate);
Expand Down
2 changes: 1 addition & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,7 @@ declare namespace dashjs {

getQualityFor(type: MediaType): number;

setQualityFor(type: MediaType, value: number): void;
setQualityFor(type: MediaType, value: number, replace?: boolean): void;

updatePortalSize(): void;

Expand Down
5 changes: 5 additions & 0 deletions samples/dash-if-reference-player/app/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ app.controller('DashController', ['$scope', '$window', 'sources', 'contributors'
$scope.jumpGapsSelected = true;
$scope.fastSwitchSelected = true;
$scope.videoAutoSwitchSelected = true;
$scope.forceQualitySwitchSelected = false;
$scope.videoQualities = [];
$scope.ABRStrategy = 'abrDynamic';

Expand Down Expand Up @@ -537,6 +538,10 @@ app.controller('DashController', ['$scope', '$window', 'sources', 'contributors'
});
};

$scope.toggleForceQualitySwitch = function () {
$scope.controlbar.forceQualitySwitch($scope.forceQualitySwitchSelected);
};

$scope.toggleScheduleWhilePaused = function () {
$scope.player.updateSettings({
'streaming': {
Expand Down
6 changes: 6 additions & 0 deletions samples/dash-if-reference-player/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,12 @@
ng-change="toggleVideoAutoSwitch()" ng-checked="videoAutoSwitchSelected">
Video Auto Switch
</label>
<label class="topcoat-checkbox" data-toggle="tooltip" data-placement="right"
title="Forces quality switch to be immediatly effective">
<input type="checkbox" id="forceQualitySwitchCB" ng-model="forceQualitySwitchSelected"
ng-change="toggleForceQualitySwitch()" ng-checked="forceQualitySwitchSelected">
Force Quality Switch
</label>
<label class="topcoat-checkbox" data-toggle="tooltip" data-placement="right"
title="ABR - Use custom ABR rules">
<input type="checkbox" id="customABRRules" ng-model="customABRRulesSelected"
Expand Down
2 changes: 1 addition & 1 deletion src/core/events/CoreEvents.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class CoreEvents extends EventsBase {
this.BUFFERING_COMPLETED = 'bufferingCompleted';
this.BUFFER_CLEARED = 'bufferCleared';
this.BYTES_APPENDED_END_FRAGMENT = 'bytesAppendedEndFragment';
this.BUFFER_REPLACEMENT_STARTED = 'bufferReplacementStarted';
this.CHECK_FOR_EXISTENCE_COMPLETED = 'checkForExistenceCompleted';
this.CURRENT_TRACK_CHANGED = 'currentTrackChanged';
this.DATA_UPDATE_COMPLETED = 'dataUpdateCompleted';
Expand All @@ -70,7 +71,6 @@ class CoreEvents extends EventsBase {
this.STREAM_REQUESTING_COMPLETED = 'streamRequestingCompleted';
this.TEXT_TRACKS_QUEUE_INITIALIZED = 'textTracksQueueInitialized';
this.TIME_SYNCHRONIZATION_COMPLETED = 'timeSynchronizationComplete';
this.TRACK_REPLACEMENT_STARTED = 'trackReplacementStarted';
this.UPDATE_TIME_SYNC_OFFSET = 'updateTimeSyncOffset';
this.URL_RESOLUTION_FAILED = 'urlResolutionFailed';
this.VIDEO_CHUNK_RECEIVED = 'videoChunkReceived';
Expand Down
9 changes: 4 additions & 5 deletions src/dash/controllers/RepresentationController.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ function RepresentationController(config) {
function setup() {
resetInitialSettings();

eventBus.on(MediaPlayerEvents.QUALITY_CHANGE_REQUESTED, onQualityChanged, instance);
eventBus.on(MediaPlayerEvents.MANIFEST_VALIDITY_CHANGED, onManifestValidityChanged, instance);
}

Expand Down Expand Up @@ -94,7 +93,6 @@ function RepresentationController(config) {
}

function reset() {
eventBus.off(MediaPlayerEvents.QUALITY_CHANGE_REQUESTED, onQualityChanged, instance);
eventBus.off(MediaPlayerEvents.MANIFEST_VALIDITY_CHANGED, onManifestValidityChanged, instance);

resetInitialSettings();
Expand Down Expand Up @@ -289,8 +287,8 @@ function RepresentationController(config) {
}
}

function onQualityChanged(e) {
currentVoRepresentation = getRepresentationForQuality(e.newQuality);
function prepareQualityChange(newQuality) {
currentVoRepresentation = getRepresentationForQuality(newQuality);
addRepresentationSwitch();
}

Expand All @@ -312,7 +310,8 @@ function RepresentationController(config) {
updateData,
getCurrentRepresentation,
getRepresentationForQuality,
reset: reset
prepareQualityChange,
reset
};

setup();
Expand Down
5 changes: 3 additions & 2 deletions src/streaming/MediaPlayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -868,13 +868,14 @@ function MediaPlayer() {
*
* @param {MediaType} type - 'video', 'audio' or 'image'
* @param {number} value - the quality index, 0 corresponding to the lowest bitrate
* @param {boolean} replace - true if segments have to be replaced by segments of the new quality
* @memberof module:MediaPlayer
* @see {@link module:MediaPlayer#setAutoSwitchQualityFor setAutoSwitchQualityFor()}
* @see {@link module:MediaPlayer#getQualityFor getQualityFor()}
* @throws {@link module:MediaPlayer~STREAMING_NOT_INITIALIZED_ERROR STREAMING_NOT_INITIALIZED_ERROR} if called before initializePlayback function
* @instance
*/
function setQualityFor(type, value) {
function setQualityFor(type, value, replace = false) {
if (!streamingInitialized) {
throw STREAMING_NOT_INITIALIZED_ERROR;
}
Expand All @@ -888,7 +889,7 @@ function MediaPlayer() {
thumbnailController.setTrackByIndex(value);
}
}
abrController.setPlaybackQuality(type, streamController.getActiveStreamInfo(), value);
abrController.setPlaybackQuality(type, streamController.getActiveStreamInfo(), value, { replace: replace });
}

/**
Expand Down
23 changes: 20 additions & 3 deletions src/streaming/Stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,14 @@ function Stream(config) {
}
}

function prepareQualityChange(e) {
const processor = _getProcessorByType(e.mediaType);

if (processor) {
processor.prepareQualityChange(e);
}
}

function addInlineEvents() {
if (eventController) {
const events = adapter.getEventsFor(streamInfo);
Expand Down Expand Up @@ -753,14 +761,22 @@ function Stream(config) {
}

function getProcessorForMediaInfo(mediaInfo) {
if (!mediaInfo) {
if (!mediaInfo || !mediaInfo.type) {
return null;
}

return _getProcessorByType(mediaInfo.type);
}

function _getProcessorByType(type) {
if (!type) {
return null;
}

let processors = getProcessors();

return processors.filter(function (processor) {
return (processor.getType() === mediaInfo.type);
return (processor.getType() === type);
})[0];
}

Expand Down Expand Up @@ -955,7 +971,8 @@ function Stream(config) {
getHasFinishedBuffering,
setPreloaded,
startScheduleControllers,
prepareTrackChange
prepareTrackChange,
prepareQualityChange
};

setup();
Expand Down
120 changes: 87 additions & 33 deletions src/streaming/StreamProcessor.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@ function StreamProcessor(config) {
eventBus.on(Events.BUFFER_LEVEL_STATE_CHANGED, _onBufferLevelStateChanged, instance);
eventBus.on(Events.BUFFER_CLEARED, _onBufferCleared, instance);
eventBus.on(Events.SEEK_TARGET, _onSeekTarget, instance);
eventBus.on(Events.QUALITY_CHANGE_REQUESTED, _onQualityChanged, instance);
eventBus.on(Events.FRAGMENT_LOADING_ABANDONED, _onFragmentLoadingAbandoned, instance);
eventBus.on(Events.FRAGMENT_LOADING_COMPLETED, _onFragmentLoadingCompleted, instance);
eventBus.on(Events.QUOTA_EXCEEDED, _onQuotaExceeded, instance);
Expand Down Expand Up @@ -240,7 +239,6 @@ function StreamProcessor(config) {
eventBus.off(Events.BUFFER_LEVEL_STATE_CHANGED, _onBufferLevelStateChanged, instance);
eventBus.off(Events.BUFFER_CLEARED, _onBufferCleared, instance);
eventBus.off(Events.SEEK_TARGET, _onSeekTarget, instance);
eventBus.off(Events.QUALITY_CHANGE_REQUESTED, _onQualityChanged, instance);
eventBus.off(Events.FRAGMENT_LOADING_ABANDONED, _onFragmentLoadingAbandoned, instance);
eventBus.off(Events.FRAGMENT_LOADING_COMPLETED, _onFragmentLoadingCompleted, instance);
eventBus.off(Events.SET_FRAGMENTED_TEXT_AFTER_DISABLED, _onSetFragmentedTextAfterDisabled, instance);
Expand Down Expand Up @@ -519,39 +517,94 @@ function StreamProcessor(config) {
* @param {object} e
* @private
*/
function _onQualityChanged(e) {
const representationInfo = getRepresentationInfo(e.newQuality);
function prepareQualityChange(e) {
logger.debug(`Preparing quality switch for type ${type}`);
const newQuality = e.newQuality;

// Stop scheduling until we are done with preparing the quality switch
scheduleController.clearScheduleTimer();

const representationInfo = getRepresentationInfo(newQuality);
scheduleController.setCurrentRepresentation(representationInfo);
bufferController.prepareForQualityChange()
representationController.prepareQualityChange(newQuality);

// Abort the current request to avoid inconsistencies. A quality switch can also be triggered manually by the application.
// If we update the buffer values now, or initialize a request to the new init segment, the currently downloading media segment might "work" with wrong values.
// Everything that is already in the buffer queue is ok and will be handled by the corresponding function below depending on the switch mode.
fragmentModel.abortRequests();

// In any case we need to update the MSE.timeOffset
bufferController.updateBufferTimestampOffset(representationInfo)
.then(() => {
// if we switch up in quality and need to replace existing parts in the buffer we need to adjust the buffer target
if (settings.get().streaming.buffer.fastSwitchEnabled) {
const time = playbackController.getTime();
let safeBufferLevel = 1.5;
const request = fragmentModel.getRequests({
state: FragmentModel.FRAGMENT_MODEL_EXECUTED,
time: time + safeBufferLevel,
threshold: 0
})[0];

if (request && !adapter.getIsTextTrack(mimeType)) {
const bufferLevel = bufferController.getBufferLevel();
const abandonmentState = abrController.getAbandonmentStateFor(streamInfo.id, type);

if (request.quality < representationInfo.quality && bufferLevel >= safeBufferLevel && abandonmentState !== MetricsConstants.ABANDON_LOAD) {
setExplicitBufferingTime(time + safeBufferLevel);
scheduleController.setCheckPlaybackQuality(false);
}
}

// If the switch should occur immediately we need to replace existing stuff in the buffer
if (e.reason && e.reason.replace) {
_prepareReplacementQualitySwitch();
}

// If fast switch is enabled we check if we are supposed to replace existing stuff in the buffer
else if (settings.get().streaming.buffer.fastSwitchEnabled) {
_prepareForFastQualitySwitch(representationInfo);
}

// Default quality switch. We append the new quality to the already buffered stuff
else {
_prepareForDefaultQualitySwitch();
}

dashMetrics.pushPlayListTraceMetrics(new Date(), PlayListTrace.REPRESENTATION_SWITCH_STOP_REASON);
dashMetrics.createPlaylistTraceMetrics(representationInfo.id, playbackController.getTime() * 1000, playbackController.getPlaybackRate());
})
.catch((e) => {
logger.error(e);
}

function _prepareReplacementQualitySwitch() {

// Inform other classes like the GapController that we are replacing existing stuff
eventBus.trigger(Events.BUFFER_REPLACEMENT_STARTED, {
mediaType: type,
streamId: streamInfo.id
}, { mediaType: type, streamId: streamInfo.id });

// Abort appending segments to the buffer. Also adjust the appendWindow as we might have been in the progress of prebuffering stuff.
bufferController.prepareForReplacementQualitySwitch()
.then(() => {
_bufferClearedForReplacement();
})
.catch(() => {
_bufferClearedForReplacement();
});
}

function _prepareForFastQualitySwitch(representationInfo) {
// if we switch up in quality and need to replace existing parts in the buffer we need to adjust the buffer target
const time = playbackController.getTime();
let safeBufferLevel = 1.5;
const request = fragmentModel.getRequests({
state: FragmentModel.FRAGMENT_MODEL_EXECUTED,
time: time + safeBufferLevel,
threshold: 0
})[0];

if (request && !adapter.getIsTextTrack(mimeType)) {
const bufferLevel = bufferController.getBufferLevel();
const abandonmentState = abrController.getAbandonmentStateFor(streamInfo.id, type);

if (request.quality < representationInfo.quality && bufferLevel >= safeBufferLevel && abandonmentState !== MetricsConstants.ABANDON_LOAD) {
const targetTime = time + safeBufferLevel;
setExplicitBufferingTime(targetTime);
scheduleController.setCheckPlaybackQuality(false);
scheduleController.startScheduleTimer();
} else {
_prepareForDefaultQualitySwitch();
}
} else {
scheduleController.startScheduleTimer();
}
}

function _prepareForDefaultQualitySwitch() {
// We might have aborted the current request. We need to set an explicit buffer time based on what we already have in the buffer.
_bufferClearedForNonReplacement()
}

/**
Expand Down Expand Up @@ -876,7 +929,7 @@ function StreamProcessor(config) {
// when we are supposed to replace it does not matter if buffering is already completed
if (shouldReplace) {
// Inform other classes like the GapController that we are replacing existing stuff
eventBus.trigger(Events.TRACK_REPLACEMENT_STARTED, {
eventBus.trigger(Events.BUFFER_REPLACEMENT_STARTED, {
mediaType: type,
streamId: streamInfo.id
}, { mediaType: type, streamId: streamInfo.id });
Expand All @@ -892,20 +945,20 @@ function StreamProcessor(config) {
return bufferController.updateBufferTimestampOffset(representationInfo);
})
.then(() => {
_bufferClearedForReplacementTrackSwitch();
_bufferClearedForReplacement();
})
.catch(() => {
_bufferClearedForReplacementTrackSwitch();
_bufferClearedForReplacement();
});
} else {
// We do not replace anything that is already in the buffer. Still we need to prepare the buffer for the track switch
bufferController.prepareForNonReplacementTrackSwitch(mediaInfo.codec)
.then(() => {
_bufferClearedForNonReplacementTrackSwitch();
_bufferClearedForNonReplacement();
})
.catch
(() => {
_bufferClearedForNonReplacementTrackSwitch();
_bufferClearedForNonReplacement();
});
}
}
Expand All @@ -914,7 +967,7 @@ function StreamProcessor(config) {
* For an instant track switch we need to adjust the buffering time after the buffer has been pruned.
* @private
*/
function _bufferClearedForReplacementTrackSwitch() {
function _bufferClearedForReplacement() {
const targetTime = playbackController.getTime();

if (settings.get().streaming.buffer.flushBufferAtTrackSwitch) {
Expand All @@ -928,7 +981,7 @@ function StreamProcessor(config) {
scheduleController.startScheduleTimer();
}

function _bufferClearedForNonReplacementTrackSwitch() {
function _bufferClearedForNonReplacement() {
const time = playbackController.getTime();
const targetTime = bufferController.getContinuousBufferTimeForTargetTime(time);

Expand Down Expand Up @@ -1010,6 +1063,7 @@ function StreamProcessor(config) {
selectMediaInfo,
addMediaInfo,
prepareTrackSwitch,
prepareQualityChange,
getMediaInfoArr,
getMediaInfo,
getMediaSource,
Expand Down
Loading

0 comments on commit 7ad55a2

Please sign in to comment.