conviva: preserve startup session across early sourcechange#104
conviva: preserve startup session across early sourcechange#104balazStefan wants to merge 1 commit intoTHEOplayer:mainfrom
Conversation
|
72ffcb0 to
f238687
Compare
MattiasBuelens
left a comment
There was a problem hiding this comment.
Thanks for your contribution! This looks pretty reasonable, I especially appreciate your efforts to add unit tests.
There are a few things that should be changed (mostly technical details), please have a look at the review comments.
| ## Unreleased | ||
|
|
||
| ### ✨ Features | ||
|
|
||
| - Added optional startup source-change preservation configuration: | ||
| - `preserveSessionOnStartupSourceChange` (default `false`) | ||
| - `startupGraceMs` (default `10000`) | ||
|
|
There was a problem hiding this comment.
Please do not modify CHANGELOG.md directly, instead use npx changeset. The bot has links that explain how to do this. 😉
|
|
||
| private currentSource: SourceDescription | undefined; | ||
| private playbackRequested: boolean = false; | ||
| private startupAt: number | null = null; |
There was a problem hiding this comment.
Nit-pick: we usually prefer undefined instead of null.
| private startupAt: number | null = null; | |
| private startupAt: number | undefined = undefined; |
| vi.mock('../../src/integration/ads/AdReporter', () => ({ | ||
| AdReporter: class { | ||
| destroy(): void {} | ||
| } | ||
| })); | ||
|
|
||
| vi.mock('../../src/integration/ads/YospaceAdReporter', () => ({ | ||
| YospaceAdReporter: class { | ||
| destroy(): void {} | ||
| } | ||
| })); | ||
|
|
||
| vi.mock('../../src/integration/ads/UplynkAdReporter', () => ({ | ||
| UplynkAdReporter: class { | ||
| destroy(): void {} | ||
| } | ||
| })); | ||
|
|
||
| vi.mock('../../src/integration/theolive/THEOliveReporter', () => ({ | ||
| THEOliveReporter: class { | ||
| destroy(): void {} | ||
| } | ||
| })); | ||
|
|
||
| vi.mock('../../src/utils/ErrorReportBuilder', () => ({ | ||
| ErrorReportBuilder: class { | ||
| destroy(): void {} | ||
|
|
||
| withPlayerBuffer() { | ||
| return this; | ||
| } | ||
|
|
||
| withErrorDetails() { | ||
| return this; | ||
| } | ||
|
|
||
| build() { | ||
| return undefined; | ||
| } | ||
| } | ||
| })); | ||
|
|
||
| type EventListener = (event?: unknown) => void; | ||
|
|
||
| class FakeEventDispatcher { | ||
| private readonly listeners = new Map<string, Set<EventListener>>(); | ||
|
|
||
| addEventListener(type: string, listener: EventListener): void { | ||
| if (!this.listeners.has(type)) { | ||
| this.listeners.set(type, new Set<EventListener>()); | ||
| } | ||
| this.listeners.get(type)!.add(listener); | ||
| } | ||
|
|
||
| removeEventListener(type: string, listener: EventListener): void { | ||
| this.listeners.get(type)?.delete(listener); | ||
| } | ||
|
|
||
| emit(type: string, event?: unknown): void { | ||
| this.listeners.get(type)?.forEach((listener) => listener(event)); | ||
| } | ||
| } | ||
|
|
||
| class FakePlayer extends FakeEventDispatcher { | ||
| public source: any; | ||
| public src: string | undefined; | ||
| public paused: boolean = true; | ||
| public ended: boolean = false; | ||
| public readyState: number = 3; | ||
| public duration: number = 120; | ||
| public currentTime: number = 0; | ||
| public videoWidth: number = 1920; | ||
| public videoHeight: number = 1080; | ||
| public videoTracks: Array<any> = [{ activeQuality: undefined }]; | ||
| public abr = { | ||
| targetBuffer: undefined, | ||
| bufferLookbackWindow: undefined, | ||
| strategy: undefined | ||
| }; | ||
| public ads: Array<any> = []; | ||
| public uplynk: undefined = undefined; | ||
| public network = new FakeEventDispatcher(); | ||
|
|
||
| setSource(src: string, title = 'Asset'): void { | ||
| this.source = { | ||
| sources: { | ||
| src, | ||
| type: 'application/vnd.apple.mpegurl' | ||
| }, | ||
| metadata: { title } | ||
| }; | ||
| this.src = src; | ||
| } | ||
| } | ||
|
|
||
| function emitPlayerEvent(player: FakePlayer, type: string): void { | ||
| if (type === 'play') player.paused = false; | ||
| if (type === 'pause') player.paused = true; | ||
| if (type === 'ended') player.ended = true; | ||
| player.emit(type); | ||
| } | ||
|
|
||
| function createVideoAnalyticsMock() { | ||
| return { | ||
| setPlayerInfo: vi.fn(), | ||
| setCallback: vi.fn(), | ||
| reportPlaybackRequested: vi.fn(), | ||
| reportPlaybackEnded: vi.fn(), | ||
| reportPlaybackMetric: vi.fn(), | ||
| setContentInfo: vi.fn(), | ||
| reportPlaybackFailed: vi.fn(), | ||
| reportPlaybackEvent: vi.fn(), | ||
| reportPlaybackError: vi.fn(), | ||
| release: vi.fn() | ||
| }; | ||
| } | ||
|
|
||
| function createAdAnalyticsMock() { | ||
| return { | ||
| setAdInfo: vi.fn(), | ||
| release: vi.fn() | ||
| }; | ||
| } |
There was a problem hiding this comment.
Could you move these mocks to a separate file? They are distracting a bit from the tests themselves.
| const convivaConfig: ConvivaConfiguration = { customerKey: 'test' }; | ||
| const connector = new ConvivaConnector(player as any, {}, convivaConfig); | ||
| expect(connector).toBeDefined(); | ||
| connector.destroy(); |
There was a problem hiding this comment.
If you add let connector: ConvivaConnector to the surrounding describe, we can put connector?.destroy() inside the afterEach. 😉
conviva: avoid ending session on early startup sourcechange
Add optional startup-preservation config:
When enabled, sourcechange events during startup (play -> playing)
within the grace window no longer end the Conviva session;
source metadata is refreshed instead.
Summary
Add optional startup source-change preservation to prevent premature Conviva session endings during startup transitions, while keeping default behavior unchanged.
What changed
API / Configuration
preserveSessionOnStartupSourceChange?: boolean(defaultfalse)startupGraceMs?: number(default10000)Handler behavior
DEFAULT_STARTUP_GRACE_MS = 10_000startupAt: number | nullonPlay: marks startup start timeonPlaying: clears startup markeronSourceChange:maybeReportPlaybackEnded: clears startup markerTests
Added unit coverage via public
ConvivaConnectorAPI and event-driven scenarios:sourcechangewithin grace -> session is preservedplaying-> session ends onsourcechangeDocumentation
Unreleased