ogplayer 1.0.4 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,9 +4,9 @@
4
4
  * by the shared private key unlocks all three platforms. No network, ever.
5
5
  *
6
6
  * License format: `OGP1.<base64url payload JSON>.<base64url ECDSA sig>`.
7
- * On the web the `apps` patterns bind to the page HOSTNAME (exact match, or
8
- * prefix match when the pattern ends with `*`). Failure is soft — the caller
9
- * shows the watermark, playback is unaffected.
7
+ * On the web the `apps` patterns bind to the page HOSTNAME: exact match, or
8
+ * `*.example.com` for the domain and all its subdomains. Failure is soft —
9
+ * the caller shows the watermark, playback is unaffected.
10
10
  */
11
11
  export type LicenseResult = {
12
12
  licensed: true;
@@ -20,7 +20,7 @@ export type LicenseResult = {
20
20
  export declare function verifyLicense(licenseKey: string | undefined, hostname: string, publicKeyB64?: string): Promise<LicenseResult>;
21
21
  /** DER SEQUENCE { INTEGER r, INTEGER s } → 64-byte raw r||s. */
22
22
  export declare function derSignatureToRaw(der: Uint8Array): Uint8Array | null;
23
- /** Exact match, or prefix match when the pattern ends with `*`. */
23
+ /** Exact match, or `*.example.com` for the domain and all its subdomains. */
24
24
  export declare function licensePatternMatches(host: string, pattern: string): boolean;
25
25
  /** Valid through the end of `exp` (YYYY-MM-DD), device local date. */
26
26
  export declare function isExpired(exp: string): boolean;
@@ -1,5 +1,5 @@
1
1
  import { type HlsConfig } from "hls.js";
2
- import { type AudioTrack, type LiveInfo, type OGAnalyticsListener, type OGMediaItem, type PlaybackListener, type PlaybackState, type SubtitleStyle, type TextTrack as OGTextTrack, type ThumbnailFrame, type VideoQuality, type VolumeControlMode } from "./types.js";
2
+ import { type AnalyticsEvent, type AudioTrack, type LiveInfo, type OGAnalyticsListener, type OGMediaItem, type PlaybackListener, type PlaybackState, type SubtitleStyle, type TextTrack as OGTextTrack, type ThumbnailFrame, type VideoQuality, type VolumeControlMode } from "./types.js";
3
3
  import type { AdListener } from "../ads/types.js";
4
4
  import type { AdsProvider } from "../ads/provider.js";
5
5
  export interface OGPlayerOptions {
@@ -66,6 +66,9 @@ export declare class OGPlayer {
66
66
  private drmLicenseRequests;
67
67
  private thumbnailTrack;
68
68
  private lastDroppedTotal;
69
+ private sessionIdValue;
70
+ /** BufferStart reasons + time-to-first-frame (per-load state machine). */
71
+ private sessionTracker;
69
72
  /** Saved content position while a content-element ads provider holds the
70
73
  * media element (FreeWheel handoff). */
71
74
  private breakResume;
@@ -144,6 +147,11 @@ export declare class OGPlayer {
144
147
  /** VMAP cue-point times in seconds (negative = postroll). */
145
148
  get adCuePoints(): number[];
146
149
  get currentLoadGeneration(): number;
150
+ /** Id of the current analytics session — a fresh UUID minted by every
151
+ * `load()`, stamped on each AnalyticsEvent of that load so consumers can
152
+ * key their pipelines without inventing their own ids. Null before the
153
+ * first load. */
154
+ get sessionId(): string | null;
147
155
  subtitleStyle: SubtitleStyle;
148
156
  subtitleTextScale: number;
149
157
  onLicenseChanged(cb: (licensed: boolean) => void): void;
@@ -154,7 +162,16 @@ export declare class OGPlayer {
154
162
  addAdListener(l: AdListener): void;
155
163
  removeAdListener(l: AdListener): void;
156
164
  private dispatch;
165
+ /** Single exit for analytics: every event leaves stamped with the session
166
+ * id and asset URL of the load it belongs to (Android parity). */
157
167
  private emit;
168
+ /** @internal Feeds a view-layer analytics event into this player's stream,
169
+ * session-stamped like every other event. Called by the SDK's own elements
170
+ * (`<og-player>`) — not host-app API (the web `emitViewAnalytics` of
171
+ * Android's internal hook; kept off the docs surface via `@internal`). */
172
+ emitViewAnalytics(e: Extract<AnalyticsEvent, {
173
+ type: "FullscreenChanged" | "OrientationChanged";
174
+ }>): void;
158
175
  /** The queue set by loadPlaylist() — empty when a single item was loaded. */
159
176
  private playlistItems;
160
177
  private playlistPosition;
@@ -212,6 +229,17 @@ export declare class OGPlayer {
212
229
  */
213
230
  retry(): void;
214
231
  private attachSource;
232
+ /**
233
+ * Emits PlaybackStarted { startupMs } — load() to the load's first
234
+ * rendered content frame (time-to-first-frame). requestVideoFrameCallback
235
+ * (Chrome/Safari) reports the actual composited frame; browsers without it
236
+ * (Firefox) fall back to 'loadeddata' — there the first frame is decoded
237
+ * and paintable, which lands a paint or two before true presentation but
238
+ * is the closest cross-browser signal. Armed from attachSource so a
239
+ * content restore after a content-element ad break (FreeWheel handoff)
240
+ * re-arms it; the tracker's once-per-load latch dedupes.
241
+ */
242
+ private armFirstFrame;
215
243
  private adSessionKey;
216
244
  private beginAdsIfNeeded;
217
245
  private startAdsSession;
@@ -0,0 +1,28 @@
1
+ import type { BufferReason } from "./types.js";
2
+ /**
3
+ * Pure per-load state behind two analytics facts: why a buffer started
4
+ * (`BufferReason`) and time-to-first-frame. Reset by every load; driven by
5
+ * the media-element events the player receives ('seeking', 'seeked',
6
+ * 'canplay'/'playing'). Pure logic — unit-tested in node without a DOM.
7
+ * The web port of Android's `PlaybackSessionTracker` (the reference).
8
+ */
9
+ export declare class PlaybackSessionTracker {
10
+ private readySeen;
11
+ private seekInFlight;
12
+ private loadStartMs;
13
+ private firstFrameEmitted;
14
+ onNewLoad(nowMs: number): void;
15
+ /** 'seeking' fired — buffering that follows is seek-caused. */
16
+ onSeek(): void;
17
+ /** 'seeked' fired — the seek is no longer in flight. */
18
+ onSeekCompleted(): void;
19
+ /** 'canplay'/'playing' — the load has been ready at least once. */
20
+ onReady(): void;
21
+ /** Reason for a buffering transition happening right now. */
22
+ bufferReason(): BufferReason;
23
+ /**
24
+ * Startup time when this is the load's first rendered frame, null on
25
+ * every later frame (and before any load).
26
+ */
27
+ takeFirstFrameStartupMs(nowMs: number): number | null;
28
+ }
@@ -0,0 +1,15 @@
1
+ import type { DrmTokenProvider, DrmTokenRequest } from "./types.js";
2
+ /**
3
+ * Fenced DRM token fetch — the web counterpart of the Android SDK's
4
+ * `OGMediaDrmCallback.fetchToken()`: a hung token service must never hold
5
+ * playback forever. Bounded attempts with a per-attempt timeout and a
6
+ * progressive backoff; the last failure propagates so callers map it to
7
+ * DRM_TOKEN_FETCH_FAILED (4002). Same 3 × 8 s fence on all platforms.
8
+ */
9
+ export declare const TOKEN_ATTEMPTS = 3;
10
+ export declare const TOKEN_TIMEOUT_MS = 8000;
11
+ /** Whole-call deadline for license/certificate HTTP requests — the web
12
+ * mirror of Android's `boundedHttpClient` callTimeout and iOS's
13
+ * `timeoutIntervalForResource` (15 s on every platform). */
14
+ export declare const LICENSE_CALL_TIMEOUT_MS = 15000;
15
+ export declare function fetchTokenHeaders(provider: DrmTokenProvider, request: DrmTokenRequest, timeoutMs?: number): Promise<Record<string, string>>;
@@ -154,7 +154,21 @@ export interface ThumbnailFrame {
154
154
  width: number;
155
155
  height: number;
156
156
  }
157
- export type AnalyticsEvent = {
157
+ /** Why a BufferStart happened — rebuffer-ratio contracts need initial and
158
+ * seek buffering excluded, so the reason is explicit instead of inferred
159
+ * from event order. */
160
+ export type BufferReason = "INITIAL" | "SEEK" | "REBUFFER";
161
+ /** Physical device orientation, as reported by the view layer. */
162
+ export type DeviceOrientation = "PORTRAIT" | "LANDSCAPE";
163
+ /** Fields stamped on every analytics event: the per-load session id (a
164
+ * fresh UUID minted by every `load()`) and the item URL, so analytics
165
+ * pipelines can key events without inventing their own ids. Absent only
166
+ * on events emitted before the first load. */
167
+ export interface AnalyticsEventBase {
168
+ sessionId?: string;
169
+ assetUrl?: string;
170
+ }
171
+ export type AnalyticsEvent = AnalyticsEventBase & ({
158
172
  type: "Play";
159
173
  } | {
160
174
  type: "Pause";
@@ -162,8 +176,16 @@ export type AnalyticsEvent = {
162
176
  type: "Seek";
163
177
  fromMs: number;
164
178
  toMs: number;
179
+ }
180
+ /** First video frame of a load rendered; `startupMs` is wall time from
181
+ * `load()` to that frame — time-to-first-frame, the industry's primary
182
+ * startup metric. Emitted once per load. */
183
+ | {
184
+ type: "PlaybackStarted";
185
+ startupMs: number;
165
186
  } | {
166
187
  type: "BufferStart";
188
+ reason: BufferReason;
167
189
  } | {
168
190
  type: "BufferEnd";
169
191
  } | {
@@ -172,16 +194,53 @@ export type AnalyticsEvent = {
172
194
  width: number;
173
195
  height: number;
174
196
  frameRate: number;
175
- } | {
197
+ }
198
+ /** Periodic playback-quality sample, every 10s while playing.
199
+ * `liveLatencyMs`/`atLiveEdge` are populated for live streams only. */
200
+ | {
176
201
  type: "QualitySnapshot";
177
202
  positionMs: number;
178
203
  bufferedMs: number;
179
204
  bandwidthEstimateBps: number;
180
205
  droppedFramesTotal: number;
206
+ liveLatencyMs?: number;
207
+ atLiveEdge?: boolean;
181
208
  } | {
182
209
  type: "DroppedFrames";
183
210
  count: number;
184
211
  elapsedMs: number;
212
+ }
213
+ /** The viewer changed volume or mute — the muted-to-sound conversion
214
+ * metric for autoplay/ad measurement. */
215
+ | {
216
+ type: "VolumeChanged";
217
+ volume: number;
218
+ muted: boolean;
219
+ }
220
+ /** A text track was selected (`id` null = subtitles off). */
221
+ | {
222
+ type: "TextTrackChanged";
223
+ id: string | null;
224
+ language: string | null;
225
+ }
226
+ /** An audio track was selected. */
227
+ | {
228
+ type: "AudioTrackChanged";
229
+ id: string;
230
+ language: string | null;
231
+ }
232
+ /** The player entered or left fullscreen (reported by the view layer;
233
+ * headless integrations do not emit it). */
234
+ | {
235
+ type: "FullscreenChanged";
236
+ isFullscreen: boolean;
237
+ }
238
+ /** The device rotated (reported by the view layer once on attach with the
239
+ * initial value, then on change). The vertical feed does not emit it —
240
+ * the feed is portrait by design. */
241
+ | {
242
+ type: "OrientationChanged";
243
+ orientation: DeviceOrientation;
185
244
  } | {
186
245
  type: "DrmKeysLoaded";
187
246
  } | {
@@ -205,7 +264,7 @@ export type AnalyticsEvent = {
205
264
  } | {
206
265
  type: "Error";
207
266
  error: OGPlayerError;
208
- };
267
+ });
209
268
  export type OGAnalyticsListener = (event: AnalyticsEvent) => void;
210
269
  /** Error taxonomy — same numeric codes as Android/iOS. */
211
270
  export declare const ErrorCodes: {
@@ -104,12 +104,23 @@ export declare class VerticalFeedPlayerPool {
104
104
  private activeIndexValue;
105
105
  private released;
106
106
  private feedMuted;
107
- private skipped;
107
+ /**
108
+ * ITEMS rejected by the max-duration guard; the feed auto-skips them.
109
+ * Keyed by the item object (not its index): positions shift on every
110
+ * `updateItems` refresh, and index-keyed positional skips would
111
+ * permanently poison whatever lands on those positions in refreshed
112
+ * lists. A refresh that rebuilds the item objects naturally clears
113
+ * their skips (the too-long stream is then re-discovered and re-skipped
114
+ * on first ready); reordering a stable array keeps them.
115
+ */
116
+ private skippedItems;
108
117
  private licensedValue;
109
118
  constructor(config: OGVerticalFeedConfig, callbacks: VerticalFeedPoolCallbacks);
110
119
  get activeIndex(): number;
111
120
  get isLicensed(): boolean | null;
121
+ /** Indices of the CURRENT items array whose item is skip-listed. */
112
122
  get skippedIndices(): ReadonlySet<number>;
123
+ private isSkipped;
113
124
  updateItems(items: OGVerticalFeedItem[]): void;
114
125
  playerAt(index: number): OGPlayer | null;
115
126
  setFeedMuted(muted: boolean): void;