@hevcjs/shaka-plugin 0.3.5 → 0.4.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/README.md CHANGED
@@ -54,6 +54,31 @@ registerHevcTransmuxer(shaka, { adaptiveCompute: false });
54
54
 
55
55
  Options passed at `attachComputeAware` time merge on top of options passed at register time — convenient when `onObservation` is only known once the UI exists.
56
56
 
57
+ ## Performance & tuning
58
+
59
+ Shaka 4.x's `Transmuxer.transmux()` contract returns one `Uint8Array` per segment, so the buffered range can only grow in whole-segment jumps. On hardware where WASM transcoding runs near real time, the playback head skirts the edge of that range: playback stutters in a few-second rhythm even though the buffer is contiguous and nothing is out of spec. The dash.js plugin does not show this, because it appends transcoded chunks to MSE progressively as the encoder emits them.
60
+
61
+ A deeper buffer gives transcoding room to stay ahead:
62
+
63
+ ```js
64
+ import { registerHevcTransmuxer, recommendedBufferConfig } from '@hevcjs/shaka-plugin';
65
+
66
+ const player = new shaka.Player();
67
+ player.configure(recommendedBufferConfig()); // before load()
68
+ await player.load(manifestUrl);
69
+ ```
70
+
71
+ That raises `streaming.bufferingGoal` to 30s, against Shaka's default of 10. Startup takes longer to fill the buffer, in exchange for playback that absorbs slower-than-real-time stretches instead of stalling on them. `configure()` deep-merges, so the rest of your configuration is untouched; only a later `configure()` setting `bufferingGoal` itself would override it.
72
+
73
+ It leaves `rebufferingGoal` alone on purpose. That setting decides whether Shaka gates playback on buffer depth at all — it defaults to 0 on Shaka 5, where the buffer poller never runs and the playback rate is never held back. Turning it on would mean that a device transcoding at around real time freezes until the goal is re-accumulated, trading a stutter for a longer hard stall. Raise it only if you have measured that it helps on your content and hardware.
74
+
75
+ This is a mitigation, not a cure: it buys headroom, it does not make transcoding faster. If `speedX` stays below 1 for long enough, the buffer drains whatever its depth. Two things help there:
76
+
77
+ - **Use the Worker** (`workerUrl`), which keeps decoding off the main thread.
78
+ - **Leave compute-aware ABR on** (the default), so the variant ceiling drops when the device cannot keep up.
79
+
80
+ `subscribeSegmentStat` reports the per-segment `speedX` if you want to see where a given device actually lands.
81
+
57
82
  ## How It Works
58
83
 
59
84
  Shaka exposes a `TransmuxerEngine` that lets plugins convert one container/codec into another before MSE sees the bytes. This package follows the same pattern as Shaka's built-in `AacTransmuxer`:
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Buffer tuning for HEVC playback through the transmuxer.
3
+ *
4
+ * Shaka 4.x's `Transmuxer.transmux()` returns one `Uint8Array` per segment,
5
+ * so the buffered range can only grow in whole-segment jumps. When WASM
6
+ * transcoding runs near real time, the playback head skirts the edge of that
7
+ * range and playback stutters even though the buffer is contiguous and no
8
+ * spec is violated. A deeper buffer gives transcoding room to stay ahead.
9
+ *
10
+ * See the "Performance & tuning" section of the plugin README.
11
+ */
12
+ /** A fragment of Shaka player configuration, for `player.configure()`. */
13
+ export interface ShakaBufferConfig {
14
+ streaming: {
15
+ /** Seconds of content to buffer ahead. Shaka's default is 10. */
16
+ bufferingGoal: number;
17
+ };
18
+ }
19
+ /**
20
+ * Buffer settings recommended when transcoding HEVC through this plugin.
21
+ *
22
+ * Merge into the player configuration before `load()`:
23
+ *
24
+ * ```ts
25
+ * player.configure(recommendedBufferConfig());
26
+ * ```
27
+ *
28
+ * Only `bufferingGoal` is touched, deliberately. `rebufferingGoal` decides
29
+ * whether Shaka gates playback on buffer depth at all: it defaults to 0 on
30
+ * Shaka 5, where 0 means the buffer poller never starts and the playback rate
31
+ * is never held back. Raising it would switch that behaviour on, and on a
32
+ * device transcoding at around real time — the case this config exists for —
33
+ * a brief dip would then freeze playback until several seconds had been
34
+ * re-accumulated. That trades a stutter for a longer hard stall, so leave it
35
+ * at whatever the application has set.
36
+ */
37
+ export declare function recommendedBufferConfig(): ShakaBufferConfig;
38
+ //# sourceMappingURL=buffer-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"buffer-config.d.ts","sourceRoot":"","sources":["../src/buffer-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,0EAA0E;AAC1E,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE;QACT,iEAAiE;QACjE,aAAa,EAAE,MAAM,CAAC;KACvB,CAAC;CACH;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,uBAAuB,IAAI,iBAAiB,CAS3D"}
package/dist/index.d.ts CHANGED
@@ -45,6 +45,13 @@
45
45
  * ```ts
46
46
  * player.configure({ mediaSource: { forceTransmux: true } });
47
47
  * ```
48
+ *
49
+ * On hardware where WASM transcoding runs near real time, a deeper buffer
50
+ * keeps playback from stuttering at the edge of the buffered range:
51
+ *
52
+ * ```ts
53
+ * player.configure(recommendedBufferConfig());
54
+ * ```
48
55
  */
49
56
  import type { HevcTransmuxerConfig } from "./transmuxer.js";
50
57
  import type { ShakaComputeAwareOptions } from "./compute-aware.js";
@@ -52,6 +59,8 @@ export { HevcTransmuxer } from "./transmuxer.js";
52
59
  export type { TransmuxOutput, HevcTransmuxerConfig } from "./transmuxer.js";
53
60
  export { attachShakaComputeAware } from "./compute-aware.js";
54
61
  export type { ShakaComputeAwareOptions } from "./compute-aware.js";
62
+ export { recommendedBufferConfig } from "./buffer-config.js";
63
+ export type { ShakaBufferConfig } from "./buffer-config.js";
55
64
  export { subscribeSegmentStat } from "@hevcjs/core";
56
65
  export type { SegmentPerfStat } from "@hevcjs/core";
57
66
  type ShakaNamespace = any;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AAGH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAE5D,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,oBAAoB,CAAC;AAEnE,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjD,YAAY,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC5E,OAAO,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAC7D,YAAY,EAAE,wBAAwB,EAAE,MAAM,oBAAoB,CAAC;AAInE,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACpD,YAAY,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAGpD,KAAK,cAAc,GAAG,GAAG,CAAC;AAE1B,KAAK,WAAW,GAAG,GAAG,CAAC;AAEvB;;;;;;;GAOG;AACH,MAAM,WAAW,qBAAsB,SAAQ,oBAAoB;IACjE;;;;;;;;;;;OAWG;IACH,eAAe,CAAC,EAAE,OAAO,GAAG,wBAAwB,CAAC;CACtD;AAED;;;;;GAKG;AACH,MAAM,WAAW,qBAAqB;IACpC,IAAI,IAAI,CAAC;IACT,4CAA4C;IAC5C,UAAU,IAAI,IAAI,CAAC;IACnB;;;;;;;;;;OAUG;IACH,kBAAkB,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,wBAAwB,GAAG,MAAM,IAAI,CAAC;CACzF;AAOD;;;;;;;;;;;GAWG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,cAAc,EACrB,MAAM,GAAE,qBAA0B,GACjC,qBAAqB,CAwCvB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AAGH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAE5D,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,oBAAoB,CAAC;AAEnE,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjD,YAAY,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC5E,OAAO,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAC7D,YAAY,EAAE,wBAAwB,EAAE,MAAM,oBAAoB,CAAC;AACnE,OAAO,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAC7D,YAAY,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAI5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACpD,YAAY,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAGpD,KAAK,cAAc,GAAG,GAAG,CAAC;AAE1B,KAAK,WAAW,GAAG,GAAG,CAAC;AAEvB;;;;;;;GAOG;AACH,MAAM,WAAW,qBAAsB,SAAQ,oBAAoB;IACjE;;;;;;;;;;;OAWG;IACH,eAAe,CAAC,EAAE,OAAO,GAAG,wBAAwB,CAAC;CACtD;AAED;;;;;GAKG;AACH,MAAM,WAAW,qBAAqB;IACpC,IAAI,IAAI,CAAC;IACT,4CAA4C;IAC5C,UAAU,IAAI,IAAI,CAAC;IACnB;;;;;;;;;;OAUG;IACH,kBAAkB,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,wBAAwB,GAAG,MAAM,IAAI,CAAC;CACzF;AAOD;;;;;;;;;;;GAWG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,cAAc,EACrB,MAAM,GAAE,qBAA0B,GACjC,qBAAqB,CAwCvB"}
package/dist/index.js CHANGED
@@ -230,6 +230,18 @@ function applyCap(player, ladder, capIndex) {
230
230
  player.configure({ abr: { restrictions } });
231
231
  }
232
232
 
233
+ // src/buffer-config.ts
234
+ function recommendedBufferConfig() {
235
+ return {
236
+ streaming: {
237
+ // 30s covers ~15 two-second segments: enough that a stretch of
238
+ // slower-than-real-time transcoding drains the buffer instead of
239
+ // letting the playback head catch up with it. Shaka's default is 10.
240
+ bufferingGoal: 30
241
+ }
242
+ };
243
+ }
244
+
233
245
  // src/index.ts
234
246
  import { subscribeSegmentStat as subscribeSegmentStat2 } from "@hevcjs/core";
235
247
  var HEVC_MIME_TYPES = [
@@ -290,6 +302,7 @@ function makeHandle(unregister, adaptive) {
290
302
  export {
291
303
  HevcTransmuxer,
292
304
  attachShakaComputeAware,
305
+ recommendedBufferConfig,
293
306
  registerHevcTransmuxer,
294
307
  subscribeSegmentStat2 as subscribeSegmentStat
295
308
  };
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/transmuxer.ts","../src/compute-aware.ts","../src/index.ts"],"sourcesContent":["/**\n * HEVC Transmuxer for Shaka Player.\n *\n * Implements the `shaka.extern.Transmuxer` interface so Shaka can ingest\n * HEVC/H.265 fMP4 segments on browsers that lack native HEVC support.\n * Uses `@hevcjs/core` SegmentTranscoder to decode HEVC and re-encode to\n * H.264 fMP4 that the browser's MSE can play.\n *\n * Modeled after `lib/transmuxer/aac_transmuxer.js` in shaka-player.\n */\n\nimport {\n SegmentTranscoder,\n TranscodeWorkerClient,\n hevcMimeToH264Codec,\n isMuxedHevcMime,\n} from \"@hevcjs/core\";\nimport type { SegmentTranscoderConfig } from \"@hevcjs/core\";\n\n/**\n * Config accepted by `HevcTransmuxer` (and forwarded by `registerHevcTransmuxer`).\n * When `workerUrl` is set, transcoding runs inside a Web Worker; otherwise\n * the HEVC decode + H.264 encode pipeline runs on the main thread.\n */\nexport interface HevcTransmuxerConfig extends SegmentTranscoderConfig {\n /** URL to the transcode worker script. When set, transcoding runs off main thread. */\n workerUrl?: string;\n}\n\n// Loose typing while we don't pull `shaka.extern.*` into the build.\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype ShakaStream = any;\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype ShakaSegmentReference = any;\n\n/**\n * Return type of `HevcTransmuxer.transmux`. Compatible with both Shaka 4.x\n * (which expects a raw `Uint8Array` and passes it straight to MSE) and 5+\n * (which checks `ArrayBuffer.isView` and falls back to `{data, init}`\n * when the value is a plain object). Returning a `Uint8Array` is the\n * common subset that works on every supported Shaka version.\n */\nexport type TransmuxOutput = Uint8Array;\n\nconst HEVC_MIME_PATTERN = /^video\\/mp4\\s*;.*codecs=\"?(hev1|hvc1)/i;\n\n/**\n * 8-byte ISO BMFF `free` box (size + type, no payload). Spec-compliant\n * padding that any MP4 parser ignores. Used as a stand-in when we need\n * to return *something* to Shaka but have nothing real to emit yet —\n * `appendBuffer(emptyUint8Array)` throws \"Overload resolution failed\"\n * on Chrome, so we can't return zero-length buffers.\n */\nconst FREE_BOX_8B = new Uint8Array([\n 0, 0, 0, 8, // size = 8\n 0x66, 0x72, 0x65, 0x65, // 'free'\n]);\n\n/**\n * Sniff whether a buffer starts with an ISO BMFF init segment.\n * Init segments begin with the `ftyp` box; media segments begin with\n * `moof` (or `styp` followed by `moof`).\n *\n * Box header layout: 4 bytes big-endian size, 4 bytes ASCII type.\n */\nexport function isInitSegment(bytes: Uint8Array): boolean {\n if (bytes.length < 8) return false;\n const boxType = String.fromCharCode(\n bytes[4]!,\n bytes[5]!,\n bytes[6]!,\n bytes[7]!,\n );\n return boxType === \"ftyp\";\n}\n\nexport class HevcTransmuxer {\n private readonly originalMimeType_: string;\n private readonly transcoderConfig_: HevcTransmuxerConfig;\n private transcoder_: SegmentTranscoder | TranscodeWorkerClient | null = null;\n private initPromise_: Promise<void> | null = null;\n private pendingHevcInit_: Uint8Array | null = null;\n private h264InitEmitted_ = false;\n // Cache for the last HEVC init segment we processed and the H.264 init we\n // produced for it. Shaka can call transmux() with the same init bytes\n // multiple times during a session (variant probing, transmuxer re-checks);\n // we must not tear down the live encoder on those redundant calls or\n // playback stalls while the encoder rebuilds. A real representation change\n // arrives with different bytes and goes through the normal `prepareInit`\n // path.\n private lastHevcInitBytes_: Uint8Array | null = null;\n private cachedH264Init_: Uint8Array | null = null;\n\n constructor(mimeType: string, config: HevcTransmuxerConfig = {}) {\n this.originalMimeType_ = mimeType;\n this.transcoderConfig_ = config;\n }\n\n destroy(): void {\n this.transcoder_?.destroy();\n this.transcoder_ = null;\n this.initPromise_ = null;\n this.pendingHevcInit_ = null;\n this.h264InitEmitted_ = false;\n this.lastHevcInitBytes_ = null;\n this.cachedH264Init_ = null;\n }\n\n isSupported(mimeType: string, _contentType?: string): boolean {\n // Muxed A/V HEVC (e.g. HLS fMP4 with codecs=\"hvc1...,mp4a...\"): the core\n // supports these via the MSE intercept, but the Shaka transmuxer path\n // isn't wired for two-track output yet. Report unsupported so Shaka\n // surfaces a clear error rather than dropping the audio track.\n if (isMuxedHevcMime(mimeType)) return false;\n return HEVC_MIME_PATTERN.test(mimeType);\n }\n\n /**\n * Output mime advertised to Shaka before any frame has been encoded.\n * Best-effort mapping based on the HEVC level declared in the input\n * (see `@hevcjs/core/codec-mapping`). The actual encoded stream may\n * use a slightly different profile/level if `H264Encoder` decides\n * differently from the encoded resolution.\n */\n convertCodecs(_contentType: string, mimeType: string): string {\n if (!HEVC_MIME_PATTERN.test(mimeType)) return mimeType;\n return `video/mp4; codecs=\"${hevcMimeToH264Codec(mimeType)}\"`;\n }\n\n getOriginalMimeType(): string {\n return this.originalMimeType_;\n }\n\n /**\n * Convert one HEVC fMP4 segment into an MSE-ready H.264 fMP4 segment.\n *\n * Shaka calls this once per segment with `reference === null` for the\n * init segment and a non-null `reference` for media segments.\n *\n * - Init segment: warm up the H.264 encoder eagerly (encodes a single\n * black frame to obtain a valid avcC) and return a complete H.264\n * init segment that MSE can immediately ingest.\n * - Media segment: decode HEVC, re-encode to H.264, mux fMP4, return.\n *\n * Returns a raw `Uint8Array` rather than `{data, init}` so the same\n * code path works on Shaka 4.x (which expects a `Uint8Array` directly)\n * and on Shaka 5+ (which accepts either via an `ArrayBuffer.isView`\n * check). Init/media segmentation is implicit in the call sequence.\n */\n async transmux(\n data: BufferSource,\n _stream: ShakaStream,\n reference: ShakaSegmentReference,\n _duration: number,\n _contentType: string,\n ): Promise<TransmuxOutput> {\n const bytes = toUint8(data);\n const isInit = reference == null || isInitSegment(bytes);\n\n if (!this.transcoder_) {\n const workerUrl = this.transcoderConfig_.workerUrl;\n if (workerUrl) {\n const worker = new TranscodeWorkerClient({\n ...this.transcoderConfig_,\n workerUrl,\n });\n this.transcoder_ = worker;\n this.initPromise_ = worker.waitReady();\n console.log(\n `[hevc.js/shaka] HEVC transcoding routed through Worker at ${workerUrl}`,\n );\n } else {\n const local = new SegmentTranscoder(this.transcoderConfig_);\n this.transcoder_ = local;\n this.initPromise_ = local.init();\n console.log(\n \"[hevc.js/shaka] HEVC transcoding runs on main thread (no workerUrl provided)\",\n );\n }\n }\n await this.initPromise_;\n\n if (isInit) {\n // Short-circuit when Shaka resends the exact same init bytes (variant\n // probe, transmuxer re-check). Going through prepareInit again would\n // close the live H.264 encoder and the next media segment would stall\n // while a new one warms up — the visible \"stutter every segment\"\n // symptom that motivated this cache. Real ABR switches arrive with\n // different bytes and fall through to the full prepareInit path.\n if (this.cachedH264Init_ && bytesEqual(bytes, this.lastHevcInitBytes_)) {\n const copy = new Uint8Array(this.cachedH264Init_.byteLength);\n copy.set(this.cachedH264Init_);\n return copy;\n }\n\n // Snapshot the input bytes *before* prepareInit. The worker variant\n // transfers `bytes.buffer` to the worker, which detaches it on the\n // main thread — so reading from `bytes` after the await would throw\n // \"TypedArray.set on a detached ArrayBuffer\".\n const initBytesSnapshot = new Uint8Array(bytes.byteLength);\n initBytesSnapshot.set(bytes);\n\n const result = await this.transcoder_!.prepareInit(bytes);\n this.h264InitEmitted_ = true;\n\n // Snapshot the output immediately. Then commit both snapshots to the\n // cache fields atomically.\n const h264InitCopy = new Uint8Array(result.initSegment.byteLength);\n h264InitCopy.set(result.initSegment);\n this.lastHevcInitBytes_ = initBytesSnapshot;\n this.cachedH264Init_ = h264InitCopy;\n\n // Defensive copy for the return value — never hand MSE a view into\n // our cache.\n const copy = new Uint8Array(h264InitCopy.byteLength);\n copy.set(h264InitCopy);\n return copy;\n }\n\n const h264Media = await this.transcoder_!.processMediaSegment(bytes);\n if (!h264Media) {\n // No frames produced (e.g. drop frames in adaptive switching). Emit\n // a spec-valid `free` box of 8 bytes — empty buffers crash Chrome's\n // appendBuffer with \"Overload resolution failed\".\n return FREE_BOX_8B;\n }\n return h264Media;\n }\n}\n\nfunction bytesEqual(a: Uint8Array, b: Uint8Array | null): boolean {\n if (!b || a.byteLength !== b.byteLength) return false;\n for (let i = 0; i < a.byteLength; i++) {\n if (a[i] !== b[i]) return false;\n }\n return true;\n}\n\nfunction toUint8(data: BufferSource): Uint8Array {\n if (data instanceof Uint8Array) return data;\n if (data instanceof ArrayBuffer) return new Uint8Array(data);\n return new Uint8Array(\n (data as ArrayBufferView).buffer,\n (data as ArrayBufferView).byteOffset,\n (data as ArrayBufferView).byteLength,\n );\n}\n","/**\n * Compute-aware ABR adapter for Shaka Player.\n *\n * Subscribes to the per-segment perf bus published by `@hevcjs/core` and,\n * when transcode throughput drifts away from a healthy `speedX`, narrows\n * the variants Shaka's own ABR controller is allowed to choose from. The\n * host ABR algorithm is never replaced — we just move the upper bound via\n * the public `player.configure({ abr: { restrictions } })` API.\n *\n * Why this exists: a variant that's reachable from a network-bandwidth\n * standpoint can still saturate the device's WASM-decode + WebCodecs-encode\n * budget, draining the buffer without Shaka's ABR ever noticing. Mainstream\n * ABR algorithms only look at network because fetch+parse+MSE-append is\n * essentially free in their world. With our transcode pipeline, it isn't.\n */\nimport {\n ComputeAwareDecider,\n subscribeSegmentStat,\n} from \"@hevcjs/core\";\nimport type {\n ComputeAwareConfig,\n SegmentPerfStat,\n} from \"@hevcjs/core\";\n\n// Loose typing while we don't pull `shaka.extern.*` into the build.\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype ShakaPlayer = any;\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype ShakaVariantTrack = any;\n\n/**\n * One entry of the ladder we feed to the decider. Heights are optional —\n * some manifests only differ in bandwidth (audio-only ladders excluded).\n */\ninterface LadderRank {\n height?: number;\n /** Per-variant video bandwidth in bits/sec (preferred), else total bandwidth. */\n bandwidth: number;\n}\n\nexport interface ShakaComputeAwareOptions extends ComputeAwareConfig {\n /**\n * Optional sink for telemetry — called on every observation, not just\n * cap changes. Useful for plotting speedX over time in a demo.\n *\n * `reason` is the decider's verdict on this observation:\n * `init` (window not full yet) | `hold` (no change) |\n * `lower` (cap stepped down) | `raise` (cap stepped up).\n */\n onObservation?: (\n stat: SegmentPerfStat,\n avgSpeedX: number,\n capIndex: number | null,\n reason: \"init\" | \"hold\" | \"lower\" | \"raise\",\n ) => void;\n}\n\n/**\n * Attach the compute-aware ABR feedback loop to a Shaka player.\n *\n * Must be called after `new shaka.Player()`. Safe to call before\n * `player.load()`: variants are looked up lazily as segments arrive.\n *\n * @returns cleanup function — unsubscribes the perf-bus listener.\n * Does NOT clear any restriction already applied to the player. If you\n * want to restore an unbounded ABR, call\n * `player.configure({ abr: { restrictions: { maxHeight: Infinity, maxBandwidth: Infinity }}})`\n * after detaching.\n */\nexport function attachShakaComputeAware(\n player: ShakaPlayer,\n options: ShakaComputeAwareOptions = {},\n): () => void {\n const { onObservation, ...deciderConfig } = options;\n const decider = new ComputeAwareDecider(deciderConfig);\n\n const unsubscribe = subscribeSegmentStat((stat: SegmentPerfStat) => {\n const ladder = readLadder(player);\n if (ladder.length === 0) return; // manifest not loaded yet, or audio-only\n\n decider.setLadderSize(ladder.length);\n const currentIdx = findCurrentIndex(player, ladder);\n const decision = decider.observe(stat.speedX, currentIdx);\n\n if (onObservation) {\n try {\n onObservation(stat, decision.avgSpeedX, decision.capIndex, decision.reason);\n } catch {\n // a buggy telemetry sink must not break ABR\n }\n }\n\n if (decision.reason === \"lower\" || decision.reason === \"raise\") {\n try {\n applyCap(player, ladder, decision.capIndex!);\n } catch (err) {\n // Player may be in a destroyed/invalid state. Rolling back the\n // decider keeps it in sync with what the player actually has\n // configured — otherwise the next decision thinks the cap is\n // already lower (or higher) than it really is.\n decider.revertLastDecision();\n // eslint-disable-next-line no-console\n console.warn(\"[hevc.js/shaka] applyCap failed, reverted decider:\", err);\n }\n }\n });\n\n return unsubscribe;\n}\n\n/**\n * Build a deduplicated, ascending ladder from `player.getVariantTracks()`.\n * Dedup key prefers `height` (the natural quality axis), falls back to\n * `videoBandwidth` for height-less manifests.\n */\nfunction readLadder(player: ShakaPlayer): LadderRank[] {\n const variants: ShakaVariantTrack[] = player.getVariantTracks?.() ?? [];\n const seen = new Map<string, LadderRank>();\n\n for (const v of variants) {\n // Skip audio-only / text variants. Anything with a height or a video\n // bandwidth/codec qualifies as a video variant.\n const hasVideo =\n v.height != null ||\n v.videoBandwidth != null ||\n (v.videoCodec != null && v.videoCodec !== \"\");\n if (!hasVideo) continue;\n\n const bw = (v.videoBandwidth ?? v.bandwidth ?? 0) as number;\n const key = v.height != null ? `h:${v.height}` : `b:${bw}`;\n if (seen.has(key)) continue;\n seen.set(key, {\n height: v.height ?? undefined,\n bandwidth: bw,\n });\n }\n\n const ladder = Array.from(seen.values());\n ladder.sort((a, b) => {\n if (a.height != null && b.height != null) return a.height - b.height;\n return a.bandwidth - b.bandwidth;\n });\n return ladder;\n}\n\nfunction findCurrentIndex(player: ShakaPlayer, ladder: LadderRank[]): number {\n const variants: ShakaVariantTrack[] = player.getVariantTracks?.() ?? [];\n const active = variants.find((t) => t.active);\n if (!active) return ladder.length - 1;\n\n if (active.height != null) {\n const idx = ladder.findIndex((v) => v.height === active.height);\n if (idx >= 0) return idx;\n }\n const bw = (active.videoBandwidth ?? active.bandwidth ?? 0) as number;\n const idx = ladder.findIndex((v) => v.bandwidth === bw);\n return idx >= 0 ? idx : ladder.length - 1;\n}\n\nfunction applyCap(player: ShakaPlayer, ladder: LadderRank[], capIndex: number): void {\n const cap = ladder[capIndex];\n if (!cap || typeof player.configure !== \"function\") return;\n\n // Always cap by bandwidth (universal). Add maxHeight when the manifest\n // exposes heights — gives Shaka a more direct signal than bytes/sec.\n const restrictions: Record<string, number> = {\n maxBandwidth: cap.bandwidth,\n };\n if (cap.height != null) restrictions.maxHeight = cap.height;\n\n player.configure({ abr: { restrictions } });\n}\n","/**\n * Shaka Player HEVC Plugin — public entry point.\n *\n * Usage (main thread, no Worker):\n * ```ts\n * import shaka from 'shaka-player';\n * import { registerHevcTransmuxer } from '@hevcjs/shaka-plugin';\n *\n * registerHevcTransmuxer(shaka, { wasmUrl: '/hevc-decode.js' });\n * const player = new shaka.Player();\n * await player.attach(videoElement);\n * await player.load(manifestUrl);\n * ```\n *\n * Usage (off-main-thread via Web Worker — recommended for 4K / smoothness):\n * ```ts\n * registerHevcTransmuxer(shaka, {\n * wasmUrl: '/hevc-decode.js',\n * workerUrl: '/transcode-worker.js',\n * });\n * ```\n *\n * Compute-aware ABR is ON by default — Shaka's bandwidth-based ABR keeps\n * choosing freely while we narrow the ceiling when the device can't keep\n * up. The player is supplied later via `attachComputeAware`:\n * ```ts\n * const handle = registerHevcTransmuxer(shaka, {\n * wasmUrl: '/hevc-decode.js',\n * workerUrl: '/transcode-worker.js',\n * // adaptiveCompute is ON by default.\n * // To opt out: adaptiveCompute: false\n * // To tune: adaptiveCompute: { targetSpeedX: 1.5, lowerAfter: 1 }\n * });\n * const player = new shaka.Player();\n * handle.attachComputeAware(player); // wire the feedback loop\n * await player.load(manifestUrl);\n * // ...\n * handle(); // unregister + detach (callable)\n * ```\n *\n * To force the transmuxer even on browsers with native HEVC support\n * (Safari, recent Chrome on macOS), use Shaka's built-in config rather\n * than patching MSE yourself:\n *\n * ```ts\n * player.configure({ mediaSource: { forceTransmux: true } });\n * ```\n */\n\nimport { HevcTransmuxer } from \"./transmuxer.js\";\nimport type { HevcTransmuxerConfig } from \"./transmuxer.js\";\nimport { attachShakaComputeAware } from \"./compute-aware.js\";\nimport type { ShakaComputeAwareOptions } from \"./compute-aware.js\";\n\nexport { HevcTransmuxer } from \"./transmuxer.js\";\nexport type { TransmuxOutput, HevcTransmuxerConfig } from \"./transmuxer.js\";\nexport { attachShakaComputeAware } from \"./compute-aware.js\";\nexport type { ShakaComputeAwareOptions } from \"./compute-aware.js\";\n// Re-export the perf-bus surface so consumers can subscribe to per-segment\n// transcode stats (speedX, frames, resolution) without depending on\n// @hevcjs/core directly.\nexport { subscribeSegmentStat } from \"@hevcjs/core\";\nexport type { SegmentPerfStat } from \"@hevcjs/core\";\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype ShakaNamespace = any;\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype ShakaPlayer = any;\n\n/**\n * Plugin configuration. Forwarded as-is to `HevcTransmuxer`. Supports the\n * `SegmentTranscoderConfig` fields (`wasmUrl`, `wasmBinaryUrl`, `fps`,\n * `bitrate`) plus an optional `workerUrl` that, when set, routes the\n * HEVC decode + H.264 encode pipeline through a Web Worker, plus an\n * optional `adaptiveCompute` flag/config to enable the compute-aware\n * ABR feedback loop.\n */\nexport interface HevcShakaPluginConfig extends HevcTransmuxerConfig {\n /**\n * Compute-aware ABR feedback. The returned handle exposes\n * `attachComputeAware(player)` that wires the host Shaka player to the\n * transcode perf bus and caps variants when the device can't keep up.\n *\n * - **On by default** (undefined or `true`) — sensible defaults.\n * - Pass an object to tune the decider knobs (`targetSpeedX`, etc.).\n * - Pass `false` to opt out: `attachComputeAware` becomes a silent no-op.\n *\n * `attachComputeAware(player)` must still be called explicitly because\n * the player instance isn't available at register time.\n */\n adaptiveCompute?: boolean | ShakaComputeAwareOptions;\n}\n\n/**\n * Return shape of `registerHevcTransmuxer`. Callable for backwards compat\n * (`handle()` unregisters the transmuxer, same as before). Methods are\n * attached as properties when `adaptiveCompute` is enabled so the existing\n * `const cleanup = registerHevcTransmuxer(...)` pattern still works.\n */\nexport interface HevcShakaPluginHandle {\n (): void;\n /** Explicit alias for the callable form. */\n unregister(): void;\n /**\n * Attach the compute-aware feedback loop to a Shaka player.\n * Active by default; becomes a silent no-op only when the registration\n * config explicitly passed `adaptiveCompute: false`.\n *\n * Options passed here are merged on top of any options passed at\n * register time, which is convenient when the telemetry sink\n * (`onObservation`) is only available once the UI exists.\n *\n * @returns cleanup function — detaches the perf-bus listener.\n */\n attachComputeAware(player: ShakaPlayer, options?: ShakaComputeAwareOptions): () => void;\n}\n\nconst HEVC_MIME_TYPES = [\n 'video/mp4; codecs=\"hev1\"',\n 'video/mp4; codecs=\"hvc1\"',\n];\n\n/**\n * Register the HEVC transmuxer with Shaka's TransmuxerEngine.\n *\n * Must be called before `player.load()`. Registers a factory for both\n * `hev1` and `hvc1` MIME types at APPLICATION priority so Shaka picks\n * our transmuxer over any default fallback.\n *\n * @param shaka the global `shaka` namespace (import or window.shaka)\n * @param config forwarded to `HevcTransmuxer` (wasmUrl, wasmBinaryUrl, fps, bitrate, workerUrl, adaptiveCompute)\n * @returns A handle that is both callable (unregisters) and exposes\n * `attachComputeAware(player)` when `adaptiveCompute` is enabled.\n */\nexport function registerHevcTransmuxer(\n shaka: ShakaNamespace,\n config: HevcShakaPluginConfig = {},\n): HevcShakaPluginHandle {\n const engine = shaka?.transmuxer?.TransmuxerEngine;\n if (!engine || typeof engine.registerTransmuxer !== \"function\") {\n console.warn(\n \"[hevc.js/shaka] shaka.transmuxer.TransmuxerEngine.registerTransmuxer not found. \" +\n \"Make sure shaka-player >= 4.0 is loaded before calling registerHevcTransmuxer().\",\n );\n return makeHandle(() => {}, undefined);\n }\n\n // External (application-supplied) plugins should register at the\n // APPLICATION priority so they override any built-in fallback. Values in\n // shaka.transmuxer.TransmuxerEngine.PluginPriority: FALLBACK=1,\n // PREFERRED_SECONDARY=2, PREFERRED=3, APPLICATION=4.\n const priority =\n engine.PluginPriority?.APPLICATION ??\n engine.PluginPriority?.PREFERRED ??\n 4;\n\n const { adaptiveCompute, ...transmuxerConfig } = config;\n\n for (const mimeType of HEVC_MIME_TYPES) {\n engine.registerTransmuxer(\n mimeType,\n () => new HevcTransmuxer(mimeType, transmuxerConfig),\n priority,\n );\n }\n\n const unregister = () => {\n if (typeof engine.unregisterTransmuxer === \"function\") {\n for (const mimeType of HEVC_MIME_TYPES) {\n // unregisterTransmuxer keys on `${mime}-${priority}` so the\n // priority used at register time must be passed back here.\n engine.unregisterTransmuxer(mimeType, priority);\n }\n }\n };\n\n return makeHandle(unregister, adaptiveCompute);\n}\n\n/**\n * Build the callable+methods handle. Keeping the callable form preserves\n * the pre-existing `const cleanup = registerHevcTransmuxer(...); cleanup();`\n * pattern; the `attachComputeAware` property is added only when the feature\n * is enabled, but a no-op is always present so consumers can call it\n * unconditionally without a type guard.\n *\n * Unified cleanup: invoking the handle (or `unregister()`) tears down both\n * the transmuxer registration AND any active compute-aware listener, so the\n * caller doesn't have to remember a separate detach. Matches the dash.js\n * plugin's `attachHevcSupport` cleanup behaviour.\n */\nfunction makeHandle(\n unregister: () => void,\n adaptive: boolean | ShakaComputeAwareOptions | undefined,\n): HevcShakaPluginHandle {\n let activeDetach: (() => void) | null = null;\n\n const tearDown = () => {\n activeDetach?.();\n activeDetach = null;\n unregister();\n };\n\n const fn = (() => tearDown()) as HevcShakaPluginHandle;\n fn.unregister = tearDown;\n fn.attachComputeAware = (\n player: ShakaPlayer,\n runtimeOpts?: ShakaComputeAwareOptions,\n ): () => void => {\n // Explicit opt-out is the only path that disables the feature.\n // `undefined` (no flag) → on; `true` → on; object → on with options.\n if (adaptive === false) return () => {};\n // Re-attaching replaces any previous listener — keep the handle's\n // tearDown able to free the *current* one.\n activeDetach?.();\n // Merge register-time options with attach-time options; attach-time\n // wins on conflicts so the caller can override defaults set early\n // (typical case: onObservation is only known once the UI exists).\n const registerOpts = typeof adaptive === \"object\" ? adaptive : {};\n const opts = { ...registerOpts, ...(runtimeOpts ?? {}) };\n const detach = attachShakaComputeAware(player, opts);\n activeDetach = detach;\n return () => {\n detach();\n if (activeDetach === detach) activeDetach = null;\n };\n };\n return fn;\n}\n"],"mappings":"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","names":["copy","idx","subscribeSegmentStat"]}
1
+ {"version":3,"sources":["../src/transmuxer.ts","../src/compute-aware.ts","../src/buffer-config.ts","../src/index.ts"],"sourcesContent":["/**\n * HEVC Transmuxer for Shaka Player.\n *\n * Implements the `shaka.extern.Transmuxer` interface so Shaka can ingest\n * HEVC/H.265 fMP4 segments on browsers that lack native HEVC support.\n * Uses `@hevcjs/core` SegmentTranscoder to decode HEVC and re-encode to\n * H.264 fMP4 that the browser's MSE can play.\n *\n * Modeled after `lib/transmuxer/aac_transmuxer.js` in shaka-player.\n */\n\nimport {\n SegmentTranscoder,\n TranscodeWorkerClient,\n hevcMimeToH264Codec,\n isMuxedHevcMime,\n} from \"@hevcjs/core\";\nimport type { SegmentTranscoderConfig } from \"@hevcjs/core\";\n\n/**\n * Config accepted by `HevcTransmuxer` (and forwarded by `registerHevcTransmuxer`).\n * When `workerUrl` is set, transcoding runs inside a Web Worker; otherwise\n * the HEVC decode + H.264 encode pipeline runs on the main thread.\n */\nexport interface HevcTransmuxerConfig extends SegmentTranscoderConfig {\n /** URL to the transcode worker script. When set, transcoding runs off main thread. */\n workerUrl?: string;\n}\n\n// Loose typing while we don't pull `shaka.extern.*` into the build.\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype ShakaStream = any;\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype ShakaSegmentReference = any;\n\n/**\n * Return type of `HevcTransmuxer.transmux`. Compatible with both Shaka 4.x\n * (which expects a raw `Uint8Array` and passes it straight to MSE) and 5+\n * (which checks `ArrayBuffer.isView` and falls back to `{data, init}`\n * when the value is a plain object). Returning a `Uint8Array` is the\n * common subset that works on every supported Shaka version.\n */\nexport type TransmuxOutput = Uint8Array;\n\nconst HEVC_MIME_PATTERN = /^video\\/mp4\\s*;.*codecs=\"?(hev1|hvc1)/i;\n\n/**\n * 8-byte ISO BMFF `free` box (size + type, no payload). Spec-compliant\n * padding that any MP4 parser ignores. Used as a stand-in when we need\n * to return *something* to Shaka but have nothing real to emit yet —\n * `appendBuffer(emptyUint8Array)` throws \"Overload resolution failed\"\n * on Chrome, so we can't return zero-length buffers.\n */\nconst FREE_BOX_8B = new Uint8Array([\n 0, 0, 0, 8, // size = 8\n 0x66, 0x72, 0x65, 0x65, // 'free'\n]);\n\n/**\n * Sniff whether a buffer starts with an ISO BMFF init segment.\n * Init segments begin with the `ftyp` box; media segments begin with\n * `moof` (or `styp` followed by `moof`).\n *\n * Box header layout: 4 bytes big-endian size, 4 bytes ASCII type.\n */\nexport function isInitSegment(bytes: Uint8Array): boolean {\n if (bytes.length < 8) return false;\n const boxType = String.fromCharCode(\n bytes[4]!,\n bytes[5]!,\n bytes[6]!,\n bytes[7]!,\n );\n return boxType === \"ftyp\";\n}\n\nexport class HevcTransmuxer {\n private readonly originalMimeType_: string;\n private readonly transcoderConfig_: HevcTransmuxerConfig;\n private transcoder_: SegmentTranscoder | TranscodeWorkerClient | null = null;\n private initPromise_: Promise<void> | null = null;\n private pendingHevcInit_: Uint8Array | null = null;\n private h264InitEmitted_ = false;\n // Cache for the last HEVC init segment we processed and the H.264 init we\n // produced for it. Shaka can call transmux() with the same init bytes\n // multiple times during a session (variant probing, transmuxer re-checks);\n // we must not tear down the live encoder on those redundant calls or\n // playback stalls while the encoder rebuilds. A real representation change\n // arrives with different bytes and goes through the normal `prepareInit`\n // path.\n private lastHevcInitBytes_: Uint8Array | null = null;\n private cachedH264Init_: Uint8Array | null = null;\n\n constructor(mimeType: string, config: HevcTransmuxerConfig = {}) {\n this.originalMimeType_ = mimeType;\n this.transcoderConfig_ = config;\n }\n\n destroy(): void {\n this.transcoder_?.destroy();\n this.transcoder_ = null;\n this.initPromise_ = null;\n this.pendingHevcInit_ = null;\n this.h264InitEmitted_ = false;\n this.lastHevcInitBytes_ = null;\n this.cachedH264Init_ = null;\n }\n\n isSupported(mimeType: string, _contentType?: string): boolean {\n // Muxed A/V HEVC (e.g. HLS fMP4 with codecs=\"hvc1...,mp4a...\"): the core\n // supports these via the MSE intercept, but the Shaka transmuxer path\n // isn't wired for two-track output yet. Report unsupported so Shaka\n // surfaces a clear error rather than dropping the audio track.\n if (isMuxedHevcMime(mimeType)) return false;\n return HEVC_MIME_PATTERN.test(mimeType);\n }\n\n /**\n * Output mime advertised to Shaka before any frame has been encoded.\n * Best-effort mapping based on the HEVC level declared in the input\n * (see `@hevcjs/core/codec-mapping`). The actual encoded stream may\n * use a slightly different profile/level if `H264Encoder` decides\n * differently from the encoded resolution.\n */\n convertCodecs(_contentType: string, mimeType: string): string {\n if (!HEVC_MIME_PATTERN.test(mimeType)) return mimeType;\n return `video/mp4; codecs=\"${hevcMimeToH264Codec(mimeType)}\"`;\n }\n\n getOriginalMimeType(): string {\n return this.originalMimeType_;\n }\n\n /**\n * Convert one HEVC fMP4 segment into an MSE-ready H.264 fMP4 segment.\n *\n * Shaka calls this once per segment with `reference === null` for the\n * init segment and a non-null `reference` for media segments.\n *\n * - Init segment: warm up the H.264 encoder eagerly (encodes a single\n * black frame to obtain a valid avcC) and return a complete H.264\n * init segment that MSE can immediately ingest.\n * - Media segment: decode HEVC, re-encode to H.264, mux fMP4, return.\n *\n * Returns a raw `Uint8Array` rather than `{data, init}` so the same\n * code path works on Shaka 4.x (which expects a `Uint8Array` directly)\n * and on Shaka 5+ (which accepts either via an `ArrayBuffer.isView`\n * check). Init/media segmentation is implicit in the call sequence.\n */\n async transmux(\n data: BufferSource,\n _stream: ShakaStream,\n reference: ShakaSegmentReference,\n _duration: number,\n _contentType: string,\n ): Promise<TransmuxOutput> {\n const bytes = toUint8(data);\n const isInit = reference == null || isInitSegment(bytes);\n\n if (!this.transcoder_) {\n const workerUrl = this.transcoderConfig_.workerUrl;\n if (workerUrl) {\n const worker = new TranscodeWorkerClient({\n ...this.transcoderConfig_,\n workerUrl,\n });\n this.transcoder_ = worker;\n this.initPromise_ = worker.waitReady();\n console.log(\n `[hevc.js/shaka] HEVC transcoding routed through Worker at ${workerUrl}`,\n );\n } else {\n const local = new SegmentTranscoder(this.transcoderConfig_);\n this.transcoder_ = local;\n this.initPromise_ = local.init();\n console.log(\n \"[hevc.js/shaka] HEVC transcoding runs on main thread (no workerUrl provided)\",\n );\n }\n }\n await this.initPromise_;\n\n if (isInit) {\n // Short-circuit when Shaka resends the exact same init bytes (variant\n // probe, transmuxer re-check). Going through prepareInit again would\n // close the live H.264 encoder and the next media segment would stall\n // while a new one warms up — the visible \"stutter every segment\"\n // symptom that motivated this cache. Real ABR switches arrive with\n // different bytes and fall through to the full prepareInit path.\n if (this.cachedH264Init_ && bytesEqual(bytes, this.lastHevcInitBytes_)) {\n const copy = new Uint8Array(this.cachedH264Init_.byteLength);\n copy.set(this.cachedH264Init_);\n return copy;\n }\n\n // Snapshot the input bytes *before* prepareInit. The worker variant\n // transfers `bytes.buffer` to the worker, which detaches it on the\n // main thread — so reading from `bytes` after the await would throw\n // \"TypedArray.set on a detached ArrayBuffer\".\n const initBytesSnapshot = new Uint8Array(bytes.byteLength);\n initBytesSnapshot.set(bytes);\n\n const result = await this.transcoder_!.prepareInit(bytes);\n this.h264InitEmitted_ = true;\n\n // Snapshot the output immediately. Then commit both snapshots to the\n // cache fields atomically.\n const h264InitCopy = new Uint8Array(result.initSegment.byteLength);\n h264InitCopy.set(result.initSegment);\n this.lastHevcInitBytes_ = initBytesSnapshot;\n this.cachedH264Init_ = h264InitCopy;\n\n // Defensive copy for the return value — never hand MSE a view into\n // our cache.\n const copy = new Uint8Array(h264InitCopy.byteLength);\n copy.set(h264InitCopy);\n return copy;\n }\n\n const h264Media = await this.transcoder_!.processMediaSegment(bytes);\n if (!h264Media) {\n // No frames produced (e.g. drop frames in adaptive switching). Emit\n // a spec-valid `free` box of 8 bytes — empty buffers crash Chrome's\n // appendBuffer with \"Overload resolution failed\".\n return FREE_BOX_8B;\n }\n return h264Media;\n }\n}\n\nfunction bytesEqual(a: Uint8Array, b: Uint8Array | null): boolean {\n if (!b || a.byteLength !== b.byteLength) return false;\n for (let i = 0; i < a.byteLength; i++) {\n if (a[i] !== b[i]) return false;\n }\n return true;\n}\n\nfunction toUint8(data: BufferSource): Uint8Array {\n if (data instanceof Uint8Array) return data;\n if (data instanceof ArrayBuffer) return new Uint8Array(data);\n return new Uint8Array(\n (data as ArrayBufferView).buffer,\n (data as ArrayBufferView).byteOffset,\n (data as ArrayBufferView).byteLength,\n );\n}\n","/**\n * Compute-aware ABR adapter for Shaka Player.\n *\n * Subscribes to the per-segment perf bus published by `@hevcjs/core` and,\n * when transcode throughput drifts away from a healthy `speedX`, narrows\n * the variants Shaka's own ABR controller is allowed to choose from. The\n * host ABR algorithm is never replaced — we just move the upper bound via\n * the public `player.configure({ abr: { restrictions } })` API.\n *\n * Why this exists: a variant that's reachable from a network-bandwidth\n * standpoint can still saturate the device's WASM-decode + WebCodecs-encode\n * budget, draining the buffer without Shaka's ABR ever noticing. Mainstream\n * ABR algorithms only look at network because fetch+parse+MSE-append is\n * essentially free in their world. With our transcode pipeline, it isn't.\n */\nimport {\n ComputeAwareDecider,\n subscribeSegmentStat,\n} from \"@hevcjs/core\";\nimport type {\n ComputeAwareConfig,\n SegmentPerfStat,\n} from \"@hevcjs/core\";\n\n// Loose typing while we don't pull `shaka.extern.*` into the build.\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype ShakaPlayer = any;\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype ShakaVariantTrack = any;\n\n/**\n * One entry of the ladder we feed to the decider. Heights are optional —\n * some manifests only differ in bandwidth (audio-only ladders excluded).\n */\ninterface LadderRank {\n height?: number;\n /** Per-variant video bandwidth in bits/sec (preferred), else total bandwidth. */\n bandwidth: number;\n}\n\nexport interface ShakaComputeAwareOptions extends ComputeAwareConfig {\n /**\n * Optional sink for telemetry — called on every observation, not just\n * cap changes. Useful for plotting speedX over time in a demo.\n *\n * `reason` is the decider's verdict on this observation:\n * `init` (window not full yet) | `hold` (no change) |\n * `lower` (cap stepped down) | `raise` (cap stepped up).\n */\n onObservation?: (\n stat: SegmentPerfStat,\n avgSpeedX: number,\n capIndex: number | null,\n reason: \"init\" | \"hold\" | \"lower\" | \"raise\",\n ) => void;\n}\n\n/**\n * Attach the compute-aware ABR feedback loop to a Shaka player.\n *\n * Must be called after `new shaka.Player()`. Safe to call before\n * `player.load()`: variants are looked up lazily as segments arrive.\n *\n * @returns cleanup function — unsubscribes the perf-bus listener.\n * Does NOT clear any restriction already applied to the player. If you\n * want to restore an unbounded ABR, call\n * `player.configure({ abr: { restrictions: { maxHeight: Infinity, maxBandwidth: Infinity }}})`\n * after detaching.\n */\nexport function attachShakaComputeAware(\n player: ShakaPlayer,\n options: ShakaComputeAwareOptions = {},\n): () => void {\n const { onObservation, ...deciderConfig } = options;\n const decider = new ComputeAwareDecider(deciderConfig);\n\n const unsubscribe = subscribeSegmentStat((stat: SegmentPerfStat) => {\n const ladder = readLadder(player);\n if (ladder.length === 0) return; // manifest not loaded yet, or audio-only\n\n decider.setLadderSize(ladder.length);\n const currentIdx = findCurrentIndex(player, ladder);\n const decision = decider.observe(stat.speedX, currentIdx);\n\n if (onObservation) {\n try {\n onObservation(stat, decision.avgSpeedX, decision.capIndex, decision.reason);\n } catch {\n // a buggy telemetry sink must not break ABR\n }\n }\n\n if (decision.reason === \"lower\" || decision.reason === \"raise\") {\n try {\n applyCap(player, ladder, decision.capIndex!);\n } catch (err) {\n // Player may be in a destroyed/invalid state. Rolling back the\n // decider keeps it in sync with what the player actually has\n // configured — otherwise the next decision thinks the cap is\n // already lower (or higher) than it really is.\n decider.revertLastDecision();\n // eslint-disable-next-line no-console\n console.warn(\"[hevc.js/shaka] applyCap failed, reverted decider:\", err);\n }\n }\n });\n\n return unsubscribe;\n}\n\n/**\n * Build a deduplicated, ascending ladder from `player.getVariantTracks()`.\n * Dedup key prefers `height` (the natural quality axis), falls back to\n * `videoBandwidth` for height-less manifests.\n */\nfunction readLadder(player: ShakaPlayer): LadderRank[] {\n const variants: ShakaVariantTrack[] = player.getVariantTracks?.() ?? [];\n const seen = new Map<string, LadderRank>();\n\n for (const v of variants) {\n // Skip audio-only / text variants. Anything with a height or a video\n // bandwidth/codec qualifies as a video variant.\n const hasVideo =\n v.height != null ||\n v.videoBandwidth != null ||\n (v.videoCodec != null && v.videoCodec !== \"\");\n if (!hasVideo) continue;\n\n const bw = (v.videoBandwidth ?? v.bandwidth ?? 0) as number;\n const key = v.height != null ? `h:${v.height}` : `b:${bw}`;\n if (seen.has(key)) continue;\n seen.set(key, {\n height: v.height ?? undefined,\n bandwidth: bw,\n });\n }\n\n const ladder = Array.from(seen.values());\n ladder.sort((a, b) => {\n if (a.height != null && b.height != null) return a.height - b.height;\n return a.bandwidth - b.bandwidth;\n });\n return ladder;\n}\n\nfunction findCurrentIndex(player: ShakaPlayer, ladder: LadderRank[]): number {\n const variants: ShakaVariantTrack[] = player.getVariantTracks?.() ?? [];\n const active = variants.find((t) => t.active);\n if (!active) return ladder.length - 1;\n\n if (active.height != null) {\n const idx = ladder.findIndex((v) => v.height === active.height);\n if (idx >= 0) return idx;\n }\n const bw = (active.videoBandwidth ?? active.bandwidth ?? 0) as number;\n const idx = ladder.findIndex((v) => v.bandwidth === bw);\n return idx >= 0 ? idx : ladder.length - 1;\n}\n\nfunction applyCap(player: ShakaPlayer, ladder: LadderRank[], capIndex: number): void {\n const cap = ladder[capIndex];\n if (!cap || typeof player.configure !== \"function\") return;\n\n // Always cap by bandwidth (universal). Add maxHeight when the manifest\n // exposes heights — gives Shaka a more direct signal than bytes/sec.\n const restrictions: Record<string, number> = {\n maxBandwidth: cap.bandwidth,\n };\n if (cap.height != null) restrictions.maxHeight = cap.height;\n\n player.configure({ abr: { restrictions } });\n}\n","/**\n * Buffer tuning for HEVC playback through the transmuxer.\n *\n * Shaka 4.x's `Transmuxer.transmux()` returns one `Uint8Array` per segment,\n * so the buffered range can only grow in whole-segment jumps. When WASM\n * transcoding runs near real time, the playback head skirts the edge of that\n * range and playback stutters even though the buffer is contiguous and no\n * spec is violated. A deeper buffer gives transcoding room to stay ahead.\n *\n * See the \"Performance & tuning\" section of the plugin README.\n */\n\n/** A fragment of Shaka player configuration, for `player.configure()`. */\nexport interface ShakaBufferConfig {\n streaming: {\n /** Seconds of content to buffer ahead. Shaka's default is 10. */\n bufferingGoal: number;\n };\n}\n\n/**\n * Buffer settings recommended when transcoding HEVC through this plugin.\n *\n * Merge into the player configuration before `load()`:\n *\n * ```ts\n * player.configure(recommendedBufferConfig());\n * ```\n *\n * Only `bufferingGoal` is touched, deliberately. `rebufferingGoal` decides\n * whether Shaka gates playback on buffer depth at all: it defaults to 0 on\n * Shaka 5, where 0 means the buffer poller never starts and the playback rate\n * is never held back. Raising it would switch that behaviour on, and on a\n * device transcoding at around real time — the case this config exists for —\n * a brief dip would then freeze playback until several seconds had been\n * re-accumulated. That trades a stutter for a longer hard stall, so leave it\n * at whatever the application has set.\n */\nexport function recommendedBufferConfig(): ShakaBufferConfig {\n return {\n streaming: {\n // 30s covers ~15 two-second segments: enough that a stretch of\n // slower-than-real-time transcoding drains the buffer instead of\n // letting the playback head catch up with it. Shaka's default is 10.\n bufferingGoal: 30,\n },\n };\n}\n","/**\n * Shaka Player HEVC Plugin — public entry point.\n *\n * Usage (main thread, no Worker):\n * ```ts\n * import shaka from 'shaka-player';\n * import { registerHevcTransmuxer } from '@hevcjs/shaka-plugin';\n *\n * registerHevcTransmuxer(shaka, { wasmUrl: '/hevc-decode.js' });\n * const player = new shaka.Player();\n * await player.attach(videoElement);\n * await player.load(manifestUrl);\n * ```\n *\n * Usage (off-main-thread via Web Worker — recommended for 4K / smoothness):\n * ```ts\n * registerHevcTransmuxer(shaka, {\n * wasmUrl: '/hevc-decode.js',\n * workerUrl: '/transcode-worker.js',\n * });\n * ```\n *\n * Compute-aware ABR is ON by default — Shaka's bandwidth-based ABR keeps\n * choosing freely while we narrow the ceiling when the device can't keep\n * up. The player is supplied later via `attachComputeAware`:\n * ```ts\n * const handle = registerHevcTransmuxer(shaka, {\n * wasmUrl: '/hevc-decode.js',\n * workerUrl: '/transcode-worker.js',\n * // adaptiveCompute is ON by default.\n * // To opt out: adaptiveCompute: false\n * // To tune: adaptiveCompute: { targetSpeedX: 1.5, lowerAfter: 1 }\n * });\n * const player = new shaka.Player();\n * handle.attachComputeAware(player); // wire the feedback loop\n * await player.load(manifestUrl);\n * // ...\n * handle(); // unregister + detach (callable)\n * ```\n *\n * To force the transmuxer even on browsers with native HEVC support\n * (Safari, recent Chrome on macOS), use Shaka's built-in config rather\n * than patching MSE yourself:\n *\n * ```ts\n * player.configure({ mediaSource: { forceTransmux: true } });\n * ```\n *\n * On hardware where WASM transcoding runs near real time, a deeper buffer\n * keeps playback from stuttering at the edge of the buffered range:\n *\n * ```ts\n * player.configure(recommendedBufferConfig());\n * ```\n */\n\nimport { HevcTransmuxer } from \"./transmuxer.js\";\nimport type { HevcTransmuxerConfig } from \"./transmuxer.js\";\nimport { attachShakaComputeAware } from \"./compute-aware.js\";\nimport type { ShakaComputeAwareOptions } from \"./compute-aware.js\";\n\nexport { HevcTransmuxer } from \"./transmuxer.js\";\nexport type { TransmuxOutput, HevcTransmuxerConfig } from \"./transmuxer.js\";\nexport { attachShakaComputeAware } from \"./compute-aware.js\";\nexport type { ShakaComputeAwareOptions } from \"./compute-aware.js\";\nexport { recommendedBufferConfig } from \"./buffer-config.js\";\nexport type { ShakaBufferConfig } from \"./buffer-config.js\";\n// Re-export the perf-bus surface so consumers can subscribe to per-segment\n// transcode stats (speedX, frames, resolution) without depending on\n// @hevcjs/core directly.\nexport { subscribeSegmentStat } from \"@hevcjs/core\";\nexport type { SegmentPerfStat } from \"@hevcjs/core\";\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype ShakaNamespace = any;\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype ShakaPlayer = any;\n\n/**\n * Plugin configuration. Forwarded as-is to `HevcTransmuxer`. Supports the\n * `SegmentTranscoderConfig` fields (`wasmUrl`, `wasmBinaryUrl`, `fps`,\n * `bitrate`) plus an optional `workerUrl` that, when set, routes the\n * HEVC decode + H.264 encode pipeline through a Web Worker, plus an\n * optional `adaptiveCompute` flag/config to enable the compute-aware\n * ABR feedback loop.\n */\nexport interface HevcShakaPluginConfig extends HevcTransmuxerConfig {\n /**\n * Compute-aware ABR feedback. The returned handle exposes\n * `attachComputeAware(player)` that wires the host Shaka player to the\n * transcode perf bus and caps variants when the device can't keep up.\n *\n * - **On by default** (undefined or `true`) — sensible defaults.\n * - Pass an object to tune the decider knobs (`targetSpeedX`, etc.).\n * - Pass `false` to opt out: `attachComputeAware` becomes a silent no-op.\n *\n * `attachComputeAware(player)` must still be called explicitly because\n * the player instance isn't available at register time.\n */\n adaptiveCompute?: boolean | ShakaComputeAwareOptions;\n}\n\n/**\n * Return shape of `registerHevcTransmuxer`. Callable for backwards compat\n * (`handle()` unregisters the transmuxer, same as before). Methods are\n * attached as properties when `adaptiveCompute` is enabled so the existing\n * `const cleanup = registerHevcTransmuxer(...)` pattern still works.\n */\nexport interface HevcShakaPluginHandle {\n (): void;\n /** Explicit alias for the callable form. */\n unregister(): void;\n /**\n * Attach the compute-aware feedback loop to a Shaka player.\n * Active by default; becomes a silent no-op only when the registration\n * config explicitly passed `adaptiveCompute: false`.\n *\n * Options passed here are merged on top of any options passed at\n * register time, which is convenient when the telemetry sink\n * (`onObservation`) is only available once the UI exists.\n *\n * @returns cleanup function — detaches the perf-bus listener.\n */\n attachComputeAware(player: ShakaPlayer, options?: ShakaComputeAwareOptions): () => void;\n}\n\nconst HEVC_MIME_TYPES = [\n 'video/mp4; codecs=\"hev1\"',\n 'video/mp4; codecs=\"hvc1\"',\n];\n\n/**\n * Register the HEVC transmuxer with Shaka's TransmuxerEngine.\n *\n * Must be called before `player.load()`. Registers a factory for both\n * `hev1` and `hvc1` MIME types at APPLICATION priority so Shaka picks\n * our transmuxer over any default fallback.\n *\n * @param shaka the global `shaka` namespace (import or window.shaka)\n * @param config forwarded to `HevcTransmuxer` (wasmUrl, wasmBinaryUrl, fps, bitrate, workerUrl, adaptiveCompute)\n * @returns A handle that is both callable (unregisters) and exposes\n * `attachComputeAware(player)` when `adaptiveCompute` is enabled.\n */\nexport function registerHevcTransmuxer(\n shaka: ShakaNamespace,\n config: HevcShakaPluginConfig = {},\n): HevcShakaPluginHandle {\n const engine = shaka?.transmuxer?.TransmuxerEngine;\n if (!engine || typeof engine.registerTransmuxer !== \"function\") {\n console.warn(\n \"[hevc.js/shaka] shaka.transmuxer.TransmuxerEngine.registerTransmuxer not found. \" +\n \"Make sure shaka-player >= 4.0 is loaded before calling registerHevcTransmuxer().\",\n );\n return makeHandle(() => {}, undefined);\n }\n\n // External (application-supplied) plugins should register at the\n // APPLICATION priority so they override any built-in fallback. Values in\n // shaka.transmuxer.TransmuxerEngine.PluginPriority: FALLBACK=1,\n // PREFERRED_SECONDARY=2, PREFERRED=3, APPLICATION=4.\n const priority =\n engine.PluginPriority?.APPLICATION ??\n engine.PluginPriority?.PREFERRED ??\n 4;\n\n const { adaptiveCompute, ...transmuxerConfig } = config;\n\n for (const mimeType of HEVC_MIME_TYPES) {\n engine.registerTransmuxer(\n mimeType,\n () => new HevcTransmuxer(mimeType, transmuxerConfig),\n priority,\n );\n }\n\n const unregister = () => {\n if (typeof engine.unregisterTransmuxer === \"function\") {\n for (const mimeType of HEVC_MIME_TYPES) {\n // unregisterTransmuxer keys on `${mime}-${priority}` so the\n // priority used at register time must be passed back here.\n engine.unregisterTransmuxer(mimeType, priority);\n }\n }\n };\n\n return makeHandle(unregister, adaptiveCompute);\n}\n\n/**\n * Build the callable+methods handle. Keeping the callable form preserves\n * the pre-existing `const cleanup = registerHevcTransmuxer(...); cleanup();`\n * pattern; the `attachComputeAware` property is added only when the feature\n * is enabled, but a no-op is always present so consumers can call it\n * unconditionally without a type guard.\n *\n * Unified cleanup: invoking the handle (or `unregister()`) tears down both\n * the transmuxer registration AND any active compute-aware listener, so the\n * caller doesn't have to remember a separate detach. Matches the dash.js\n * plugin's `attachHevcSupport` cleanup behaviour.\n */\nfunction makeHandle(\n unregister: () => void,\n adaptive: boolean | ShakaComputeAwareOptions | undefined,\n): HevcShakaPluginHandle {\n let activeDetach: (() => void) | null = null;\n\n const tearDown = () => {\n activeDetach?.();\n activeDetach = null;\n unregister();\n };\n\n const fn = (() => tearDown()) as HevcShakaPluginHandle;\n fn.unregister = tearDown;\n fn.attachComputeAware = (\n player: ShakaPlayer,\n runtimeOpts?: ShakaComputeAwareOptions,\n ): () => void => {\n // Explicit opt-out is the only path that disables the feature.\n // `undefined` (no flag) → on; `true` → on; object → on with options.\n if (adaptive === false) return () => {};\n // Re-attaching replaces any previous listener — keep the handle's\n // tearDown able to free the *current* one.\n activeDetach?.();\n // Merge register-time options with attach-time options; attach-time\n // wins on conflicts so the caller can override defaults set early\n // (typical case: onObservation is only known once the UI exists).\n const registerOpts = typeof adaptive === \"object\" ? adaptive : {};\n const opts = { ...registerOpts, ...(runtimeOpts ?? {}) };\n const detach = attachShakaComputeAware(player, opts);\n activeDetach = detach;\n return () => {\n detach();\n if (activeDetach === detach) activeDetach = null;\n };\n };\n return fn;\n}\n"],"mappings":"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","names":["copy","idx","subscribeSegmentStat"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hevcjs/shaka-plugin",
3
- "version": "0.3.5",
3
+ "version": "0.4.0",
4
4
  "description": "Shaka Player plugin for HEVC/H.265 playback — registers a Shaka Transmuxer that decodes HEVC streams via @hevcjs/core",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -49,7 +49,7 @@
49
49
  "url": "https://github.com/lid-labs/hevc.js/issues"
50
50
  },
51
51
  "dependencies": {
52
- "@hevcjs/core": "1.4.2"
52
+ "@hevcjs/core": "1.4.3"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "shaka-player": ">=4.0.0"