@oddin-gg/havik-player 1.2.0 → 1.3.0

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/docs/API.md CHANGED
@@ -148,21 +148,31 @@ See [`WaitForLive`](#waitforlive) for tuning, and [`createPlayer`](#createplayer
148
148
  parameters. Use the **same api‑key** to resolve playback and to fetch licenses.
149
149
  The managed and iframe players handle this for you.
150
150
 
151
- > **FairPlay (Safari/iOS):** this version does not implement the FairPlay EME
152
- > handshake. Safari uses native HLS passthrough, which only plays DRM content where the
153
- > operating system already trusts the license/certificate origins. Full FairPlay support
154
- > is planned. Widevine browsers are fully supported.
151
+ > **FairPlay (Safari/iOS):** implemented on the hls.js engine. When the playback
152
+ > descriptor carries FairPlay material and the platform is WebKit, the player
153
+ > offers `com.apple.fps` alongside Widevine: hls.js fetches the application
154
+ > certificate, handles the encrypted event, POSTs the SPC to the signed license
155
+ > URL and applies the returned CKC. No configuration is needed beyond the same
156
+ > api‑key used for playback.
157
+ >
158
+ > **iOS third‑party browsers (Chrome/Firefox/Edge on iOS):** iOS has no
159
+ > Widevine CDM, so protected playback in these browsers depends entirely on
160
+ > whether the WebKit build grants them FairPlay. Don't assume either way —
161
+ > call [`detectDrmSupport`](#detectdrmsupport) and let the probe decide, so
162
+ > viewers see an accurate message instead of a black player.
155
163
 
156
164
  ### Browser support
157
165
 
158
- | Browser | Engine | DRM |
159
- | -------------------------------- | ---------- | ---------------------------------------------- |
160
- | Chrome, Edge (desktop & Android) | hls.js | Widevine ✓ |
161
- | Firefox | hls.js | Widevine ✓ |
162
- | Safari / iOS | native HLS | FairPlay — native passthrough only (see above) |
166
+ | Browser | Engine | DRM |
167
+ | -------------------------------- | --------------------- | -------------------------------------------------- |
168
+ | Chrome, Edge (desktop & Android) | hls.js | Widevine ✓ |
169
+ | Firefox | hls.js | Widevine ✓ |
170
+ | Safari (macOS, iOS 17.1+) | hls.js (via MMS) | FairPlay ✓ (`com.apple.fps`) |
171
+ | Older Safari / iOS | native HLS (fallback) | FairPlay — OS passthrough only |
172
+ | Chrome/Firefox/Edge **on iOS** | hls.js | probe with `detectDrmSupport` (no Widevine on iOS) |
163
173
 
164
- Requires a browser with Media Source Extensions and Encrypted Media Extensions
165
- (all evergreen browsers).
174
+ Requires a browser with Media Source Extensions (or ManagedMediaSource) and
175
+ Encrypted Media Extensions (all evergreen browsers).
166
176
 
167
177
  ---
168
178
 
@@ -217,7 +227,7 @@ player.on('error', (e) => console.error(e.code, e.httpStatus, e.message));
217
227
  | `errorMessage` | `string` | the error's message | Message on the fatal-error card (otherwise the error's own message). |
218
228
  | `waitForLive` | [`WaitForLive`](#waitforlive) | — | Arm on an upcoming match and auto‑attach at go‑live. |
219
229
  | `lowLatency` | `'auto' \| boolean` | `'auto'` | `'auto'` lets the player decide from the manifest. |
220
- | `engine` | `'auto' \| 'hls' \| 'native'` | `'auto'` | `'auto'` picks native HLS on Safari, hls.js elsewhere. |
230
+ | `engine` | `'auto' \| 'hls' \| 'native'` | `'auto'` | `'auto'` prefers hls.js wherever it is supported — including modern Safari/iOS via ManagedMediaSource — and falls back to native HLS otherwise. |
221
231
  | `userId` | `string` | — | Optional viewer id (sent as `X-User-Id` on license requests). |
222
232
  | `debug` | `boolean` | `false` | Turn on hls.js's internal console logging — fragment/part scheduling, ABR switches, EME key sessions and license traffic. For diagnosing a playback problem with Oddin support; leave it off in production (it logs on every fragment, and the EME lines include license request/response metadata). hls.js engine only. `hlsConfig.debug` still wins if you set both. |
223
233
  | `hlsConfig` | `Partial<HlsConfig>` | — | Escape hatch for advanced hls.js tuning. |
@@ -338,23 +348,23 @@ Notes:
338
348
 
339
349
  `on(event, cb)` — the payload type depends on the event ([`PlayerEventMap`](#playereventmap)):
340
350
 
341
- | Event | Payload | Fires when |
342
- | ------------------ | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
343
- | `ready` | `void` | The manifest is parsed and the player is attached. |
344
- | `waiting` | [`WaitState`](#waitstate) | Armed and waiting for go‑live; carries the next‑poll countdown. |
345
- | `playing` | `void` | Playback is progressing. |
346
- | `buffering` | `void` | The player is rebuffering. |
347
- | `paused` | `void` | Playback is paused. |
348
- | `ended` | `void` | The live stream ended. Playback is stopped and the last frame is kept. |
349
- | `error` | [`PlaybackError`](#playbackerror) | A fatal error occurred (playback stopped). |
350
- | `warning` | [`PlaybackError`](#playbackerror) | A non-fatal condition (e.g. FairPlay passthrough, a failed license refresh). Playback continues. |
351
- | `autoplayblocked` | `void` | The browser blocked autoplay (`NotAllowedError`). Show a tap-to-play / tap-to-unmute affordance, then call `player.play()` from the gesture. |
352
- | `qualitychange` | `number` | Active rendition changed (level index, `-1` = auto). |
353
- | `audiotrackchange` | `number` | Active audio track changed (track id). |
354
- | `texttrackchange` | `number` | Active text track changed (track id, `-1` = off). |
355
- | `pipchange` | `boolean` | Picture-in-Picture entered (`true`) or exited (`false`). |
356
- | `stats` | [`PlaybackStats`](#playbackstats) | Periodic stats sample (`statsIntervalMs`). |
357
- | `statechange` | [`PlayerState`](#playerstate) | Any state transition. |
351
+ | Event | Payload | Fires when |
352
+ | ------------------ | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
353
+ | `ready` | `void` | The manifest is parsed and the player is attached. |
354
+ | `waiting` | [`WaitState`](#waitstate) | Armed and waiting for go‑live. On the polling path it fires per poll with the countdown; on the SSE arming path it fires once, just after `createPlayer` resolves (with `ttkMs` when the server sent a kickoff time). |
355
+ | `playing` | `void` | Playback is progressing. |
356
+ | `buffering` | `void` | The player is rebuffering. |
357
+ | `paused` | `void` | Playback is paused. |
358
+ | `ended` | `void` | The live stream ended. Playback is stopped and the last frame is kept. |
359
+ | `error` | [`PlaybackError`](#playbackerror) | A fatal error occurred (playback stopped). |
360
+ | `warning` | [`PlaybackError`](#playbackerror) | A non-fatal condition (e.g. FairPlay passthrough, a failed license refresh). Playback continues. |
361
+ | `autoplayblocked` | `void` | The browser blocked autoplay (`NotAllowedError`). Show a tap-to-play / tap-to-unmute affordance, then call `player.play()` from the gesture. |
362
+ | `qualitychange` | `number` | Active rendition changed (level index, `-1` = auto). |
363
+ | `audiotrackchange` | `number` | Active audio track changed (track id). |
364
+ | `texttrackchange` | `number` | Active text track changed (track id, `-1` = off). |
365
+ | `pipchange` | `boolean` | Picture-in-Picture entered (`true`) or exited (`false`). |
366
+ | `stats` | [`PlaybackStats`](#playbackstats) | Periodic stats sample (`statsIntervalMs`). |
367
+ | `statechange` | [`PlayerState`](#playerstate) | Any state transition. |
358
368
 
359
369
  <a id="controls--theming"></a>
360
370
 
@@ -550,6 +560,45 @@ hls.loadSource(stream.manifestUrl);
550
560
  hls.attachMedia(videoElement);
551
561
  ```
552
562
 
563
+ <a id="detectdrmsupport"></a>
564
+
565
+ ### `detectDrmSupport`
566
+
567
+ ```ts
568
+ function detectDrmSupport(opts?: {
569
+ systems?: DrmSystem[]; // default: ['widevine', 'fairplay'], probed in order
570
+ timeoutMs?: number; // OVERALL budget for the whole call, default 5000
571
+ }): Promise<DrmSupportResult>;
572
+
573
+ interface DrmSupportResult {
574
+ verdict: 'supported' | 'no-cdm' | 'insecure-context' | 'blocked-by-policy' | 'probe-timeout';
575
+ system?: DrmSystem; // first working system, when supported
576
+ detail: Partial<Record<DrmSystem, string>>; // per-system outcome, for support bundles
577
+ }
578
+ ```
579
+
580
+ Answers "can protected playback work in this browser at all?" so you can show
581
+ an accurate message instead of a black player. It probes
582
+ `requestMediaKeySystemAccess` **and** `createMediaKeys()` per system (access
583
+ alone is not proof), with configs matching what the engine will actually
584
+ request, bounded by a timeout. It never rejects.
585
+
586
+ Rules of use:
587
+
588
+ - **Call it lazily, cache the verdict yourself** — only once you know the
589
+ content is DRM‑protected (`descriptor.drmEnabled`). The EME spec allows the
590
+ probe to trigger consent prompts or CDM downloads, and results are not
591
+ memoized; never run it on page load, and never probe for a clear stream.
592
+ - **Probe, don't UA‑sniff.** The verdict is the playability gate; use the UA
593
+ only to word the message.
594
+ - `insecure-context` means EME is unavailable by spec — typical when testing
595
+ over `http://<LAN‑IP>`; `localhost` is fine.
596
+ - `blocked-by-policy` means an embedding problem (an iframe missing
597
+ `allow="encrypted-media"`), not an unsupported browser — surface it to the
598
+ integrator, not the viewer.
599
+ - `supported` is necessary, not sufficient: licensing, CORS onboarding and
600
+ packaging still apply.
601
+
553
602
  <a id="getdeviceid"></a>
554
603
 
555
604
  ### `getDeviceId` / `clearDeviceId`
@@ -1123,16 +1172,17 @@ published at [status.oddin-video.gg](https://status.oddin-video.gg).
1123
1172
 
1124
1173
  ## Troubleshooting
1125
1174
 
1126
- | Symptom | Likely cause |
1127
- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
1128
- | `NETWORK` error on every call; preflight fails in devtools | Your origin isn't on the key's allowed‑origins / DRM CORS allow‑list. See [Onboarding](#onboarding--cors). |
1129
- | `UNAUTHORIZED` (401) | Wrong/expired api‑key, or wrong `baseUrl` environment (test vs prod key). |
1130
- | `FORBIDDEN` (403) on license requests | The license URL was modified, or a different api‑key was used for the license than for playback. POST the URL verbatim with the same key. |
1131
- | `NOT_FOUND` (404) for a match you can see | The key isn't entitled to that tournament (404 is intentionally indistinguishable from "unknown"). |
1132
- | Player stays in `waiting` forever | The match hasn't gone live yet; the SDK is polling. Set `waitForLive.timeoutMs` to bound it. |
1133
- | Autoplay doesn't start | Browser sound policy — set `muted: true` for autoplay, or start playback from a user gesture. |
1134
- | Black video on Safari with DRM | FairPlay EME isn't implemented in this version (Widevine browsers are supported). |
1135
- | Black video, `levelHeight`/`droppedFrames` stuck | Check the browser console for an EME/CDM error and verify DRM CORS onboarding. |
1175
+ | Symptom | Likely cause |
1176
+ | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1177
+ | `NETWORK` error on every call; preflight fails in devtools | Your origin isn't on the key's allowed‑origins / DRM CORS allow‑list. See [Onboarding](#onboarding--cors). |
1178
+ | `UNAUTHORIZED` (401) | Wrong/expired api‑key, or wrong `baseUrl` environment (test vs prod key). |
1179
+ | `FORBIDDEN` (403) on license requests | The license URL was modified, or a different api‑key was used for the license than for playback. POST the URL verbatim with the same key. |
1180
+ | `NOT_FOUND` (404) for a match you can see | The key isn't entitled to that tournament (404 is intentionally indistinguishable from "unknown"). |
1181
+ | Player stays in `waiting` forever | The match hasn't gone live yet; the SDK is polling. Set `waitForLive.timeoutMs` to bound it. |
1182
+ | Autoplay doesn't start | Browser sound policy — set `muted: true` for autoplay, or start playback from a user gesture. |
1183
+ | Black video on Safari with DRM | Check the console for an EME/CDM error; verify the origin is on the DRM CORS allow‑list. On very old Safari the native fallback plays only OS‑trusted passthrough. |
1184
+ | Black video in Chrome/Firefox/Edge **on iOS** | iOS has no Widevine CDM, and FairPlay availability outside Safari varies by WebKit build. Call `detectDrmSupport()`: on `no-cdm`, tell the viewer this browser can't play protected streams. |
1185
+ | Black video, `levelHeight`/`droppedFrames` stuck | Check the browser console for an EME/CDM error and verify DRM CORS onboarding. |
1136
1186
 
1137
1187
  Include `PlaybackError.requestId` (when present) when contacting support — it correlates
1138
1188
  to server‑side logs.
package/embed/index.html CHANGED
@@ -18,7 +18,7 @@
18
18
  apiKey, parentOrigin). When injected, the URL query string is IGNORED for
19
19
  security; the api-key must NOT travel in the query in production.
20
20
  -->
21
- <script src="https://cdn.jsdelivr.net/npm/@oddin-gg/havik-player@1.2.0/dist/havik-player.global.js" integrity="sha384-xlhuEyjXmH+i5h/uDHbq7qDClRNwVVbbm6ZNstinanu2eLw5TwtP300ByLacLnbv" crossorigin="anonymous"></script>
21
+ <script src="https://cdn.jsdelivr.net/npm/@oddin-gg/havik-player@1.3.0/dist/havik-player.global.js" integrity="sha384-F3IxzdIz3HFRw1ocTFFP1C3j1Q49vcrjE53xL+Bzf2rrS/+oSTT5LtFGbz9LxUwx" crossorigin="anonymous"></script>
22
22
  <script>
23
23
  HavikPlayer.bootEmbed();
24
24
  </script>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oddin-gg/havik-player",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Havik HTML5 player SDK — LL-HLS + DRM playback for Oddin live streams (managed, bring-your-own-player, and iframe-embed modes).",
5
5
  "type": "module",
6
6
  "sideEffects": false,