Skip to content

Commit

Permalink
Queue events from backgrounded/detached providers (#2650)
Browse files Browse the repository at this point in the history
Queue events from providers that are backgrounded or detached, and then fire those events when the provider is foregrounded or reattached.

JW8-1043
  • Loading branch information
robwalch authored and johnBartos committed Jan 5, 2018
1 parent e94115d commit 0581f09
Show file tree
Hide file tree
Showing 12 changed files with 271 additions and 42 deletions.
2 changes: 1 addition & 1 deletion karma.conf.js
Expand Up @@ -44,7 +44,7 @@ module.exports = function(config) {
const packageInfo = JSON.parse(require('fs').readFileSync('package.json', 'utf8'));

config.set({
frameworks: ['mocha', 'chai', 'sinon'],
frameworks: ['mocha', 'sinon-chai'],
reporters: testReporters,
port: serverPort, // web server port
colors: true, // colors in the output (reporters and logs)
Expand Down
21 changes: 21 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Expand Up @@ -47,12 +47,13 @@
"karma-requirejs": "1.1.0",
"karma-safari-launcher": "1.0.0",
"karma-sinon": "1.0.5",
"karma-sinon-chai": "1.3.3",
"karma-webpack": "2.0.9",
"less": "2.7.3",
"less-loader": "4.0.5",
"load-grunt-tasks": "3.5.2",
"loader-utils": "1.1.0",
"marked": "^0.3.9",
"marked": "0.3.9",
"mocha": "4.1.0",
"node-libs-browser": "2.1.0",
"phantomjs-polyfill": "0.0.2",
Expand All @@ -63,6 +64,7 @@
"requirejs": "2.3.5",
"simple-style-loader": "0.4.3",
"sinon": "4.1.3",
"sinon-chai": "2.14.0",
"source-map": "0.6.1",
"stylelint": "8.4.0",
"svg-inline-loader": "0.8.0",
Expand Down
34 changes: 19 additions & 15 deletions src/js/controller/controller.js
Expand Up @@ -686,21 +686,22 @@ Object.assign(Controller.prototype, {
}

function addProgramControllerListeners() {
_programController.on('all', _triggerAfterReady, _this);
_programController.on('subtitlesTracks', (e) => {
_captions.setSubtitlesTracks(e.tracks);
const defaultCaptionsIndex = _captions.getCurrentIndex();

// set the current captions if the default index isn't 0 or "Off"
if (defaultCaptionsIndex > 0) {
_setCurrentCaptions(defaultCaptionsIndex);
}
});
_programController.on(MEDIA_COMPLETE, () => {
// Insert a small delay here so that other complete handlers can execute
resolved.then(_completeHandler);
});
_programController.on(MEDIA_ERROR, _this.triggerError, _this);
_programController
.on('all', _triggerAfterReady, _this)
.on('subtitlesTracks', (e) => {
_captions.setSubtitlesTracks(e.tracks);
const defaultCaptionsIndex = _captions.getCurrentIndex();

// set the current captions if the default index isn't 0 or "Off"
if (defaultCaptionsIndex > 0) {
_setCurrentCaptions(defaultCaptionsIndex);
}
}, _this)
.on(MEDIA_COMPLETE, () => {
// Insert a small delay here so that other complete handlers can execute
resolved.then(_completeHandler);
}, _this)
.on(MEDIA_ERROR, _this.triggerError, _this);
}

function syncInitialModelState() {
Expand Down Expand Up @@ -876,6 +877,9 @@ Object.assign(Controller.prototype, {
_captions.destroy();
_captions = null;
}
if (_programController) {
_programController.destroy();
}
this.instreamDestroy();
};

Expand Down
20 changes: 10 additions & 10 deletions src/js/program/media-controller.js
@@ -1,5 +1,6 @@
import cancelable from 'utils/cancelable';
import Eventable from 'utils/eventable';
import ApiQueueDecorator from 'api/api-queue';
import { ProviderListener } from 'program/program-listeners';
import { resolved } from 'polyfills/promise';
import { MediaModel } from 'controller/model';
Expand All @@ -22,6 +23,8 @@ export default class MediaController extends Eventable {
this.providerListener = new ProviderListener(this);
this.thenPlayPromise = cancelable(() => {});
addProviderListeners(this);
this.eventQueue = new ApiQueueDecorator(this, ['trigger'],
() => !this.attached || this.background);
}

play(playReason) {
Expand Down Expand Up @@ -65,6 +68,7 @@ export default class MediaController extends Eventable {
const { provider, mediaModel } = this;
mediaModel.off();
provider.off();
this.eventQueue.destroy();
this.detach();
if (provider.getContainer()) {
provider.remove();
Expand All @@ -79,8 +83,7 @@ export default class MediaController extends Eventable {

// Restore the playback rate to the provider in case it changed while detached and we reused a video tag.
model.setPlaybackRate(model.get('defaultPlaybackRate'));

addProviderListeners(this);
this.eventQueue.flush();
provider.attachMedia();
this.attached = true;
model.set('attached', true);
Expand All @@ -93,7 +96,6 @@ export default class MediaController extends Eventable {
detach() {
const { model, provider } = this;
this.thenPlayPromise.cancel();
removeProviderListeners(this);
provider.detachMedia();
this.attached = false;
model.set('attached', false);
Expand Down Expand Up @@ -233,8 +235,11 @@ export default class MediaController extends Eventable {
container.removeChild(provider.video);
this.container = null;
}
} else if (this.beforeComplete) {
this._playbackComplete();
} else {
this.eventQueue.flush();
if (this.beforeComplete) {
this._playbackComplete();
}
}
}

Expand Down Expand Up @@ -277,10 +282,5 @@ function syncPlayerWithMediaModel(mediaModel) {
}

function addProviderListeners(mediaController) {
removeProviderListeners(mediaController);
mediaController.provider.on('all', mediaController.providerListener, mediaController);
}

function removeProviderListeners(mediaController) {
mediaController.provider.off('all', mediaController.providerListener, mediaController);
}
10 changes: 10 additions & 0 deletions src/js/program/program-controller.js
Expand Up @@ -294,6 +294,16 @@ class ProgramController extends Eventable {
this.mediaPool.prime();
}

/**
* Removes all event listeners and destroys all media.
* @returns {undefined}
*/
destroy() {
this.off();
this._destroyBackgroundMedia();
this._destroyActiveMedia();
}

/**
* Activates the provided media controller, placing it into the foreground.
* @returns {undefined}
Expand Down
13 changes: 6 additions & 7 deletions src/js/program/program-listeners.js
Expand Up @@ -16,9 +16,8 @@ export function ProviderListener(mediaController) {
case MEDIA_TYPE:
if (mediaModel.get(MEDIA_TYPE) !== data.mediaType) {
mediaModel.set(MEDIA_TYPE, data.mediaType);
mediaController.trigger(type, event);
}
return;
break;
case MEDIA_VISUAL_QUALITY:
mediaModel.set(MEDIA_VISUAL_QUALITY, Object.assign({}, data));
return;
Expand All @@ -31,11 +30,11 @@ export function ProviderListener(mediaController) {
const previousState = mediaModel.attributes.mediaState;
mediaModel.attributes.mediaState = data.newstate;
mediaModel.trigger('change:mediaState', mediaModel, data.newstate, previousState);
}
// This "return" is important because
// we are choosing to not propagate model event.
// Instead letting the master controller do so
return;
}
case MEDIA_COMPLETE:
mediaController.beforeComplete = true;
mediaController.trigger(MEDIA_BEFORECOMPLETE, event);
Expand All @@ -56,7 +55,7 @@ export function ProviderListener(mediaController) {
}
case MEDIA_BUFFER:
mediaModel.set('buffer', data.bufferPercent);
break;
/* falls through to update duration while media is loaded */
case MEDIA_TIME: {
mediaModel.set('position', data.position);
const duration = data.duration;
Expand Down Expand Up @@ -93,10 +92,10 @@ export function MediaControllerListener(model, programController) {
break;
case 'flashBlocked':
model.set('flashBlocked', true);
break;
return;
case 'flashUnblocked':
model.set('flashBlocked', false);
break;
return;
case MEDIA_VOLUME:
model.set(type, data[type]);
return;
Expand All @@ -112,8 +111,8 @@ export function MediaControllerListener(model, programController) {
if (rate > 0) {
model.set('playbackRate', rate);
}
return;
}
break;
case MEDIA_META: {
Object.assign(model.get('itemMeta'), data.metadata);
break;
Expand Down
4 changes: 3 additions & 1 deletion src/js/providers/default.js
Expand Up @@ -2,6 +2,8 @@ import { PLAYER_STATE, MEDIA_TYPE } from 'events/events';

const noop = function() {};
const returnFalse = (() => false);
const getNameResult = { name: 'default' };
const returnName = (() => getNameResult);

/** Audio Track information for tracks returned by {@link Api#getAudioTracks jwplayer().getAudioTracks()}
* @typedef {object} AudioTrackOption
Expand Down Expand Up @@ -50,7 +52,7 @@ const DefaultProvider = {
// Sets the parent element, causing provider to append <video> into it
setContainer: returnFalse,

getName: noop,
getName: returnName,

getQualityLevels: noop,
getCurrentQuality: noop,
Expand Down
16 changes: 14 additions & 2 deletions test/mock/mock-provider.js
Expand Up @@ -5,15 +5,27 @@ import BackboneEvents from 'utils/backbone.events';
import ProviderDefaults from 'providers/default';


class MockDefault {};
class MockDefault {}
MockDefault.prototype = Object.assign({}, ProviderDefaults, BackboneEvents, VideoAction, VideoAttached, Tracks);

export default class MockProvider extends MockDefault {
constructor() {
constructor(playerId, playerConfig, mediaElement) {
super();
this.video = mediaElement;
}

getName() {
return { name: 'mock' };
}

getContainer() {
return this.container;
}

setContainer(element) {
this.container = element;
if (this.video && this.video.parentNode !== element) {
element.appendChild(this.video);
}
}
}
2 changes: 1 addition & 1 deletion test/unit/background-loading-test.js
Expand Up @@ -2,7 +2,7 @@ import Model from 'controller/model';
import ProgramController from 'program/program-controller';
import MediaController from 'program/media-controller';
import MockProvider from 'mock/mock-provider';
import MediaElementPool from 'program/media-element-pool'
import MediaElementPool from 'program/media-element-pool';
import sinon from 'sinon';

const defaultConfig = {
Expand Down
9 changes: 5 additions & 4 deletions test/unit/media-pool-tests.js
@@ -1,4 +1,5 @@
import MediaPool from 'program/media-element-pool';
import sinon from 'sinon';

describe('Media Element Pool', function () {
let mediaPool = null;
Expand Down Expand Up @@ -36,10 +37,10 @@ describe('Media Element Pool', function () {
});

it('synchronizes volume across the pool', function () {
mediaPool.syncVolume(50);
for (let i = 0; i < 3; i++) {
expect(mediaPool.getPrimedElement().volume).to.equal(0.5);
}
mediaPool.syncVolume(50);
for (let i = 0; i < 3; i++) {
expect(mediaPool.getPrimedElement().volume).to.equal(0.5);
}
});

it('synchronizes mute across the pool', function () {
Expand Down

0 comments on commit 0581f09

Please sign in to comment.