@vindral/web-sdk 4.5.1 → 4.6.0-11-g50f93209

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.
package/legacy.d.ts CHANGED
@@ -192,6 +192,18 @@ export interface DrmOptions {
192
192
  export type Media = "audio" | "video" | "audio+video";
193
193
  type WebCodecsHardwareAccelerationPreference = "no-preference" | "prefer-hardware" | "prefer-software";
194
194
  type DecoderType = "mse" | "webcodecs" | "wasm";
195
+ /** What the OS media controls show for the stream. Mirrors the `MediaMetadata` fields. */
196
+ export interface MediaSessionMetadata {
197
+ title?: string;
198
+ artist?: string;
199
+ album?: string;
200
+ /** Fetched by the operating system, so the URLs should not carry credentials. */
201
+ artwork?: Array<{
202
+ src: string;
203
+ sizes?: string;
204
+ type?: string;
205
+ }>;
206
+ }
195
207
  /**
196
208
  * Options for the Vindral instance
197
209
  *
@@ -275,13 +287,36 @@ export interface Options {
275
287
  */
276
288
  mseOpusEnabled?: boolean;
277
289
  /**
278
- * Enable or disable support for playing audio in the background for iOS devices.
290
+ * Keep playback running while the page is in the background. Enabled by default.
291
+ * When disabled, the session disconnects while hidden and reconnects when visible.
279
292
  *
280
- * Is false (disabled) by default.
293
+ * Browser media policies still apply; iOS Safari pauses hidden video regardless.
294
+ * Call `play()` again from a user gesture or media-control action to resume.
281
295
  *
282
- * Note: This may be enabled by default in a future (major) release
296
+ * When enabled, hidden sessions that are muted outside Picture-in-Picture or browser-paused
297
+ * are disconnected after the inactivity timeout; an explicit `pause()` disconnects at once.
298
+ */
299
+ backgroundPlayEnabled?: boolean;
300
+ /**
301
+ * @deprecated Use `backgroundPlayEnabled` instead.
302
+ *
303
+ * Legacy alias kept for backwards compatibility.
283
304
  */
284
305
  iosBackgroundPlayEnabled?: boolean;
306
+ /**
307
+ * Show the stream in the browser and OS media controls (notification, lock screen, toolbar) and
308
+ * let play and pause from there reach the SDK. Enabled by default.
309
+ *
310
+ * The SDK takes over `navigator.mediaSession` for the document, so a page with its own Media
311
+ * Session integration should disable this. On canvas playback paths a silent `<audio>` element is
312
+ * added to `document.body` for the controls to attach to.
313
+ */
314
+ mediaSessionEnabled?: boolean;
315
+ /**
316
+ * Title, artist and artwork for the media controls. Without it the browser derives them from the
317
+ * page. Read once at construction.
318
+ */
319
+ mediaSessionMetadata?: MediaSessionMetadata;
285
320
  /**
286
321
  * Enable or disable Adaptive Bit Rate. This allows for automatically adapting the incoming bit rate based on
287
322
  * the viewers bandwidth and thus avoiding buffering events. This also disables the
@@ -396,6 +431,9 @@ export interface Options {
396
431
  * This enables platform media features such as fullscreen and picture-in-picture
397
432
  * for non-MSE playback by routing the canvas output through a `<video>` element.
398
433
  *
434
+ * On iOS the audio then plays from its own element, so it continues in the background and under
435
+ * lock. After an extended pause from the lock screen, reopening Safari may be required to resume.
436
+ *
399
437
  * Disabled by default.
400
438
  */
401
439
  streamToMediaElementEnabled?: boolean;
@@ -656,6 +694,7 @@ export declare const CONNECTION_FAILED_AFTER_RETRIES_CODE = "connection_failed_w
656
694
  export declare const AUTHENTICATION_FAILED_CODE = "authentication_error";
657
695
  export declare const AUTHENTICATION_EXPIRED_CODE = "authentication_expired";
658
696
  export declare const CHANNEL_NOT_FOUND_CODE = "channel_not_found";
697
+ export declare const CHANNEL_NOT_LIVE_CODE = "channel_not_live";
659
698
  export declare const NO_INCOMING_DATA = "no_incoming_data_error";
660
699
  export declare const INACTIVITY_CODE = "connection_inactivity";
661
700
  export declare const DISCONNECTED_BY_EDGE = "disconnected_by_edge";
@@ -918,6 +957,13 @@ interface JitterModuleStatistics {
918
957
  audioJitter: JitterStatistics | undefined;
919
958
  videoJitter: JitterStatistics | undefined;
920
959
  }
960
+ type DecoyState = "detached" | "attached" | "playing" | "blocked";
961
+ interface MediaSessionModuleStatistics {
962
+ mediaSessionSupported: boolean;
963
+ mediaSessionOwner: boolean;
964
+ mediaSessionPlaybackState: MediaSessionPlaybackState;
965
+ decoyState?: DecoyState;
966
+ }
921
967
  interface MseModuleStatistics {
922
968
  quotaErrorCount: number;
923
969
  mediaSourceOpenTime: number;
@@ -1029,8 +1075,10 @@ interface SyncModuleStatistics {
1029
1075
  interface VideoPlayerStatistics {
1030
1076
  renderedFrameCount: number;
1031
1077
  rendererDroppedFrameCount: number;
1078
+ /** The platform's own view of the audio context, which a mute reading cannot tell apart. */
1079
+ audioContextState?: AudioContextState | "none";
1032
1080
  }
1033
- type ModuleStatistics = AdaptivityStatistics & BufferTimeStatistics & ConnectionStatistics & ConstraintCapStatistics & DecoderStatistics & DocumentStateModulesStatistics & IncomingDataModuleStatistics & JitterModuleStatistics & MseModuleStatistics & PlaybackModuleStatistics & QualityOfServiceModuleStatistics & RenditionsModuleStatistics & SyncModuleStatistics & TelemetryModuleStatistics & VideoPlayerStatistics;
1081
+ type ModuleStatistics = AdaptivityStatistics & BufferTimeStatistics & ConnectionStatistics & ConstraintCapStatistics & DecoderStatistics & DocumentStateModulesStatistics & IncomingDataModuleStatistics & JitterModuleStatistics & Partial<MediaSessionModuleStatistics> & MseModuleStatistics & PlaybackModuleStatistics & QualityOfServiceModuleStatistics & RenditionsModuleStatistics & SyncModuleStatistics & TelemetryModuleStatistics & VideoPlayerStatistics;
1034
1082
  /**
1035
1083
  * Contains internal statistics.
1036
1084
  *
@@ -1090,6 +1138,8 @@ export type Statistics = ModuleStatistics & ReturnType<UserAgentInformation["get
1090
1138
  streamToMediaElementEnabled?: boolean;
1091
1139
  /** @deprecated */
1092
1140
  iosMediaElementEnabled?: boolean;
1141
+ /** Whether the element carrying the audio is paused right now, on the sinks that have one. */
1142
+ mediaElementPaused?: boolean;
1093
1143
  /**
1094
1144
  * Average bitrate for the entire session in bits/second.
1095
1145
  */
@@ -1111,11 +1161,49 @@ export type Statistics = ModuleStatistics & ReturnType<UserAgentInformation["get
1111
1161
  */
1112
1162
  timeshiftAdjustmentsPerMinute: number;
1113
1163
  };
1164
+ /**
1165
+ * Represents a Vindral client instance
1166
+ *
1167
+ * The most most essential methods when using the Vindral class are:
1168
+ *
1169
+ * - connect() - this has to be called to actually start connecting
1170
+ * - attach() - to attach the Vindral video view to the DOM so that users can see it
1171
+ * - userInput() - to activate audio on browsers that require a user gesture to play audio
1172
+ * - unload() - unloads the instance, its very important that this is called when cleaning up the Vindral instance, otherwise background timers may leak.
1173
+ *
1174
+ * The Vindral instance will emit a variety of events during its lifetime. Use .on("event-name", callback) to listen to these events.
1175
+ * See [[PublicVindralEvents]] for the events types that can be emitted.
1176
+ *
1177
+ * ```typescript
1178
+ * // minimal configuration of a Vindral client instance
1179
+ * const instance = new Vindral({
1180
+ * url: "https://lb.cdn.vindral.com",
1181
+ * channelId: "vindral_demo1_ci_099ee1fa-80f3-455e-aa23-3d184e93e04f",
1182
+ * })
1183
+ *
1184
+ * // Will be called when timed metadata is received
1185
+ * instance.on("metadata", console.log)
1186
+ *
1187
+ * // Will be called when a user interaction is needed to activate audio
1188
+ * instance.on("needs user input", console.log)
1189
+ *
1190
+ * // Start connecting to the cdn
1191
+ * instance.connect()
1192
+ *
1193
+ * // Attach the video view to the DOM
1194
+ * instance.attach(document.getElementById("root"))
1195
+ *
1196
+ * // When done with the instance
1197
+ * instance.unload()
1198
+ * ```
1199
+ */
1114
1200
  export declare class Vindral extends Emitter<PublicVindralEvents> {
1115
1201
  #private;
1116
1202
  private static MAX_POOL_SIZE;
1117
- private static DISCONNECT_TIMEOUT;
1118
1203
  private static REMOVE_CUE_THRESHOLD;
1204
+ private static BACKGROUND_INACTIVITY_DISCONNECT_TIMEOUT;
1205
+ /** How long a pause on a visible page may still turn out to be the platform backgrounding us. */
1206
+ private static VISIBLE_PAUSE_DISCONNECT_DELAY;
1119
1207
  /**
1120
1208
  * Picture in picture
1121
1209
  */
@@ -1158,6 +1246,10 @@ export declare class Vindral extends Emitter<PublicVindralEvents> {
1158
1246
  private offscreenCanvasElement?;
1159
1247
  private webCodecsRenditionSupport;
1160
1248
  private hasCalledConnect;
1249
+ private hasRequestedAudioSession;
1250
+ private isUserPaused;
1251
+ /** Only the sinks that carry the audio report this, so a paused element here means silence. */
1252
+ private isMediaElementPaused;
1161
1253
  private latestEmittedLanguages;
1162
1254
  private wakeLock;
1163
1255
  private pool;
@@ -1172,7 +1264,8 @@ export declare class Vindral extends Emitter<PublicVindralEvents> {
1172
1264
  private readonly unextractableSamplesWarned;
1173
1265
  private sizes;
1174
1266
  private isSuspended;
1175
- private disconnectTimeout;
1267
+ private inactivityDisconnectInterval;
1268
+ private visiblePauseTimeout;
1176
1269
  private offscreenSubtitleInterval?;
1177
1270
  constructor(options: Options);
1178
1271
  /**
@@ -1474,6 +1567,8 @@ export declare class Vindral extends Emitter<PublicVindralEvents> {
1474
1567
  private isSupportedVideoCodecProfile;
1475
1568
  private supportedAudioCodecs;
1476
1569
  private initializeDecodingModule;
1570
+ private capturesCanvasOnWebKit;
1571
+ private connectWebKitVideoOutput;
1477
1572
  /**
1478
1573
  * Set up a polling interval that forwards subtitle cues to the decoder worker
1479
1574
  * when OffscreenCanvas rendering is active. This is needed because the canvas modules'
@@ -1505,6 +1600,12 @@ export declare class Vindral extends Emitter<PublicVindralEvents> {
1505
1600
  * Pauses the stream. Call .play() to resume playback again.
1506
1601
  */
1507
1602
  pause: () => void;
1603
+ /**
1604
+ * A browser-initiated decoy pause means an interruption (another app took the focus, headphones
1605
+ * unplugged), so give the audio up rather than play over it. WebKit holds the decoy on purpose.
1606
+ */
1607
+ private yieldToInterruption;
1608
+ private fromNativeControls;
1508
1609
  private registerDebugInstance;
1509
1610
  /**
1510
1611
  *
@@ -1535,8 +1636,24 @@ export declare class Vindral extends Emitter<PublicVindralEvents> {
1535
1636
  */
1536
1637
  getStatistics: () => Statistics;
1537
1638
  private resetModules;
1639
+ private disconnectForInactivePage;
1640
+ private scheduleInactivityDisconnect;
1641
+ /**
1642
+ * iOS pauses the element a moment before it hides the page, and that pause reaches us while the
1643
+ * page still reports visible. Acting on it at once tears the session down, so every app switch
1644
+ * comes back as a full reconnect. Wait for the hide that follows a platform pause instead.
1645
+ * Nowhere else pauses before hiding, so there a visible pause is the viewer's at once.
1646
+ */
1647
+ private onVisiblePause;
1648
+ private disconnectForVisiblePause;
1649
+ private clearVisiblePauseDisconnect;
1650
+ private clearInactivityDisconnect;
1651
+ private isHiddenWithBackgroundPlayback;
1652
+ private isVideoOnScreen;
1653
+ private isHiddenWithBackgroundPlayDisabled;
1538
1654
  private suspend;
1539
1655
  private unsuspend;
1656
+ private resumeModules;
1540
1657
  private getRuntimeInfo;
1541
1658
  private getPlaybackBufferState;
1542
1659
  private onMediaElementBufferState;
@@ -1550,6 +1667,20 @@ export declare class Vindral extends Emitter<PublicVindralEvents> {
1550
1667
  private get targetSubscription();
1551
1668
  private timeToFirstFrame;
1552
1669
  private resolvedDecoder;
1670
+ /**
1671
+ * On mobile Chromium with WebCodecs audio the play action arrives but no sound comes back until
1672
+ * the page is visible, so a hidden pause is a stop there. Reads the decoder in use.
1673
+ */
1674
+ private hiddenPauseIsStop;
1675
+ private usesSplitCanvasAudio;
1676
+ private isWebKitCanvasPath;
1677
+ private holdsDecoyWhilePaused;
1678
+ private streamsToMediaElement;
1679
+ /**
1680
+ * Safari 16.4+: a playback audio session keeps Web Audio alive in the background. Document-wide
1681
+ * and not restorable, so it waits for playback to be wanted rather than for construction.
1682
+ */
1683
+ private requestAudioSessionOnce;
1553
1684
  private willUseMediaSource;
1554
1685
  private shouldUseWebCodecs;
1555
1686
  private isWebCodecsVideoAvailable;
@@ -1560,6 +1691,8 @@ export declare class Vindral extends Emitter<PublicVindralEvents> {
1560
1691
  * is the optimal rendering path for WebCodecs (worker renders VideoFrames directly).
1561
1692
  */
1562
1693
  private shouldUseOffscreenCanvas;
1694
+ private isBackgroundPlayEnabled;
1695
+ private shouldAllowBackgroundPlayback;
1563
1696
  }
1564
1697
  interface TelemetryModuleStatistics {
1565
1698
  /**
@@ -1731,7 +1864,8 @@ export interface PublicVindralEvents {
1731
1864
  * Is emitted during connection whether the channel is live or not.
1732
1865
  *
1733
1866
  * If the channel is not live, the Vindral instance will try to reconnect until the `reconnectHandler`
1734
- * determines that no more retries should be made.
1867
+ * determines that no more retries should be made. Each attempt also emits a non-fatal `error` with
1868
+ * the code `channel_not_live`.
1735
1869
  *
1736
1870
  * Note: If the web-sdk is instantiated at the same time as you are starting the stream it is possible
1737
1871
  * that this emits false until the started state has propagated through the system.
@@ -1800,6 +1934,10 @@ export interface CastSenderEvents {
1800
1934
  * When the remote connection receives a server wallclock time event
1801
1935
  */
1802
1936
  "server wallclock time": number;
1937
+ /**
1938
+ * When receiver availability changes. True when at least one receiver is reachable.
1939
+ */
1940
+ "devices available": boolean;
1803
1941
  }
1804
1942
  /**
1805
1943
  * Used for initializing the CastSender
@@ -1826,6 +1964,9 @@ export declare class CastSender extends Emitter<CastSenderEvents> {
1826
1964
  private state;
1827
1965
  private config;
1828
1966
  private unloaded;
1967
+ private logger;
1968
+ /** Last reported availability, undefined until the Cast API tells us */
1969
+ private devicesAvailable;
1829
1970
  constructor(config: CastConfig);
1830
1971
  /**
1831
1972
  * True if the instance is casting right now
@@ -1885,6 +2026,9 @@ export declare class CastSender extends Emitter<CastSenderEvents> {
1885
2026
  private send;
1886
2027
  private onMessage;
1887
2028
  private onSessionStarted;
2029
+ private onCastStateChanged;
2030
+ /** Emits on change only, since connecting and connected are all "available" */
2031
+ private updateDevicesAvailable;
1888
2032
  private onSessionStateChanged;
1889
2033
  private getInstance;
1890
2034
  private getSession;