diff --git a/CHANGELOG.md b/CHANGELOG.md index 462e6c0..f9edbe8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] +### Fixed +- Delayed buffering reporting on CSAI events +- Reporting of ad break ended for SSAI ads ## [6.1.1] - 2024-12-19 ### Fixed diff --git a/spec/helper/MockHelper.ts b/spec/helper/MockHelper.ts index 2ec7abb..510b424 100644 --- a/spec/helper/MockHelper.ts +++ b/spec/helper/MockHelper.ts @@ -222,9 +222,7 @@ export namespace MockHelper { } }), subtitles: { - list: jest.fn(() => { - return []; - }), + list: jest.fn(() => []), }, on: (eventType: PlayerEvent, callback: PlayerEventCallback) => playerEventHelper.on(eventType, callback), off: (eventType: PlayerEvent, callback: PlayerEventCallback) => playerEventHelper.off(eventType, callback), diff --git a/spec/tests/ConvivaAnalytics.spec.ts b/spec/tests/ConvivaAnalytics.spec.ts index e4ebf48..263a77b 100644 --- a/spec/tests/ConvivaAnalytics.spec.ts +++ b/spec/tests/ConvivaAnalytics.spec.ts @@ -503,18 +503,21 @@ describe(ConvivaAnalytics, () => { }) it('reports ad finished', () => { + playerEventHelper.fireAdBreakStartedEvent(0); playerEventHelper.fireAdFinishedEvent(); expect(MockHelper.latestAdAnalytics.reportAdEnded).toHaveBeenCalled(); }) it('reports ad skipped', () => { + playerEventHelper.fireAdBreakStartedEvent(0); playerEventHelper.fireAdSkippedEvent(); expect(MockHelper.latestAdAnalytics.reportAdSkipped).toHaveBeenCalled(); }) it('reports ad error', () => { + playerEventHelper.fireAdBreakStartedEvent(0); playerEventHelper.fireAdErrorEvent(); expect(MockHelper.latestAdAnalytics.reportAdError).toHaveBeenCalledWith( @@ -524,12 +527,13 @@ describe(ConvivaAnalytics, () => { }) it('reports ad break ended when restoring content', () => { + playerEventHelper.fireAdBreakStartedEvent(0); playerEventHelper.fireRestoringContentEvent(); expect(MockHelper.latestVideoAnalytics.reportAdBreakEnded).toHaveBeenCalledTimes(1); }); - it('reports the playback metric on ad finished', () => { + it('reports the playback metric on ad break finished', () => { (MockHelper.latestVideoAnalytics.reportPlaybackMetric as jest.Mock).mockReset(); playerEventHelper.fireAdBreakFinishedEvent(); diff --git a/spec/tests/ConvivaAnalyticsTracker.spec.ts b/spec/tests/ConvivaAnalyticsTracker.spec.ts index 0011cf3..037dd09 100644 --- a/spec/tests/ConvivaAnalyticsTracker.spec.ts +++ b/spec/tests/ConvivaAnalyticsTracker.spec.ts @@ -77,83 +77,62 @@ describe(ConvivaAnalyticsTracker, () => { expect(invokedTimesAfter).toBe(invokedTimesBefore); }) - it('should report ad break ended on RestoringContent event', () => { - const { playerMock, playerEventHelper } = MockHelper.createPlayerMock(); - const convivaAnalyticsTracker = new ConvivaAnalyticsTracker('test-key'); - - convivaAnalyticsTracker.attachPlayer(playerMock); - playerEventHelper.firePlayEvent(); - convivaAnalyticsTracker.trackRestoringContent(); - - expect(MockHelper.latestVideoAnalytics.reportAdBreakEnded).toHaveBeenCalledTimes(1); - }); - - it('should not report the player state on AdBreakFinished events if there is still an active ad', () => { - const { playerMock, playerEventHelper } = MockHelper.createPlayerMock(); - const convivaAnalyticsTracker = new ConvivaAnalyticsTracker('test-key'); - - convivaAnalyticsTracker.attachPlayer(playerMock); - playerEventHelper.fireAdBreakFinishedEvent(); - convivaAnalyticsTracker.trackAdBreakFinished(); - - expect(MockHelper.latestVideoAnalytics.reportPlaybackMetric).not.toHaveBeenCalled(); - }); - - it('should report the player state on AdBreakFinished events if there is no active ad', () => { - const { playerMock, playerEventHelper } = MockHelper.createPlayerMock(); - const convivaAnalyticsTracker = new ConvivaAnalyticsTracker('test-key'); - - convivaAnalyticsTracker.attachPlayer(playerMock); - playerEventHelper.firePlayEvent(); - convivaAnalyticsTracker.trackRestoringContent(); - convivaAnalyticsTracker.trackAdBreakFinished(); + describe('CSAI', () => { + let convivaAnalyticsTracker: ConvivaAnalyticsTracker; - expect(MockHelper.latestVideoAnalytics.reportPlaybackMetric).toHaveBeenCalled(); + beforeEach(() => { + const { playerMock, playerEventHelper } = MockHelper.createPlayerMock(); + convivaAnalyticsTracker = new ConvivaAnalyticsTracker('test-key'); + convivaAnalyticsTracker.attachPlayer(playerMock); + playerEventHelper.firePlayEvent(); + jest.spyOn(playerMock, 'isPlaying').mockReturnValue(true); + }); + + it('should not report ad break ended on AdBreakFinished if there is still an active ad', () => { + convivaAnalyticsTracker.trackAdBreakStarted(Conviva.Constants.AdType.CLIENT_SIDE); + convivaAnalyticsTracker.trackAdBreakFinished(Conviva.Constants.AdType.CLIENT_SIDE); + + expect(MockHelper.latestVideoAnalytics.reportAdBreakEnded).toHaveBeenCalledTimes(0); + expect(MockHelper.latestVideoAnalytics.reportPlaybackMetric).not.toHaveBeenCalledWith(Conviva.Constants.Playback.PLAYER_STATE, Conviva.Constants.PlayerState.PLAYING); + }); + + it('should report ad break ended if AdBreakFinished is preceeded by RestoringContent event', () => { + convivaAnalyticsTracker.trackAdBreakStarted(Conviva.Constants.AdType.CLIENT_SIDE); + convivaAnalyticsTracker.trackRestoringContent(); + convivaAnalyticsTracker.trackAdBreakFinished(Conviva.Constants.AdType.CLIENT_SIDE); + + expect(MockHelper.latestVideoAnalytics.reportAdBreakEnded).toHaveBeenCalledTimes(1); + expect(MockHelper.latestVideoAnalytics.reportPlaybackMetric).toHaveBeenCalledWith(Conviva.Constants.Playback.PLAYER_STATE, Conviva.Constants.PlayerState.PLAYING); + }); + + it('should not report ad break ended multiple times', () => { + convivaAnalyticsTracker.trackAdBreakStarted(Conviva.Constants.AdType.CLIENT_SIDE); + convivaAnalyticsTracker.trackRestoringContent(); + convivaAnalyticsTracker.trackRestoringContent(); + convivaAnalyticsTracker.trackRestoringContent(); + + expect(MockHelper.latestVideoAnalytics.reportAdBreakEnded).toHaveBeenCalledTimes(1); + }); }); - describe('trackPlaybackStateChanged', () => { + describe('SSAI', () => { let convivaAnalyticsTracker: ConvivaAnalyticsTracker; beforeEach(() => { + const { playerMock, playerEventHelper } = MockHelper.createPlayerMock(); convivaAnalyticsTracker = new ConvivaAnalyticsTracker('test-key'); - - const {playerMock} = MockHelper.createPlayerMock(); convivaAnalyticsTracker.attachPlayer(playerMock); - jest.spyOn(playerMock, 'getSource').mockImplementation(() => ({ title: 'test-title' })); - convivaAnalyticsTracker.initializeSession(); - }) - - test.each([ - PlayerEvent.Play, - PlayerEvent.Seek, - PlayerEvent.TimeShift, - PlayerEvent.AdBreakStarted, - PlayerEvent.AdFinished, - ])('should start timer for stalling when reported player event is %s', (event) => { - const stallTrackingStartTimeoutSpy = jest.spyOn(convivaAnalyticsTracker['stallTrackingTimeout'], 'start'); - - convivaAnalyticsTracker.trackPlaybackStateChanged({ type: event } as PlayerEventBase); - - expect(stallTrackingStartTimeoutSpy).toHaveBeenCalled(); - }) - - test.each([ - PlayerEvent.StallStarted, - PlayerEvent.Playing, - PlayerEvent.Paused, - PlayerEvent.Seeked, - PlayerEvent.TimeShifted, - PlayerEvent.StallEnded, - PlayerEvent.PlaybackFinished, - PlayerEvent.AdStarted, - ])('should clear timer for stalling when reported player event is %s', (event) => { - const stallTrackingStopTimeoutSpy = jest.spyOn(convivaAnalyticsTracker['stallTrackingTimeout'], 'clear'); - - convivaAnalyticsTracker.trackPlaybackStateChanged({ type: event } as PlayerEventBase); - - expect(stallTrackingStopTimeoutSpy).toHaveBeenCalled(); - }) - }) + playerEventHelper.firePlayEvent(); + jest.spyOn(playerMock, 'isPlaying').mockReturnValue(true); + }); + + it('should report the player state on AdBreakFinished when there is an active ad', () => { + convivaAnalyticsTracker.trackAdBreakStarted(Conviva.Constants.AdType.SERVER_SIDE); + convivaAnalyticsTracker.trackAdBreakFinished(Conviva.Constants.AdType.SERVER_SIDE); + + expect(MockHelper.latestVideoAnalytics.reportPlaybackMetric).toHaveBeenCalledWith(Conviva.Constants.Playback.PLAYER_STATE, Conviva.Constants.PlayerState.PLAYING); + }); + }); }) const getInvokedTimes = (mock: unknown) => { diff --git a/spec/tests/PlayerEvents.spec.ts b/spec/tests/PlayerEvents.spec.ts index 2764eea..e03d745 100644 --- a/spec/tests/PlayerEvents.spec.ts +++ b/spec/tests/PlayerEvents.spec.ts @@ -1,22 +1,27 @@ import { MockHelper, PlayerEventHelper } from '../helper/MockHelper'; import { ConvivaAnalytics } from '../../src/ts'; import * as Conviva from '@convivainc/conviva-js-coresdk'; -import { PlayerAPI } from 'bitmovin-player'; +import { AdEvent, PlayerAPI } from 'bitmovin-player'; import { ConvivaAnalyticsTracker } from '../../src/ts/ConvivaAnalyticsTracker'; +import { PlayerEvent } from '../helper/PlayerEvent'; jest.mock('@convivainc/conviva-js-coresdk', () => { const { MockHelper } = jest.requireActual('../helper/MockHelper'); return MockHelper.createConvivaMock(); }); +const PlayerState = Conviva.Constants.PlayerState; +const PLAYER_STATE = Conviva.Constants.Playback.PLAYER_STATE; + describe('player event tests', () => { let playerMock: PlayerAPI; let playerEventHelper: PlayerEventHelper + let convivaAnalytics: ConvivaAnalytics; beforeEach(() => { ({ playerMock, playerEventHelper } = MockHelper.createPlayerMock()); - new ConvivaAnalytics(playerMock, 'TEST-KEY'); + convivaAnalytics = new ConvivaAnalytics(playerMock, 'TEST-KEY'); }); describe('player event handling', () => { @@ -39,41 +44,96 @@ describe('player event tests', () => { playerEventHelper.firePlayEvent(); }); - it('on playing', () => { - jest.spyOn(playerMock, 'isPaused').mockReturnValue(false); - jest.spyOn(playerMock, 'isPlaying').mockReturnValue(true); - playerEventHelper.firePlayingEvent(); - - expect(MockHelper.latestVideoAnalytics.reportPlaybackMetric).toHaveBeenCalledWith( - Conviva.Constants.Playback.PLAYER_STATE, - Conviva.Constants.PlayerState.PLAYING, - ); + test.each` + event | isAdActive | expectedPlayerState + ${PlayerEvent.Playing} | ${false} | ${PlayerState.PLAYING} + ${PlayerEvent.Playing} | ${true} | ${PlayerState.PLAYING} + ${PlayerEvent.Paused} | ${false} | ${PlayerState.PAUSED} + ${PlayerEvent.Paused} | ${true} | ${PlayerState.PAUSED} + ${PlayerEvent.StallStarted} | ${false} | ${PlayerState.BUFFERING} + ${PlayerEvent.StallStarted} | ${true} | ${PlayerState.BUFFERING} + ${PlayerEvent.StallEnded} | ${false} | ${PlayerState.PLAYING} + ${PlayerEvent.StallEnded} | ${true} | ${PlayerState.PLAYING} + ${PlayerEvent.Seeked} | ${false} | ${PlayerState.PLAYING} + ${PlayerEvent.Seeked} | ${true} | ${PlayerState.PLAYING} + ${PlayerEvent.TimeShifted} | ${false} | ${PlayerState.PLAYING} + ${PlayerEvent.TimeShifted} | ${true} | ${PlayerState.PLAYING} + `('should report player state $expectedPlayerState on ad metric $isAdActive after event $event', ({ event, isAdActive, expectedPlayerState }) => { + jest.spyOn(playerMock, 'isPlaying').mockReturnValue(expectedPlayerState === PlayerState.PLAYING); + jest.spyOn(playerMock, 'isPaused').mockReturnValue(expectedPlayerState === PlayerState.PAUSED); + + const reportAdMetricSpy = jest.spyOn(MockHelper.latestAdAnalytics, 'reportAdMetric'); + const reportPlaybackMetricSpy = jest.spyOn(MockHelper.latestVideoAnalytics, 'reportPlaybackMetric') + const mockAdData: AdEvent = { ad: { id: 'test-ad-id', data: {} } } as AdEvent; + + if (isAdActive) { + convivaAnalytics['convivaAnalyticsTracker'].trackAdBreakStarted(Conviva.Constants.AdType.CLIENT_SIDE); + } + playerEventHelper.fireEvent({ time: 0, timestamp: Date.now(), type: event, ...mockAdData }); + + if (isAdActive) { + expect(reportAdMetricSpy).toHaveBeenLastCalledWith(PLAYER_STATE, expectedPlayerState); + expect(reportPlaybackMetricSpy).not.toHaveBeenLastCalledWith(PLAYER_STATE, expectedPlayerState); + } else { + expect(reportAdMetricSpy).not.toHaveBeenLastCalledWith(PLAYER_STATE, expectedPlayerState); + expect(reportPlaybackMetricSpy).toHaveBeenLastCalledWith(PLAYER_STATE, expectedPlayerState); + } }); - it('on pause', () => { - jest.spyOn(playerMock, 'isPlaying').mockReturnValue(false); - jest.spyOn(playerMock, 'isPaused').mockReturnValue(true); - playerEventHelper.firePauseEvent(); - - expect(MockHelper.latestVideoAnalytics.reportPlaybackMetric).toHaveBeenCalledWith( - Conviva.Constants.Playback.PLAYER_STATE, - Conviva.Constants.PlayerState.PAUSED, - ); + test.each` + event | isAdActive | expectedAdMetric | expectedPlaybackMetric + ${PlayerEvent.AdBreakStarted} | ${false} | ${PlayerState.BUFFERING} | ${undefined} + ${PlayerEvent.AdBreakStarted} | ${true} | ${PlayerState.BUFFERING} | ${undefined} + ${PlayerEvent.AdStarted} | ${false} | ${undefined} | ${undefined} + ${PlayerEvent.AdStarted} | ${true} | ${PlayerState.PLAYING} | ${undefined} + ${PlayerEvent.AdError} | ${false} | ${undefined} | ${undefined} + ${PlayerEvent.AdError} | ${true} | ${undefined} | ${undefined} + ${PlayerEvent.AdSkipped} | ${false} | ${undefined} | ${undefined} + ${PlayerEvent.AdSkipped} | ${true} | ${undefined} | ${undefined} + ${PlayerEvent.AdFinished} | ${false} | ${undefined} | ${undefined} + ${PlayerEvent.AdFinished} | ${true} | ${PlayerState.BUFFERING} | ${undefined} + ${PlayerEvent.RestoringContent} | ${false} | ${undefined} | ${PlayerState.BUFFERING} + ${PlayerEvent.RestoringContent} | ${true} | ${undefined} | ${PlayerState.BUFFERING} + ${PlayerEvent.AdBreakFinished} | ${false} | ${undefined} | ${PlayerState.PLAYING} + ${PlayerEvent.AdBreakFinished} | ${true} | ${undefined} | ${undefined} + `('should report ad metric $expectedAdMetric and playback metric $expectedPlaybackMetric on event $event when ad is active $isAdActive', ({ event, isAdActive, expectedAdMetric, expectedPlaybackMetric }) => { + jest.spyOn(playerMock, 'isPlaying').mockReturnValue(expectedPlaybackMetric === PlayerState.PLAYING || expectedAdMetric === PlayerState.PLAYING); + + const reportAdMetricSpy = jest.spyOn(MockHelper.latestAdAnalytics, 'reportAdMetric'); + const reportPlaybackMetricSpy = jest.spyOn(MockHelper.latestVideoAnalytics, 'reportPlaybackMetric') + const mockAdData: AdEvent = { ad: { id: 'test-ad-id', data: {} } } as AdEvent; + + if (isAdActive) { + if (event === PlayerEvent.AdStarted || event === PlayerEvent.AdFinished) { + // Needs ad break initialization + playerEventHelper.fireAdBreakStartedEvent(0); + } else { + // Just track the active ad break state + convivaAnalytics['convivaAnalyticsTracker'].trackAdBreakStarted(Conviva.Constants.AdType.CLIENT_SIDE); + } + } + playerEventHelper.fireEvent({ time: 0, timestamp: Date.now(), type: event, ...mockAdData }); + + if (expectedAdMetric) { + expect(reportAdMetricSpy).toHaveBeenLastCalledWith(PLAYER_STATE, expectedAdMetric); + } else { + expect(reportAdMetricSpy).not.toHaveBeenLastCalledWith(PLAYER_STATE, expect.anything()); + } + + if (expectedPlaybackMetric) { + expect(reportPlaybackMetricSpy).toHaveBeenLastCalledWith(PLAYER_STATE, expectedPlaybackMetric); + } else { + expect(reportPlaybackMetricSpy).not.toHaveBeenLastCalledWith(PLAYER_STATE, expect.anything()); + } }); }); - it('should not crash here', () => { - jest.spyOn(playerMock, 'isPaused').mockReturnValue(true); - playerEventHelper.firePauseEvent(); - expect(MockHelper.latestVideoAnalytics.reportPlaybackMetric).not.toHaveBeenCalled(); - }); - - describe('v8 stalling handling', () => { + describe('delayed stalling reporting', () => { // In v8 there is no stalling event between play / playing; seek / seeked; timeshift / thimeshifted but it // can be treated as stalling so we need to report it (maybe timeout in favor of seeking in buffer) describe('reports stalling', () => { - describe('durring playback', () => { + describe('during playback', () => { beforeEach(() => { playerEventHelper.firePlayEvent(); playerEventHelper.firePlayingEvent(); @@ -162,6 +222,37 @@ describe('player event tests', () => { ); }); }); + + describe('stallTrackingTimeout', () => { + test.each([ + PlayerEvent.Play, + PlayerEvent.Seek, + PlayerEvent.TimeShift, + ])('should start timer for stalling when reported player event is %s', (event) => { + const stallTrackingStartTimeoutSpy = jest.spyOn(convivaAnalytics['convivaAnalyticsTracker'], 'startStallTrackingTimeout'); + + playerEventHelper.fireEvent({ time: 0, timestamp: Date.now(), type: event }); + + expect(stallTrackingStartTimeoutSpy).toHaveBeenCalled(); + }); + + test.each([ + PlayerEvent.StallStarted, + PlayerEvent.Playing, + PlayerEvent.Paused, + PlayerEvent.Seeked, + PlayerEvent.TimeShifted, + PlayerEvent.StallEnded, + PlayerEvent.PlaybackFinished, + PlayerEvent.AdStarted, + ])('should clear timer for stalling when reported player event is %s', (event) => { + const stallTrackingStopTimeoutSpy = jest.spyOn(convivaAnalytics['convivaAnalyticsTracker'], 'clearStallTrackingTimeout'); + + playerEventHelper.fireEvent({ time: 0, timestamp: Date.now(), type: event }); + + expect(stallTrackingStopTimeoutSpy).toHaveBeenCalled(); + }); + }); }); describe('end session', () => { @@ -251,7 +342,7 @@ describe('player event tests', () => { ); }); - it('track mid-roll ad', () => { + it('track mid-roll ad', () => { playerEventHelper.fireAdBreakStartedEvent(5); playerEventHelper.fireAdStartedEvent(); expect(MockHelper.latestVideoAnalytics.reportAdBreakStarted).toHaveBeenCalledTimes(1); diff --git a/src/ts/ConvivaAnalytics.ts b/src/ts/ConvivaAnalytics.ts index ac886f5..b94d4f1 100644 --- a/src/ts/ConvivaAnalytics.ts +++ b/src/ts/ConvivaAnalytics.ts @@ -240,17 +240,7 @@ export class ConvivaAnalytics { private onPlaybackStateChanged = (event: PlayerEventBase) => { this.debugLog('[ ConvivaAnalytics ] [ Player Event ] playback state change related event', event); - this.convivaAnalyticsTracker.trackPlaybackStateChanged(event); - }; - - private onPlay = (event: PlaybackEvent) => { - this.debugLog('[ ConvivaAnalytics ] [ Player Event ] play', event); - - if (!this.convivaAnalyticsTracker.canTrackPlayEvent) { - return; - } - - this.onPlaybackStateChanged(event); + this.convivaAnalyticsTracker.trackPlaybackStateFromEvent(event); }; private onPlaying = (event: PlaybackEvent) => { @@ -258,11 +248,6 @@ export class ConvivaAnalytics { this.onPlaybackStateChanged(event); }; - private onPlaybackFinished = (event: PlayerEventBase) => { - this.debugLog('[ ConvivaAnalytics ] [ Player Event ] playback finished', event); - this.onPlaybackStateChanged(event); - }; - private onVideoQualityChanged = (event: VideoQualityChangedEvent) => { this.debugLog('[ ConvivaAnalytics ] [ Player Event ] video quality changed', event); this.convivaAnalyticsTracker.trackVideoQualityChanged(event); @@ -278,23 +263,33 @@ export class ConvivaAnalytics { this.debugLog('[ ConvivaAnalytics ] [ Player Event ] adbreak started', event); this.lastAdBreakEvent = event; this.convivaAnalyticsTracker.trackAdBreakStarted(Conviva.Constants.AdType.CLIENT_SIDE); - this.convivaAnalyticsTracker.trackPlaybackStateChanged(event); + this.convivaAnalyticsTracker.trackPlaybackStateFromEvent(event); }; private onAdStarted = (event: AdEvent) => { + if (!this.lastAdBreakEvent) { + this.debugLog('[ ConvivaAnalytics ] received ad started without active ad break', event); + return; + } + this.debugLog('[ ConvivaAnalytics ] [ Player Event ] ad started', event); const adInfo = AdHelper.extractCsaiConvivaAdInfo(this.lastAdBreakEvent, this.mainContentDuration, event); const bitrateKbps = event.ad.data?.bitrate; this.convivaAnalyticsTracker.trackAdStarted(adInfo, Conviva.Constants.AdType.CLIENT_SIDE, bitrateKbps); - this.convivaAnalyticsTracker.trackPlaybackStateChanged(event); + // No need to call reportPlaybackStateFromEvent as this is covered by `trackAdStarted` } private onAdFinished = (event: AdEvent) => { + if (!this.lastAdBreakEvent) { + this.debugLog('[ ConvivaAnalytics ] received ad finished without active ad break', event); + return; + } + this.debugLog('[ ConvivaAnalytics ] [ Player Event ] ad finished', event); this.convivaAnalyticsTracker.trackAdFinished(); - this.convivaAnalyticsTracker.trackPlaybackStateChanged(event); + this.convivaAnalyticsTracker.trackPlaybackStateFromEvent(event); } private onAdSkipped = (event: AdEvent) => { @@ -306,11 +301,13 @@ export class ConvivaAnalytics { private onRestoringContent = (event: PlayerEventBase) => { this.debugLog('[ ConvivaAnalytics ] [ Player Event ] restoring content', event); this.convivaAnalyticsTracker.trackRestoringContent(); + this.convivaAnalyticsTracker.trackPlaybackStateFromEvent(event); }; private onAdBreakFinished = (event: AdBreakEvent) => { this.debugLog('[ ConvivaAnalytics ] [ Player Event ] adbreak finished', event); - this.convivaAnalyticsTracker.trackAdBreakFinished(); + this.convivaAnalyticsTracker.trackAdBreakFinished(Conviva.Constants.AdType.CLIENT_SIDE); + // No need to call reportPlaybackStateFromEvent as this is covered by `trackAdBreakFinished` } private onAdError = (event: ErrorEvent) => { @@ -322,7 +319,6 @@ export class ConvivaAnalytics { private onSeek = (event: SeekEvent) => { this.debugLog('[ ConvivaAnalytics ] [ Player Event ] seek', event); this.convivaAnalyticsTracker.trackSeekStart(event.seekTarget); - this.onPlaybackStateChanged(event); }; private onSeeked = (event: SeekEvent) => { @@ -335,7 +331,6 @@ export class ConvivaAnalytics { this.debugLog('[ ConvivaAnalytics ] [ Player Event ] time shift', event); // According to conviva it is valid to pass -1 for seeking in live streams this.convivaAnalyticsTracker.trackSeekStart(-1); - this.onPlaybackStateChanged(event); }; private onTimeShifted = (event: TimeShiftEvent) => { @@ -374,14 +369,29 @@ export class ConvivaAnalytics { this.mainContentDuration = this.player.getDuration(); }; + private static readonly stallTrackingStartEvents = [ + PlayerEvent.Play, + PlayerEvent.Seek, + PlayerEvent.TimeShift, + ]; + + private static readonly stallTrackingClearEvents = [ + PlayerEvent.StallStarted, // StallStarted is reported as BUFFERING immediately. Does not need the delayed timeout approach. + PlayerEvent.Playing, + PlayerEvent.Paused, + PlayerEvent.Seeked, + PlayerEvent.TimeShifted, + PlayerEvent.StallEnded, + PlayerEvent.PlaybackFinished, + PlayerEvent.AdStarted, + ]; + private registerPlayerEvents(): void { this.handlers.add(PlayerEvent.SourceLoaded, this.onSourceLoaded); - this.handlers.add(PlayerEvent.Play, this.onPlay); this.handlers.add(PlayerEvent.Playing, this.onPlaying); this.handlers.add(PlayerEvent.Paused, this.onPlaybackStateChanged); this.handlers.add(PlayerEvent.StallStarted, this.onPlaybackStateChanged); this.handlers.add(PlayerEvent.StallEnded, this.onPlaybackStateChanged); - this.handlers.add(PlayerEvent.PlaybackFinished, this.onPlaybackFinished); this.handlers.add(PlayerEvent.VideoPlaybackQualityChanged, this.onVideoQualityChanged); this.handlers.add(PlayerEvent.AudioPlaybackQualityChanged, this.onCustomEvent); this.handlers.add(PlayerEvent.Muted, this.onCustomEvent); @@ -406,6 +416,18 @@ export class ConvivaAnalytics { this.handlers.add(PlayerEvent.CastStarted, this.onCustomEvent); this.handlers.add(PlayerEvent.CastStopped, this.onCustomEvent); + + ConvivaAnalytics.stallTrackingStartEvents.forEach((eventName) => { + this.handlers.add(eventName, (event) => { + this.convivaAnalyticsTracker.startStallTrackingTimeout(event); + }); + }); + + ConvivaAnalytics.stallTrackingClearEvents.forEach((eventName) => { + this.handlers.add(eventName, (event) => { + this.convivaAnalyticsTracker.clearStallTrackingTimeout(event); + }); + }); } private unregisterPlayerEvents(): void { diff --git a/src/ts/ConvivaAnalyticsSsai.ts b/src/ts/ConvivaAnalyticsSsai.ts index 449cf51..3a4a2d8 100644 --- a/src/ts/ConvivaAnalyticsSsai.ts +++ b/src/ts/ConvivaAnalyticsSsai.ts @@ -89,6 +89,6 @@ export class ConvivaAnalyticsSsai { } this._isAdBreakActive = false; - this.convivaAnalyticsTracker.trackAdBreakFinished(); + this.convivaAnalyticsTracker.trackAdBreakFinished(Conviva.Constants.AdType.SERVER_SIDE); } } diff --git a/src/ts/ConvivaAnalyticsTracker.ts b/src/ts/ConvivaAnalyticsTracker.ts index 1e7b230..688febc 100644 --- a/src/ts/ConvivaAnalyticsTracker.ts +++ b/src/ts/ConvivaAnalyticsTracker.ts @@ -205,21 +205,29 @@ export class ConvivaAnalyticsTracker { // Since there are no stall events during play / playing; seek / seeked; timeShift / timeShifted we need // to track stalling state between those events. To prevent tracking eg. when seeking in buffer we delay it. private stallTrackingTimeout: Timeout = new Timeout(ConvivaAnalyticsTracker.STALL_TRACKING_DELAY_MS, () => { - if (this._isAdBreakActive) { - this.debugLog('[ ConvivaAnalyticsTracker ] report buffering ad playback state'); - this.convivaAdAnalytics.reportAdMetric( - Conviva.Constants.Playback.PLAYER_STATE, - Conviva.Constants.PlayerState.BUFFERING, - ); - } else { - this.debugLog('[ ConvivaAnalyticsTracker ] report buffering playback state'); - this.convivaVideoAnalytics.reportPlaybackMetric( - Conviva.Constants.Playback.PLAYER_STATE, - Conviva.Constants.PlayerState.BUFFERING, - ); - } + this.trackPlaybackState(Conviva.Constants.PlayerState.BUFFERING); }); + public startStallTrackingTimeout(event: PlayerEventBase) { + if (!this.isSessionActive()) { + return; + } + + this.debugLog(`[ ConvivaAnalyticsTracker ] start stall tracking after ${event.type} event`); + + this.stallTrackingTimeout.start(); + } + + public clearStallTrackingTimeout(event: PlayerEventBase) { + if (!this.stallTrackingTimeout.isActive()) { + return; + } + + this.debugLog(`[ ConvivaAnalyticsTracker ] stop stall tracking after ${event.type} event`); + + this.stallTrackingTimeout.clear(); + } + /** * Boolean to track whether a session was ended by an upstream caller instead of within internal session management. * If this is true, we should avoid initializing a new session internally if a session is not active @@ -601,66 +609,42 @@ export class ConvivaAnalyticsTracker { } private onSourceLoaded = (event: PlayerEventBase) => { - this.debugLog('[ ConvivaAnalyticsTracker ] [ Player Event ] source loaded', event); - if (!this.isSessionActive()) { return; } + this.debugLog('[ ConvivaAnalyticsTracker ] building content metadata after source loaded event', event); + this.buildContentMetadata(); this.updateSession(); }; - public trackPlaybackStateChanged(event: PlayerEventBase) { - if (!this.isSessionActive()) { - return; - } - + public trackPlaybackStateFromEvent(event: PlayerEventBase) { const playerState = PlayerStateHelper.getPlayerStateFromEvent(event, this.player); - const stallTrackingStartEvents = [ - PlayerEvent.Play, - PlayerEvent.Seek, - PlayerEvent.TimeShift, - PlayerEvent.AdBreakStarted, - PlayerEvent.AdFinished, - PlayerEvent.RestoringContent, - ]; - const stallTrackingClearEvents = [ - PlayerEvent.StallStarted, // StallStarted is reported as BUFFERING immediately. Does not need the delayed timeout approach. - PlayerEvent.Playing, - PlayerEvent.Paused, - PlayerEvent.Seeked, - PlayerEvent.TimeShifted, - PlayerEvent.StallEnded, - PlayerEvent.PlaybackFinished, - PlayerEvent.AdStarted, - ]; - - if (stallTrackingStartEvents.indexOf(event.type) !== -1) { - this.stallTrackingTimeout.start(); - } else if (stallTrackingClearEvents.indexOf(event.type) !== -1) { - this.stallTrackingTimeout.clear(); - } + this.debugLog(`[ ConvivaAnalyticsTracker ] inferred player state ${playerState} from ${event.type} event`, {playerState, event}); if (playerState) { - if (this._isAdBreakActive) { - this.debugLog('[ ConvivaAnalyticsTracker ] report ad playback state', playerState); - this.convivaAdAnalytics.reportAdMetric(Conviva.Constants.Playback.PLAYER_STATE, playerState); - } else { - this.debugLog('[ ConvivaAnalyticsTracker ] report playback state', playerState); - this.convivaVideoAnalytics.reportPlaybackMetric(Conviva.Constants.Playback.PLAYER_STATE, playerState); - } + this.trackPlaybackState(playerState); + } + } + + private trackPlaybackState(playerState: Conviva.valueof) { + if (!this.isSessionActive()) { + return; } - if (event.type === PlayerEvent.PlaybackFinished) { - this.debugLog('[ ConvivaAnalyticsTracker ] report playback ended'); - this.convivaVideoAnalytics.reportPlaybackEnded(); + if (this._isAdBreakActive) { + this.debugLog('[ ConvivaAnalyticsTracker ] report ad playback state', playerState); + this.convivaAdAnalytics.reportAdMetric(Conviva.Constants.Playback.PLAYER_STATE, playerState); + } else { + this.debugLog('[ ConvivaAnalyticsTracker ] report playback state', playerState); + this.convivaVideoAnalytics.reportPlaybackMetric(Conviva.Constants.Playback.PLAYER_STATE, playerState); } } private onPlay = (event: PlaybackEvent) => { - this.debugLog('[ ConvivaAnalyticsTracker ] [ Player Event ] play'); + this.debugLog('[ ConvivaAnalyticsTracker ] checking if session needs to be initialized after play event'); if (!this.canTrackPlayEvent) { return; @@ -681,23 +665,25 @@ export class ConvivaAnalyticsTracker { }; private onPlaying = (event: PlaybackEvent) => { - this.debugLog('[ ConvivaAnalyticsTracker ] [ Player Event ] playing', event); - if (!this.isSessionActive()) { return; } + this.debugLog('[ ConvivaAnalyticsTracker ] updating session metadata after playing event', event); + this.contentMetadataBuilder.setPlaybackStarted(true); this.updateSession(); }; private onPlaybackFinished = (event: PlayerEventBase) => { - this.debugLog('[ ConvivaAnalyticsTracker ] [ Player Event ] playback finished', event); - if (!this.isSessionActive()) { return; } + this.debugLog('[ ConvivaAnalyticsTracker ] releasing everything after playback finished event', event); + + this.trackPlaybackFinished(); + this.convivaVideoAnalytics.release(); this.convivaVideoAnalytics = null; @@ -705,6 +691,11 @@ export class ConvivaAnalyticsTracker { this.convivaAdAnalytics = null; }; + private trackPlaybackFinished = () => { + this.debugLog('[ ConvivaAnalyticsTracker ] report playback ended'); + this.convivaVideoAnalytics.reportPlaybackEnded(); + } + public trackVideoQualityChanged = (event: VideoQualityChangedEvent) => { if (!this.isSessionActive()) { return; @@ -747,7 +738,7 @@ export class ConvivaAnalyticsTracker { }); this.convivaAdAnalytics.reportAdStarted(adInfo); - this.debugLog(`[ ConvivaAnalyticsTracker ] report ${PlayerStateHelper.getPlayerState(this.player)} ad playback state`); + this.debugLog(`[ ConvivaAnalyticsTracker ] report ${PlayerStateHelper.getPlayerState(this.player)} ad playback state on ad started event`); this.convivaAdAnalytics.reportAdMetric(Conviva.Constants.Playback.PLAYER_STATE, PlayerStateHelper.getPlayerState(this.player)); if (type === Conviva.Constants.AdType.SERVER_SIDE) { @@ -788,7 +779,7 @@ export class ConvivaAnalyticsTracker { }; public trackRestoringContent = () => { - if (!this.isSessionActive()) { + if (!this.isSessionActive() || !this._isAdBreakActive) { return; } @@ -798,15 +789,21 @@ export class ConvivaAnalyticsTracker { this.convivaVideoAnalytics.reportAdBreakEnded(); }; - public trackAdBreakFinished = () => { - if (!this.isSessionActive() || this._isAdBreakActive) { + public trackAdBreakFinished = (type: Conviva.valueof) => { + const shouldExpectAnotherCsaiAd = type === Conviva.Constants.AdType.CLIENT_SIDE && this._isAdBreakActive; + if (!this.isSessionActive() || shouldExpectAnotherCsaiAd) { + // We only care to update the playback state when we are restoring main content. return; } - this.debugLog(`[ ConvivaAnalyticsTracker ] report ${PlayerStateHelper.getPlayerState(this.player)} playback state`); + this.trackRestoringContent(); + + const playerState = PlayerStateHelper.getPlayerState(this.player); + + this.debugLog(`[ ConvivaAnalyticsTracker ] report ${playerState} playback state on ad break finished event`); this.convivaVideoAnalytics.reportPlaybackMetric( Conviva.Constants.Playback.PLAYER_STATE, - PlayerStateHelper.getPlayerState(this.player), + playerState, ); }; @@ -929,7 +926,7 @@ export class ConvivaAnalyticsTracker { }; private onSourceUnloaded = (event: PlayerEventBase) => { - this.debugLog('[ ConvivaAnalyticsTracker ] [ Player Event ] source unloaded', event); + this.debugLog('[ ConvivaAnalyticsTracker ] checking if seession needs to be ended after source unloaded', event); if (this._isAdBreakActive) { // Ignore sourceUnloaded events during ads @@ -939,6 +936,7 @@ export class ConvivaAnalyticsTracker { } }; + // These are attached earlier than the ones inside `ConvivaAnalytics` private registerPlayerEvents(): void { this.handlers.add(PlayerEvent.SourceLoaded, this.onSourceLoaded); this.handlers.add(PlayerEvent.Play, this.onPlay); diff --git a/src/ts/helper/PlayerStateHelper.ts b/src/ts/helper/PlayerStateHelper.ts index 2b51aae..9b7a7c2 100644 --- a/src/ts/helper/PlayerStateHelper.ts +++ b/src/ts/helper/PlayerStateHelper.ts @@ -6,6 +6,9 @@ export class PlayerStateHelper { let playerState; switch (event.type) { + case PlayerEvent.AdBreakStarted: + case PlayerEvent.AdFinished: + case PlayerEvent.RestoringContent: case PlayerEvent.StallStarted: playerState = Conviva.Constants.PlayerState.BUFFERING; break;