Skip to content

Commit

Permalink
Merge pull request #117 from bitmovin/feature/PW-21589-improve-vst-tr…
Browse files Browse the repository at this point in the history
…acking

Address underreported VST after ad pre-roll
  • Loading branch information
wasp898 authored Nov 27, 2024
2 parents 2c7077b + 36476fe commit d32828e
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 10 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

### Fixed
- Ad-related delays not contributing to rebuffering metrics
- Underreported VST after playing CSAI pre-rolls

## [5.4.0] - 2024-08-27
### Added
Expand Down
112 changes: 112 additions & 0 deletions spec/tests/ConvivaAnalyticsTracker.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { PlayerEvent, PlayerEventBase } from "bitmovin-player";
import { ConvivaAnalyticsTracker } from "../../src/ts/ConvivaAnalyticsTracker";
import { MockHelper } from "../helper/MockHelper";
import * as Conviva from '@convivainc/conviva-js-coresdk';
import { PlayerStateHelper } from "../../src/ts/helper/PlayerStateHelper";

jest.mock('@convivainc/conviva-js-coresdk', () => {
const { MockHelper } = jest.requireActual('../helper/MockHelper');
Expand Down Expand Up @@ -121,6 +122,117 @@ describe(ConvivaAnalyticsTracker, () => {
expect(stallTrackingStopTimeoutSpy).toHaveBeenCalled();
})
})

/**
* Since the web player dispatches AdBreakFinished only after main content has successfully restored,
* a workaround is in place to signal the end of the ad break early so that VST can properly be tracked.
*/
describe('eager ad break ended signalling', () => {
let convivaAnalyticsTracker: ConvivaAnalyticsTracker;
let reportAdBreakEndedSpy: jest.SpyInstance;
let reportAdBreakStartedSpy: jest.SpyInstance;

beforeEach(() => {
convivaAnalyticsTracker = new ConvivaAnalyticsTracker('test-key');

const {playerMock} = MockHelper.createPlayerMock();
convivaAnalyticsTracker.attachPlayer(playerMock);
jest.spyOn(playerMock, 'getSource').mockImplementation(() => ({ title: 'test-title' }));
convivaAnalyticsTracker.initializeSession();

jest.useFakeTimers();
reportAdBreakEndedSpy = jest.spyOn(convivaAnalyticsTracker['convivaVideoAnalytics'], 'reportAdBreakEnded');
reportAdBreakStartedSpy = jest.spyOn(convivaAnalyticsTracker['convivaVideoAnalytics'], 'reportAdBreakStarted');
});

it('should report ad break ended on AdBreakFinished by default', () => {
convivaAnalyticsTracker.trackAdBreakStarted(Conviva.Constants.AdType.CLIENT_SIDE);
convivaAnalyticsTracker.trackAdStarted({}, Conviva.Constants.AdType.CLIENT_SIDE);
convivaAnalyticsTracker.trackAdFinished();

expect(reportAdBreakEndedSpy).toHaveBeenCalledTimes(0);

convivaAnalyticsTracker.trackAdBreakFinished();

expect(reportAdBreakEndedSpy).toHaveBeenCalledTimes(1);
})

it('should report ad break ended early if AdBreakFinished is too slow', () => {
convivaAnalyticsTracker.trackAdBreakStarted(Conviva.Constants.AdType.CLIENT_SIDE);
convivaAnalyticsTracker.trackAdStarted({}, Conviva.Constants.AdType.CLIENT_SIDE);
convivaAnalyticsTracker.trackAdFinished();

expect(reportAdBreakEndedSpy).toHaveBeenCalledTimes(0);

jest.runAllTimers();

expect(reportAdBreakEndedSpy).toHaveBeenCalledTimes(1);
})

it('should not report ad break ended twice if AdBreakFinished is emitted after the ad finished timer has already expired', () => {
convivaAnalyticsTracker.trackAdBreakStarted(Conviva.Constants.AdType.CLIENT_SIDE);
convivaAnalyticsTracker.trackAdStarted({}, Conviva.Constants.AdType.CLIENT_SIDE);
convivaAnalyticsTracker.trackAdFinished();

jest.runAllTimers();

expect(reportAdBreakEndedSpy).toHaveBeenCalledTimes(1);

convivaAnalyticsTracker.trackAdBreakFinished();

expect(reportAdBreakEndedSpy).toHaveBeenCalledTimes(1);
})

it('should not report ad break ended if AdFinished is followed by a new AdStarted', () => {
convivaAnalyticsTracker.trackAdBreakStarted(Conviva.Constants.AdType.CLIENT_SIDE);
convivaAnalyticsTracker.trackAdStarted({}, Conviva.Constants.AdType.CLIENT_SIDE);

expect(reportAdBreakStartedSpy).toHaveBeenCalledTimes(1);

convivaAnalyticsTracker.trackAdFinished();
convivaAnalyticsTracker.trackAdStarted({}, Conviva.Constants.AdType.CLIENT_SIDE);

jest.runAllTimers();

expect(reportAdBreakEndedSpy).toHaveBeenCalledTimes(0);
expect(reportAdBreakStartedSpy).toHaveBeenCalledTimes(1);
})

it('should report ad break started again after wrongly reporting ad break ended', () => {
convivaAnalyticsTracker.trackAdBreakStarted(Conviva.Constants.AdType.CLIENT_SIDE);
convivaAnalyticsTracker.trackAdStarted({}, Conviva.Constants.AdType.CLIENT_SIDE);

expect(reportAdBreakStartedSpy).toHaveBeenCalledTimes(1);

convivaAnalyticsTracker.trackAdFinished();
jest.runAllTimers();

expect(reportAdBreakEndedSpy).toHaveBeenCalledTimes(1);

convivaAnalyticsTracker.trackAdStarted({}, Conviva.Constants.AdType.CLIENT_SIDE);

expect(reportAdBreakStartedSpy).toHaveBeenCalledTimes(2);
})

it('should only signal that playback has resumed after the actual AdBreakFinished', () => {
const { AdType, Playback, PlayerState } = Conviva.Constants;
const reportPlaybackSpy = jest.spyOn(convivaAnalyticsTracker['convivaVideoAnalytics'], 'reportPlaybackMetric');
jest.spyOn(PlayerStateHelper, 'getPlayerState').mockReturnValue(PlayerState.PLAYING);

convivaAnalyticsTracker.trackAdBreakStarted(AdType.CLIENT_SIDE);
convivaAnalyticsTracker.trackAdStarted({}, AdType.CLIENT_SIDE);
convivaAnalyticsTracker.trackAdFinished();

jest.runAllTimers();

expect(reportAdBreakEndedSpy).toHaveBeenCalledTimes(1);
expect(reportPlaybackSpy).not.toHaveBeenCalledWith(Playback.PLAYER_STATE, PlayerState.PLAYING);

convivaAnalyticsTracker.trackAdBreakFinished();

expect(reportPlaybackSpy).toHaveBeenCalledWith(Playback.PLAYER_STATE, PlayerState.PLAYING);
})
});
})

const getInvokedTimes = (mock: unknown) => {
Expand Down
46 changes: 36 additions & 10 deletions src/ts/ConvivaAnalyticsTracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,9 @@ export interface EventAttributes {
export class ConvivaAnalyticsTracker {
private static readonly VERSION: string = '{{VERSION}}';

public static readonly AD_BREAK_FINISHED_DELAY_MS = 250;
public static readonly STALL_TRACKING_DELAY_MS = 100;

private _player: PlayerAPI;

private get player(): PlayerAPI {
Expand Down Expand Up @@ -736,10 +738,19 @@ export class ConvivaAnalyticsTracker {
};

public trackAdStarted = (adInfo: Conviva.ConvivaMetadata, type: Conviva.valueof<Conviva.ConvivaConstants['AdType']>, bitrateKbps?: number) => {
// Clear the timeout that may have been scheduled by a previous ad finished event, as the ad break is not actually over yet.
this.adBreakFinishedTimeout.clear();

if (!this.isSessionActive()) {
return;
}

if (!this.isAdBreakActive) {
// If no ad break is active, it must mean that the `adBreakFinishedTimeout` ran before AdStarted was emitted.
// Then we need to report this as the start of a new ad break.
this.trackAdBreakStarted(type);
}

this.debugLog('[ ConvivaAnalyticsTracker ] report ad started', {
adInfo,
type,
Expand Down Expand Up @@ -769,15 +780,6 @@ export class ConvivaAnalyticsTracker {
}
}

public trackAdFinished = () => {
if (!this.isSessionActive()) {
return;
}

this.debugLog('[ ConvivaAnalyticsTracker ] report ad ended');
this.convivaAdAnalytics.reportAdEnded();
}

public trackAdSkipped = () => {
if (!this.isSessionActive()) {
return;
Expand All @@ -787,15 +789,39 @@ export class ConvivaAnalyticsTracker {
this.convivaAdAnalytics.reportAdSkipped();
};

public trackAdBreakFinished = () => {
public trackAdFinished = () => {
if (!this.isSessionActive()) {
return;
}

this.debugLog('[ ConvivaAnalyticsTracker ] report ad ended');
this.convivaAdAnalytics.reportAdEnded();

// Start timer to report ad break finished, as waiting for the event will cause VST to be too low.
this.adBreakFinishedTimeout.start();
}

private reportAdBreakEnded = () => {
this._isAdBreakActive = false;

this.debugLog('[ ConvivaAnalyticsTracker ] report ad break ended');
this.convivaVideoAnalytics.reportAdBreakEnded();
}

private adBreakFinishedTimeout = new Timeout(ConvivaAnalyticsTracker.AD_BREAK_FINISHED_DELAY_MS, this.reportAdBreakEnded);

public trackAdBreakFinished = () => {
// Clear the timeout to prevent the ad break finished event from being reported twice
this.adBreakFinishedTimeout.clear();

if (!this.isSessionActive()) {
return;
}

if (this.isAdBreakActive) {
// If ad break is still active, it must mean that the event was faster than the `adBreakFinishedTimeout`
this.reportAdBreakEnded();
}

this.debugLog(`[ ConvivaAnalyticsTracker ] report ${PlayerStateHelper.getPlayerState(this.player)} playback state`);
this.convivaVideoAnalytics.reportPlaybackMetric(
Expand Down

0 comments on commit d32828e

Please sign in to comment.