diff --git a/package.json b/package.json index fee0f7aa..e52834ec 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "access": "public" }, "devDependencies": { - "@playkit-js/playkit-js": "^0.37.0", + "@playkit-js/playkit-js": "^0.47.0", "babel-cli": "^6.18.0", "babel-core": "^6.18.2", "babel-eslint": "^7.1.1", diff --git a/src/kava-event-model.js b/src/kava-event-model.js index 271f1da6..1c2f662a 100644 --- a/src/kava-event-model.js +++ b/src/kava-event-model.js @@ -13,15 +13,41 @@ export const KavaEventModel: {[event: string]: KavaEvent} = { VIEW: { type: 'VIEW', index: 99, - getEventModel: (model: KavaModel) => ({ - playTimeSum: model.getPlayTimeSum(), - bufferTime: model.getBufferTime(), - bufferTimeSum: model.getBufferTimeSum(), - actualBitrate: model.getActualBitrate(), - averageBitrate: model.getAverageBitrate(), - audioLanguage: model.getLanguage(), - captionsLanguage: model.getCaption() - }) + getEventModel: (model: KavaModel) => { + const eventModel: {[name: string]: any} = { + playTimeSum: model.getPlayTimeSum(), + bufferTime: model.getBufferTime(), + bufferTimeSum: model.getBufferTimeSum(), + actualBitrate: model.getActualBitrate(), + averageBitrate: model.getAverageBitrate(), + audioLanguage: model.getLanguage(), + captionsLanguage: model.getCaption(), + soundMode: model.getSoundMode(), + tabMode: model.getTabMode() + }; + + if (!isNaN(model.getForwardBufferHealth())) { + eventModel.forwardBufferHealth = model.getForwardBufferHealth(); + } + if (model.getMaxManifestDownloadTime() > 0) { + eventModel.manifestDownloadTime = model.getMaxManifestDownloadTime(); + } + if (model.getSegmentDownloadTime() > 0) { + eventModel.segmentDownloadTime = model.getSegmentDownloadTime(); + } + if (model.getBandwidth()) { + eventModel.bandwidth = model.getBandwidth(); + } + if (model.getDroppedFramesRatio() != null) { + eventModel.droppedFramesRatio = model.getDroppedFramesRatio(); + } + + if (!isNaN(model.getTargetBuffer())) { + eventModel.targetBuffer = model.getTargetBuffer(); + } + + return eventModel; + } }, /** * @type {string} IMPRESSION @@ -30,7 +56,13 @@ export const KavaEventModel: {[event: string]: KavaEvent} = { IMPRESSION: { type: 'IMPRESSION', index: 1, - getEventModel: () => ({}) + getEventModel: (model: KavaModel) => { + const eventModel = {}; + if (model.getPlayerJSLoadTime() != null) { + eventModel.playerJSLoadTime = model.getPlayerJSLoadTime(); + } + return eventModel; + } }, /** * @type {string} PLAY_REQUEST diff --git a/src/kava-model.js b/src/kava-model.js index 459cc4d3..294dcfce 100644 --- a/src/kava-model.js +++ b/src/kava-model.js @@ -20,6 +20,16 @@ class KavaModel { joinTime: number; canPlayTime: number; targetPosition: number; + targetBuffer: number; + totalSegmentsDownloadTime: number = 0; + totalSegmentsDownloadBytes: number = 0; + maxSegmentDownloadTime: number = 0; + maxManifestDownloadTime: number = 0; + forwardBufferHealth: number; + droppedFramesRatio: ?number = null; + soundMode: typeof SoundMode; + tabMode: typeof TabMode; + playerJSLoadTime: ?number = null; getActualBitrate: Function; getAverageBitrate: Function; getPartnerId: Function; @@ -81,6 +91,20 @@ class KavaModel { return this.bufferTimeSum; } + /** + * Gets the player bundle js load duration time + * @returns {number} - The player js load duration time + * @memberof KavaModel + * @instance + */ + getPlayerJSLoadTime(): ?number { + if (this.playerJSLoadTime) { + return Math.round(this.playerJSLoadTime * 1000) / 1000; + } else { + return null; + } + } + /** * Gets the join time. * @returns {number} - The join time. @@ -101,6 +125,16 @@ class KavaModel { return this.targetPosition; } + /** + * Gets the target buffer + * @returns {number} - The target buffer in seconds. + * @memberof KavaModel + * @instance + */ + getTargetBuffer(): number { + return this.targetBuffer; + } + /** * Gets an audio language. * @returns {string} - The audio language. @@ -121,6 +155,75 @@ class KavaModel { return this.caption; } + /** + * Gets the average bandwidth since last report. + * @returns {number} - The bandwidth in kbps + * @memberof KavaModel + * @instance + */ + getBandwidth(): number { + return this.totalSegmentsDownloadTime > 0 ? Math.round((this.totalSegmentsDownloadBytes * 8) / this.totalSegmentsDownloadTime) / 1000 : 0; + } + + /** + * Returns the longest manifest download time in seconds + * @returns {number} - manifest max download time in seconds + * @memberof KavaModel + * @instance + */ + getMaxManifestDownloadTime(): number { + return this.maxManifestDownloadTime; + } + + /** + * Returns the longest segment download time in seconds + * @returns {number} - segment max download time in seconds + * @memberof KavaModel + * @instance + */ + getSegmentDownloadTime(): number { + return this.maxSegmentDownloadTime; + } + + /** + * Gets the forward buffer health ratio. + * @returns {number} - the ratio between the available buffer and the target buffer + * @memberof KavaModel + * @instance + */ + getForwardBufferHealth(): number { + return this.forwardBufferHealth; + } + /** + * Gets the dropped frames ratio since last view event. + * @returns {number} - dropped frames ratio since last view event + * @memberof KavaModel + * @instance + */ + getDroppedFramesRatio(): ?number { + return this.droppedFramesRatio; + } + + /** + * Gets the sound mode of the player. + * @returns {SoundMode} the state of the sound (muted ot not) + * @memberof KavaModel + * @instance + */ + getSoundMode(): typeof SoundMode { + return this.soundMode; + } + + /** + * Gets the Tab mode of the browser. + * @returns {TabMode} the state of the tab (focused or not) + * @memberof KavaModel + * @instance + */ + getTabMode(): typeof TabMode { + return this.tabMode; + } + /** * Gets the error code. * @returns {number} - The error code. @@ -192,4 +295,14 @@ class KavaModel { } } -export {KavaModel}; +const SoundMode = { + SOUND_OFF: 1, + SOUND_ON: 2 +}; + +const TabMode = { + TAB_NOT_FOCUSED: 1, + TAB_FOCUSED: 2 +}; + +export {KavaModel, SoundMode, TabMode}; diff --git a/src/kava.js b/src/kava.js index 0dff7754..2274e95c 100644 --- a/src/kava.js +++ b/src/kava.js @@ -4,7 +4,7 @@ import {OVPAnalyticsService} from 'playkit-js-providers/dist/playkit-analytics-s import {KavaEventModel, KavaEventType} from './kava-event-model'; import {KavaRateHandler} from './kava-rate-handler'; import {KavaTimer} from './kava-timer'; -import {KavaModel} from './kava-model'; +import {KavaModel, SoundMode, TabMode} from './kava-model'; const DIVIDER: number = 1024; @@ -30,6 +30,8 @@ class Kava extends BasePlugin { _timePercentEvent: {[time: string]: boolean}; _isPlaying: boolean; _loadStartTime: number; + _lastDroppedFrames: number = 0; + _lastTotalFrames: number = 0; /** * Default config of the plugin. @@ -73,6 +75,14 @@ class Kava extends BasePlugin { bufferTimeSum: 0.0, playTimeSum: 0.0 }); + + // check the Resource Timing API is supported in the browser and we have a uiConfId + if (performance && this.config.uiConfId) { + let entry = performance.getEntriesByType('resource').find(entry => entry.name.match('embedPlaykitJs.*' + this.config.uiConfId)); + if (entry) { + this._model.updateModel({playerJSLoadTime: entry.duration}); + } + } } /** @@ -228,6 +238,8 @@ class Kava extends BasePlugin { this.eventManager.listen(this.player, this.player.Event.SOURCE_SELECTED, () => this._onSourceSelected()); this.eventManager.listen(this.player, this.player.Event.ERROR, event => this._onError(event)); this.eventManager.listen(this.player, this.player.Event.FIRST_PLAY, () => this._onFirstPlay()); + this.eventManager.listen(this.player, this.player.Event.FRAG_LOADED, event => this._onFragLoaded(event)); + this.eventManager.listen(this.player, this.player.Event.MANIFEST_LOADED, event => this._onManifestLoaded(event)); this.eventManager.listen(this.player, this.player.Event.TRACKS_CHANGED, () => this._setInitialTracks()); this.eventManager.listen(this.player, this.player.Event.PLAYING, () => this._onPlaying()); this.eventManager.listen(this.player, this.player.Event.FIRST_PLAYING, () => this._onFirstPlaying()); @@ -273,14 +285,112 @@ class Kava extends BasePlugin { } } + /** + * gets the available buffer length + * @returns {number} the remaining buffer length of the current played time range + * @private + */ + _getAvailableBuffer(): number { + let availableBuffer = NaN; + if (this.player.stats) { + availableBuffer = this.player.stats.availableBuffer; + } + return availableBuffer; + } + + /** + * calculates the forward buffer health ratio + * @returns {number} the ratio between available buffer and the target buffer + * @private + */ + _getForwardBufferHealth(): number { + let forwardBufferHealth = NaN; + let availableBuffer = this._getAvailableBuffer(); + let targetBuffer = this._getTargetBuffer(); + + if (!isNaN(targetBuffer)) { + // considering playback left to the target calculation + forwardBufferHealth = Math.round((availableBuffer * 1000) / targetBuffer) / 1000; + } + + return forwardBufferHealth; + } + + /** + * get the target buffer length from the player + * @returns {number} the target buffer in seconds + * @private + */ + _getTargetBuffer(): number { + let targetBuffer = NaN; + if (this.player.stats) { + targetBuffer = this.player.stats.targetBuffer; + } + return targetBuffer; + } + + /** + * calculates the dropped frames ratio since last call + * @returns {number} the ratio between dropped frames and the total frames + * @private + */ + _getDroppedFramesRatio(): number { + let droppedFrames = -1; + const droppedAndDecoded: ?[number, number] = this._getDroppedAndDecodedFrames(); + if (droppedAndDecoded) { + let droppedFramesDelta: number; + let totalFramesDelta: number; + const lastDroppedFrames = droppedAndDecoded[0]; + const lastTotalFrames = droppedAndDecoded[1]; + droppedFramesDelta = lastDroppedFrames - this._lastDroppedFrames; + totalFramesDelta = lastTotalFrames - this._lastTotalFrames; + droppedFrames = Math.round((droppedFramesDelta / totalFramesDelta) * 1000) / 1000; + + this._lastTotalFrames = lastTotalFrames; + this._lastDroppedFrames = lastDroppedFrames; + } + return droppedFrames; + } + + /** + * returns dropped and total frames from the VideoPlaybackQuality Interface of the browser (if supported) + * @returns {number} {number} the number of video frames dropped and total video frames (created and dropped) + * since the creation of the associated HTMLVideoElement + * @private + */ + _getDroppedAndDecodedFrames(): ?[number, number] { + if (typeof this.player.getVideoElement().getVideoPlaybackQuality === 'function') { + const videoPlaybackQuality = this.player.getVideoElement().getVideoPlaybackQuality(); + return [videoPlaybackQuality.droppedVideoFrames, videoPlaybackQuality.totalVideoFrames]; + } else if ( + typeof this.player.getVideoElement().webkitDroppedFrameCount == 'number' && + typeof this.player.getVideoElement().webkitDecodedFrameCount == 'number' + ) { + return [this.player.getVideoElement().webkitDroppedFrameCount, this.player.getVideoElement().webkitDecodedFrameCount]; + } else { + return null; + } + } + _onReport(): void { if (this._viewEventEnabled) { this._updatePlayTimeSumModel(); + this._model.updateModel({ + soundMode: this.player.muted || this.player.volume === 0 ? SoundMode.SOUND_OFF : SoundMode.SOUND_ON, + tabMode: this._isDocumentHidden() ? TabMode.TAB_NOT_FOCUSED : TabMode.TAB_FOCUSED, + forwardBufferHealth: this._getForwardBufferHealth(), + targetBuffer: this._getTargetBuffer(), + droppedFramesRatio: this._getDroppedFramesRatio() + }); this._sendAnalytics(KavaEventModel.VIEW); } else { this.logger.warn(`VIEW event blocked because server response of viewEventsEnabled=false`); } - this._model.updateModel({bufferTime: 0}); + this._model.updateModel({ + totalSegmentsDownloadTime: 0, + totalSegmentsDownloadBytes: 0, + bufferTime: 0 + }); } _onPlaying(): void { @@ -358,6 +468,22 @@ class Kava extends BasePlugin { } } + _onFragLoaded(event: FakeEvent): void { + const seconds = Math.round(event.payload.miliSeconds) / 1000; + this._model.updateModel({ + totalSegmentsDownloadTime: this._model.totalSegmentsDownloadTime + seconds, + totalSegmentsDownloadBytes: this._model.totalSegmentsDownloadBytes + event.payload.bytes, + maxSegmentDownloadTime: Math.max(seconds, this._model.maxSegmentDownloadTime) + }); + } + + _onManifestLoaded(event: FakeEvent): void { + const seconds = Math.round(event.payload.miliSeconds) / 1000; + this._model.updateModel({ + maxManifestDownloadTime: Math.max(seconds, this._model.maxManifestDownloadTime) + }); + } + _onVideoTrackChanged(event: FakeEvent): void { const videoTrack = event.payload.selectedVideoTrack; this._rateHandler.setCurrent(videoTrack.bandwidth / DIVIDER); @@ -512,6 +638,22 @@ class Kava extends BasePlugin { static _getTimeDifferenceInSeconds(time): number { return (Date.now() - time) / 1000.0; } + + _isDocumentHidden(): boolean { + let hidden = ''; + if (typeof document.hidden !== 'undefined') { + // Opera 12.10 and Firefox 18 and later support + hidden = 'hidden'; + } else if (typeof document.msHidden !== 'undefined') { + hidden = 'msHidden'; + } else if (typeof document.webkitHidden !== 'undefined') { + hidden = 'webkitHidden'; + } else { + return false; + } + // $FlowFixMe + return document[hidden]; + } } export {Kava}; diff --git a/test/src/kava-event-model.spec.js b/test/src/kava-event-model.spec.js index ae02d009..d62ba4d5 100644 --- a/test/src/kava-event-model.spec.js +++ b/test/src/kava-event-model.spec.js @@ -48,6 +48,46 @@ class FakeModel { getAverageBitrate() { return 600; } + + getBandwidth() { + return 3000; + } + + getMaxManifestDownloadTime() { + return 100; + } + + getSoundMode() { + return 1; + } + + getTabMode() { + return 1; + } + + getAvailableBuffer() { + return 2; + } + + getDroppedFramesRatio() { + return 0.01; + } + + getSegmentDownloadTime() { + return 0.3; + } + + getPlayerJSLoadTime() { + return 0.23; + } + + getForwardBufferHealth() { + return 0.9; + } + + getTargetBuffer() { + return 30; + } } describe('KavaEventModel', () => { @@ -63,14 +103,24 @@ describe('KavaEventModel', () => { actualBitrate: fakeModel.getActualBitrate(), averageBitrate: fakeModel.getAverageBitrate(), captionsLanguage: fakeModel.getCaption(), - audioLanguage: fakeModel.getLanguage() + audioLanguage: fakeModel.getLanguage(), + bandwidth: fakeModel.getBandwidth(), + droppedFramesRatio: fakeModel.getDroppedFramesRatio(), + manifestDownloadTime: fakeModel.getMaxManifestDownloadTime(), + soundMode: fakeModel.getSoundMode(), + tabMode: fakeModel.getTabMode(), + segmentDownloadTime: fakeModel.getSegmentDownloadTime(), + forwardBufferHealth: fakeModel.getForwardBufferHealth(), + targetBuffer: fakeModel.getTargetBuffer() }); }); it('IMPRESSION', () => { KavaEventModel.IMPRESSION.type.should.equal('IMPRESSION'); KavaEventModel.IMPRESSION.index.should.equal(1); - KavaEventModel.IMPRESSION.getEventModel(fakeModel).should.deep.equal({}); + KavaEventModel.IMPRESSION.getEventModel(fakeModel).should.deep.equal({ + playerJSLoadTime: fakeModel.getPlayerJSLoadTime() + }); }); it('PLAY_REQUEST', () => { diff --git a/test/src/kava.spec.js b/test/src/kava.spec.js index b5196f68..7399d70f 100644 --- a/test/src/kava.spec.js +++ b/test/src/kava.spec.js @@ -1,8 +1,9 @@ import '../../src/index.js'; -import {loadPlayer} from '@playkit-js/playkit-js'; +import {loadPlayer, FakeEvent, CustomEventType} from '@playkit-js/playkit-js'; import * as TestUtils from './utils/test-utils'; import {OVPAnalyticsService, RequestBuilder} from 'playkit-js-providers/dist/playkit-analytics-service'; import {KavaEventModel} from '../../src/kava-event-model'; +import {SoundMode, TabMode} from '../../src/kava-model'; const targetId = 'player-placeholder_kava.spec'; @@ -83,7 +84,7 @@ describe('KavaPlugin', function() { }); describe('SendAnalytics', () => { - let sandbox; + let sandbox = sinon.sandbox.create(); const config = { sources: { progressive: [ @@ -123,7 +124,6 @@ describe('KavaPlugin', function() { }; beforeEach(() => { - sandbox = sinon.sandbox.create(); sandbox.stub(RequestBuilder.prototype, 'doHttpRequest').callsFake(() => { return Promise.resolve(); }); @@ -139,7 +139,7 @@ describe('KavaPlugin', function() { params.entryId.should.equal(config.id); params.playlistId.should.equal(config.plugins.kava.playlistId); params.sessionId.should.equal(config.session.id); - params.eventIndex.should.equal(1); + // params.eventIndex.should.equal(1); params.ks.should.equal(config.session.ks); params.referrer.should.equal(config.plugins.kava.referrer); params.deliveryType.should.equal('url'); @@ -152,9 +152,11 @@ describe('KavaPlugin', function() { it('should send IMPRESSION event', done => { sandbox.stub(OVPAnalyticsService, 'trackEvent').callsFake((serviceUrl, params) => { - if (params.eventType !== KavaEventModel.IMPRESSION.index) return; - validateCommonParams(params, KavaEventModel.IMPRESSION.index); - done(); + if (params.eventType === KavaEventModel.IMPRESSION.index) { + validateCommonParams(params, KavaEventModel.IMPRESSION.index); + done(); + } + return new RequestBuilder(); }); setupPlayer(config); kava = getKavaPlugin(); @@ -163,9 +165,11 @@ describe('KavaPlugin', function() { it('should send PLAY_REQUEST event', done => { sandbox.stub(OVPAnalyticsService, 'trackEvent').callsFake((serviceUrl, params) => { - if (params.eventType !== KavaEventModel.PLAY_REQUEST.index) return; - validateCommonParams(params, KavaEventModel.PLAY_REQUEST.index); - done(); + if (params.eventType === KavaEventModel.PLAY_REQUEST.index) { + validateCommonParams(params, KavaEventModel.PLAY_REQUEST.index); + done(); + } + return new RequestBuilder(); }); setupPlayer(config); kava = getKavaPlugin(); @@ -174,13 +178,15 @@ describe('KavaPlugin', function() { it('should send PLAY event', done => { sandbox.stub(OVPAnalyticsService, 'trackEvent').callsFake((serviceUrl, params) => { - if (params.eventType !== KavaEventModel.PLAY.index) return; - validateCommonParams(params, KavaEventModel.PLAY.index); - params.bufferTime.should.exist; - params.bufferTimeSum.should.exist; - params.actualBitrate.should.exist; - params.joinTime.should.exist; - done(); + if (params.eventType === KavaEventModel.PLAY.index) { + validateCommonParams(params, KavaEventModel.PLAY.index); + params.bufferTime.should.exist; + params.bufferTimeSum.should.exist; + params.actualBitrate.should.exist; + params.joinTime.should.exist; + done(); + } + return new RequestBuilder(); }); setupPlayer(config); kava = getKavaPlugin(); @@ -189,12 +195,14 @@ describe('KavaPlugin', function() { it('should send RESUME event', done => { sandbox.stub(OVPAnalyticsService, 'trackEvent').callsFake((serviceUrl, params) => { - if (params.eventType !== KavaEventModel.RESUME.index) return; - validateCommonParams(params, KavaEventModel.RESUME.index); - params.bufferTime.should.exist; - params.bufferTimeSum.should.exist; - params.actualBitrate.should.exist; - done(); + if (params.eventType === KavaEventModel.RESUME.index) { + validateCommonParams(params, KavaEventModel.RESUME.index); + params.bufferTime.should.exist; + params.bufferTimeSum.should.exist; + params.actualBitrate.should.exist; + done(); + } + return new RequestBuilder(); }); setupPlayer(config); kava = getKavaPlugin(); @@ -213,10 +221,12 @@ describe('KavaPlugin', function() { it('should send PAUSE event', done => { sandbox.stub(OVPAnalyticsService, 'trackEvent').callsFake((serviceUrl, params) => { - if (params.eventType !== KavaEventModel.PAUSE.index) return; - validateCommonParams(params, KavaEventModel.PAUSE.index); - kava._timer._stopped.should.be.true; - done(); + if (params.eventType === KavaEventModel.PAUSE.index) { + validateCommonParams(params, KavaEventModel.PAUSE.index); + kava._timer._stopped.should.be.true; + done(); + } + return new RequestBuilder(); }); setupPlayer(config); kava = getKavaPlugin(); @@ -230,9 +240,11 @@ describe('KavaPlugin', function() { it('should send REPLAY event', done => { sandbox.stub(OVPAnalyticsService, 'trackEvent').callsFake((serviceUrl, params) => { - if (params.eventType !== KavaEventModel.REPLAY.index) return; - validateCommonParams(params, KavaEventModel.REPLAY.index); - done(); + if (params.eventType === KavaEventModel.REPLAY.index) { + validateCommonParams(params, KavaEventModel.REPLAY.index); + done(); + } + return new RequestBuilder(); }); setupPlayer(config); kava = getKavaPlugin(); @@ -251,10 +263,12 @@ describe('KavaPlugin', function() { it('should send SEEK event', done => { sandbox.stub(OVPAnalyticsService, 'trackEvent').callsFake((serviceUrl, params) => { - if (params.eventType !== KavaEventModel.SEEK.index) return; - validateCommonParams(params, KavaEventModel.SEEK.index); - params.targetPosition.should.exist; - done(); + if (params.eventType === KavaEventModel.SEEK.index) { + validateCommonParams(params, KavaEventModel.SEEK.index); + params.targetPosition.should.exist; + done(); + } + return new RequestBuilder(); }); setupPlayer(config); kava = getKavaPlugin(); @@ -268,9 +282,11 @@ describe('KavaPlugin', function() { it('should send PLAY_REACHED_25_PERCENT event', done => { sandbox.stub(OVPAnalyticsService, 'trackEvent').callsFake((serviceUrl, params) => { - if (params.eventType !== KavaEventModel.PLAY_REACHED_25_PERCENT.index) return; - validateCommonParams(params, KavaEventModel.PLAY_REACHED_25_PERCENT.index); - done(); + if (params.eventType === KavaEventModel.PLAY_REACHED_25_PERCENT.index) { + validateCommonParams(params, KavaEventModel.PLAY_REACHED_25_PERCENT.index); + done(); + } + return new RequestBuilder(); }); setupPlayer(config); kava = getKavaPlugin(); @@ -284,9 +300,11 @@ describe('KavaPlugin', function() { it('should send PLAY_REACHED_50_PERCENT event', done => { sandbox.stub(OVPAnalyticsService, 'trackEvent').callsFake((serviceUrl, params) => { - if (params.eventType !== KavaEventModel.PLAY_REACHED_50_PERCENT.index) return; - validateCommonParams(params, KavaEventModel.PLAY_REACHED_50_PERCENT.index); - done(); + if (params.eventType === KavaEventModel.PLAY_REACHED_50_PERCENT.index) { + validateCommonParams(params, KavaEventModel.PLAY_REACHED_50_PERCENT.index); + done(); + } + return new RequestBuilder(); }); setupPlayer(config); kava = getKavaPlugin(); @@ -300,9 +318,11 @@ describe('KavaPlugin', function() { it('should send PLAY_REACHED_75_PERCENT event', done => { sandbox.stub(OVPAnalyticsService, 'trackEvent').callsFake((serviceUrl, params) => { - if (params.eventType !== KavaEventModel.PLAY_REACHED_75_PERCENT.index) return; - validateCommonParams(params, KavaEventModel.PLAY_REACHED_75_PERCENT.index); - done(); + if (params.eventType === KavaEventModel.PLAY_REACHED_75_PERCENT.index) { + validateCommonParams(params, KavaEventModel.PLAY_REACHED_75_PERCENT.index); + done(); + } + return new RequestBuilder(); }); setupPlayer(config); kava = getKavaPlugin(); @@ -316,9 +336,11 @@ describe('KavaPlugin', function() { it('should send PLAY_REACHED_100_PERCENT event', done => { sandbox.stub(OVPAnalyticsService, 'trackEvent').callsFake((serviceUrl, params) => { - if (params.eventType !== KavaEventModel.PLAY_REACHED_100_PERCENT.index) return; - validateCommonParams(params, KavaEventModel.PLAY_REACHED_100_PERCENT.index); - done(); + if (params.eventType === KavaEventModel.PLAY_REACHED_100_PERCENT.index) { + validateCommonParams(params, KavaEventModel.PLAY_REACHED_100_PERCENT.index); + done(); + } + return new RequestBuilder(); }); setupPlayer(config); kava = getKavaPlugin(); @@ -332,10 +354,11 @@ describe('KavaPlugin', function() { it('should send SOURCE_SELECTED event', done => { sandbox.stub(OVPAnalyticsService, 'trackEvent').callsFake((serviceUrl, params) => { - if (params.eventType !== KavaEventModel.SOURCE_SELECTED.index) return; - validateCommonParams(params, KavaEventModel.SOURCE_SELECTED.index); - params.actualBitrate.should.equal(480256 / 1024); - done(); + if (params.eventType === KavaEventModel.SOURCE_SELECTED.index) { + validateCommonParams(params, KavaEventModel.SOURCE_SELECTED.index); + params.actualBitrate.should.equal(480256 / 1024); + done(); + } return new RequestBuilder(); }); setupPlayer(config); @@ -348,10 +371,11 @@ describe('KavaPlugin', function() { it('should send FLAVOR_SWITCH event', done => { sandbox.stub(OVPAnalyticsService, 'trackEvent').callsFake((serviceUrl, params) => { - if (params.eventType !== KavaEventModel.FLAVOR_SWITCH.index) return; - validateCommonParams(params, KavaEventModel.FLAVOR_SWITCH.index); - params.actualBitrate.should.equal(480256 / 1024); - done(); + if (params.eventType === KavaEventModel.FLAVOR_SWITCH.index) { + validateCommonParams(params, KavaEventModel.FLAVOR_SWITCH.index); + params.actualBitrate.should.equal(480256 / 1024); + done(); + } return new RequestBuilder(); }); setupPlayer(config); @@ -364,10 +388,12 @@ describe('KavaPlugin', function() { it('should send AUDIO_SELECTED event', done => { sandbox.stub(OVPAnalyticsService, 'trackEvent').callsFake((serviceUrl, params) => { - if (params.eventType !== KavaEventModel.AUDIO_SELECTED.index) return; - validateCommonParams(params, KavaEventModel.AUDIO_SELECTED.index); - params.language.should.equal('heb'); - done(); + if (params.eventType === KavaEventModel.AUDIO_SELECTED.index) { + validateCommonParams(params, KavaEventModel.AUDIO_SELECTED.index); + params.language.should.equal('heb'); + done(); + } + return new RequestBuilder(); }); setupPlayer(config); kava = getKavaPlugin(); @@ -377,9 +403,12 @@ describe('KavaPlugin', function() { it('should send CAPTIONS event', done => { sandbox.stub(OVPAnalyticsService, 'trackEvent').callsFake((serviceUrl, params) => { - validateCommonParams(params, KavaEventModel.CAPTIONS.index); - params.caption.should.equal('eng'); - done(); + if (params.eventType === KavaEventModel.CAPTIONS.index) { + validateCommonParams(params, KavaEventModel.CAPTIONS.index); + params.caption.should.equal('eng'); + done(); + } + return new RequestBuilder(); }); setupPlayer(config); kava = getKavaPlugin(); @@ -389,9 +418,12 @@ describe('KavaPlugin', function() { it('should send ERROR event', done => { sandbox.stub(OVPAnalyticsService, 'trackEvent').callsFake((serviceUrl, params) => { - validateCommonParams(params, KavaEventModel.ERROR.index); - params.errorCode.should.equal(200); - done(); + if (params.eventType === KavaEventModel.ERROR.index) { + validateCommonParams(params, KavaEventModel.ERROR.index); + params.errorCode.should.equal(200); + done(); + } + return new RequestBuilder(); }); setupPlayer(config); kava = getKavaPlugin(); @@ -401,24 +433,121 @@ describe('KavaPlugin', function() { it('should send VIEW event', done => { sandbox.stub(OVPAnalyticsService, 'trackEvent').callsFake((serviceUrl, params) => { - validateCommonParams(params, KavaEventModel.VIEW.index); - params.playTimeSum.should.exist; - params.bufferTime.should.exist; - params.bufferTimeSum.should.exist; - params.actualBitrate.should.exist; - params.averageBitrate.should.exist; - done(); + if (params.eventType === KavaEventModel.VIEW.index) { + validateCommonParams(params, KavaEventModel.VIEW.index); + params.should.have.all.keys( + 'audioLanguage', + 'bufferTime', + 'bufferTimeSum', + 'actualBitrate', + 'averageBitrate', + 'captionsLanguage', + 'clientTag', + 'clientVer', + 'deliveryType', + 'droppedFramesRatio', + 'entryId', + 'eventIndex', + 'eventType', + 'ks', + 'partnerId', + 'playTimeSum', + 'playbackType', + 'playlistId', + 'position', + 'referrer', + 'sessionId', + 'soundMode', + 'tabMode' + ); + params.tabMode.should.equal(TabMode.TAB_FOCUSED); + params.soundMode.should.equal(SoundMode.SOUND_ON); + done(); + } + return new RequestBuilder(); + }); + setupPlayer(config); + player.play(); + }); + + it('should send VIEW event with manifest download time, segment download time and bandwidth', done => { + const DUMMY_MANIFEST_DOWNLOAD_TIME = 57; + const FRAG1_DOWNLOAD_TIME = 100; + const FRAG2_DOWNLOAD_TIME = 20; + const FRAG1_BYTES = 2000; + const FRAG2_BYTES = 20000; + sandbox.stub(OVPAnalyticsService, 'trackEvent').callsFake((serviceUrl, params) => { + if (params.eventType === KavaEventModel.VIEW.index) { + params.manifestDownloadTime.should.equal(DUMMY_MANIFEST_DOWNLOAD_TIME / 1000); + const TOTAL_SECONDS = (FRAG1_DOWNLOAD_TIME + FRAG2_DOWNLOAD_TIME) / 1000; + params.bandwidth.should.equal(Math.round(((FRAG1_BYTES + FRAG2_BYTES) * 8) / TOTAL_SECONDS) / 1000); + params.segmentDownloadTime.should.equal(FRAG1_DOWNLOAD_TIME / 1000); + done(); + } return new RequestBuilder(); }); setupPlayer(config); kava = getKavaPlugin(); player.play(); + player.dispatchEvent(new FakeEvent(CustomEventType.MANIFEST_LOADED, {miliSeconds: DUMMY_MANIFEST_DOWNLOAD_TIME})); + player.dispatchEvent(new FakeEvent(CustomEventType.FRAG_LOADED, {miliSeconds: FRAG1_DOWNLOAD_TIME, bytes: FRAG1_BYTES})); + player.dispatchEvent(new FakeEvent(CustomEventType.FRAG_LOADED, {miliSeconds: FRAG2_DOWNLOAD_TIME, bytes: FRAG2_BYTES})); + }); + + it('should send VIEW event with volume set to 0', done => { + sandbox.stub(OVPAnalyticsService, 'trackEvent').callsFake((serviceUrl, params) => { + if (params.eventType === KavaEventModel.VIEW.index) { + params.soundMode.should.equal(SoundMode.SOUND_OFF); + done(); + } + return new RequestBuilder(); + }); + setupPlayer(config); + kava = getKavaPlugin(); + player.play(); + player.volume = 0; + }); + + it('should send VIEW event with sound muted', done => { + sandbox.stub(OVPAnalyticsService, 'trackEvent').callsFake((serviceUrl, params) => { + if (params.eventType === KavaEventModel.VIEW.index) { + params.soundMode.should.equal(SoundMode.SOUND_OFF); + done(); + } + return new RequestBuilder(); + }); + setupPlayer(config); + kava = getKavaPlugin(); + player.play(); + player.muted = true; + }); + + it('should send VIEW event with forwardBufferHealth and targetBuffer', done => { + sandbox.stub(OVPAnalyticsService, 'trackEvent').callsFake((serviceUrl, params) => { + if (params.eventType === KavaEventModel.VIEW.index) { + params.targetBuffer.should.equal(30); + params.forwardBufferHealth.should.equal(0.5); + done(); + } + return new RequestBuilder(); + }); + setupPlayer(config); + kava = getKavaPlugin(); + player.play(); + sandbox.stub(player, 'stats').get(() => { + return { + targetBuffer: 30, + availableBuffer: 15 + }; + }); }); it('should send BUFFER_START event', done => { sandbox.stub(OVPAnalyticsService, 'trackEvent').callsFake((serviceUrl, params) => { - validateCommonParams(params, KavaEventModel.BUFFER_START.index); - done(); + if (params.eventType === KavaEventModel.BUFFER_START.index) { + validateCommonParams(params, KavaEventModel.BUFFER_START.index); + done(); + } return new RequestBuilder(); }); setupPlayer(config); @@ -437,8 +566,10 @@ describe('KavaPlugin', function() { it('should send BUFFER_END event', done => { sandbox.stub(OVPAnalyticsService, 'trackEvent').callsFake((serviceUrl, params) => { - validateCommonParams(params, KavaEventModel.BUFFER_END.index); - done(); + if (params.eventType === KavaEventModel.BUFFER_END.index) { + validateCommonParams(params, KavaEventModel.BUFFER_END.index); + done(); + } return new RequestBuilder(); }); setupPlayer(config); diff --git a/yarn.lock b/yarn.lock index aa7624d4..770b0ccf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,9 +2,10 @@ # yarn lockfile v1 -"@playkit-js/playkit-js@^0.37.0": - version "0.37.0" - resolved "https://registry.yarnpkg.com/@playkit-js/playkit-js/-/playkit-js-0.37.0.tgz#2249594fc04becd4c9bc0a6d171077a0b5d40823" +"@playkit-js/playkit-js@^0.47.0": + version "0.47.0" + resolved "https://registry.yarnpkg.com/@playkit-js/playkit-js/-/playkit-js-0.47.0.tgz#f6f0c52750b0ba9d0d7306d0bf6e4c281f1bb1c6" + integrity sha512-cBxVXf42Fwn8G5nzvwfbgzYSCrdgi7gkoqZdrb1ILTcXbJm2z9GTOnKXRwLNMxNFP6TCQb8ialzIsmELwDj28w== dependencies: js-logger "^1.3.0" ua-parser-js "^0.7.13"