@hevcjs/shaka-plugin 0.2.1 → 0.2.2

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/dist/index.js CHANGED
@@ -33,6 +33,15 @@ var HevcTransmuxer = class {
33
33
  this.initPromise_ = null;
34
34
  this.pendingHevcInit_ = null;
35
35
  this.h264InitEmitted_ = false;
36
+ // Cache for the last HEVC init segment we processed and the H.264 init we
37
+ // produced for it. Shaka can call transmux() with the same init bytes
38
+ // multiple times during a session (variant probing, transmuxer re-checks);
39
+ // we must not tear down the live encoder on those redundant calls or
40
+ // playback stalls while the encoder rebuilds. A real representation change
41
+ // arrives with different bytes and goes through the normal `prepareInit`
42
+ // path.
43
+ this.lastHevcInitBytes_ = null;
44
+ this.cachedH264Init_ = null;
36
45
  this.originalMimeType_ = mimeType;
37
46
  this.transcoderConfig_ = config;
38
47
  }
@@ -42,6 +51,8 @@ var HevcTransmuxer = class {
42
51
  this.initPromise_ = null;
43
52
  this.pendingHevcInit_ = null;
44
53
  this.h264InitEmitted_ = false;
54
+ this.lastHevcInitBytes_ = null;
55
+ this.cachedH264Init_ = null;
45
56
  }
46
57
  isSupported(mimeType, _contentType) {
47
58
  return HEVC_MIME_PATTERN.test(mimeType);
@@ -102,10 +113,21 @@ var HevcTransmuxer = class {
102
113
  }
103
114
  await this.initPromise_;
104
115
  if (isInit) {
116
+ if (this.cachedH264Init_ && bytesEqual(bytes, this.lastHevcInitBytes_)) {
117
+ const copy2 = new Uint8Array(this.cachedH264Init_.byteLength);
118
+ copy2.set(this.cachedH264Init_);
119
+ return copy2;
120
+ }
121
+ const initBytesSnapshot = new Uint8Array(bytes.byteLength);
122
+ initBytesSnapshot.set(bytes);
105
123
  const result = await this.transcoder_.prepareInit(bytes);
106
124
  this.h264InitEmitted_ = true;
107
- const copy = new Uint8Array(result.initSegment.byteLength);
108
- copy.set(result.initSegment);
125
+ const h264InitCopy = new Uint8Array(result.initSegment.byteLength);
126
+ h264InitCopy.set(result.initSegment);
127
+ this.lastHevcInitBytes_ = initBytesSnapshot;
128
+ this.cachedH264Init_ = h264InitCopy;
129
+ const copy = new Uint8Array(h264InitCopy.byteLength);
130
+ copy.set(h264InitCopy);
109
131
  return copy;
110
132
  }
111
133
  const h264Media = await this.transcoder_.processMediaSegment(bytes);
@@ -115,6 +137,13 @@ var HevcTransmuxer = class {
115
137
  return h264Media;
116
138
  }
117
139
  };
140
+ function bytesEqual(a, b) {
141
+ if (!b || a.byteLength !== b.byteLength) return false;
142
+ for (let i = 0; i < a.byteLength; i++) {
143
+ if (a[i] !== b[i]) return false;
144
+ }
145
+ return true;
146
+ }
118
147
  function toUint8(data) {
119
148
  if (data instanceof Uint8Array) return data;
120
149
  if (data instanceof ArrayBuffer) return new Uint8Array(data);
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/transmuxer.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} 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\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 }\n\n isSupported(mimeType: string, _contentType?: string): boolean {\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 const result = await this.transcoder_!.prepareInit(bytes);\n this.h264InitEmitted_ = true;\n // Defensive copy: avoids any risk of the underlying ArrayBuffer being\n // detached or mutated between this return and the eventual MSE append.\n const copy = new Uint8Array(result.initSegment.byteLength);\n copy.set(result.initSegment);\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 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 * 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 * 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\";\n\nexport { HevcTransmuxer } from \"./transmuxer.js\";\nexport type { TransmuxOutput, HevcTransmuxerConfig } from \"./transmuxer.js\";\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype ShakaNamespace = 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.\n */\nexport type HevcShakaPluginConfig = HevcTransmuxerConfig;\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)\n * @returns A cleanup function that unregisters the transmuxer.\n */\nexport function registerHevcTransmuxer(\n shaka: ShakaNamespace,\n config: HevcShakaPluginConfig = {},\n): () => void {\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 () => {};\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 for (const mimeType of HEVC_MIME_TYPES) {\n engine.registerTransmuxer(\n mimeType,\n () => new HevcTransmuxer(mimeType, config),\n priority,\n );\n }\n\n return () => {\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"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/transmuxer.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} 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 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 * 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 * 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\";\n\nexport { HevcTransmuxer } from \"./transmuxer.js\";\nexport type { TransmuxOutput, HevcTransmuxerConfig } from \"./transmuxer.js\";\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype ShakaNamespace = 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.\n */\nexport type HevcShakaPluginConfig = HevcTransmuxerConfig;\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)\n * @returns A cleanup function that unregisters the transmuxer.\n */\nexport function registerHevcTransmuxer(\n shaka: ShakaNamespace,\n config: HevcShakaPluginConfig = {},\n): () => void {\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 () => {};\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 for (const mimeType of HEVC_MIME_TYPES) {\n engine.registerTransmuxer(\n mimeType,\n () => new HevcTransmuxer(mimeType, config),\n priority,\n );\n }\n\n return () => {\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"],"mappings":";AAWA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AA4BP,IAAM,oBAAoB;AAS1B,IAAM,cAAc,IAAI,WAAW;AAAA,EACjC;AAAA,EAAG;AAAA,EAAG;AAAA,EAAG;AAAA;AAAA,EACT;AAAA,EAAM;AAAA,EAAM;AAAA,EAAM;AAAA;AACpB,CAAC;AASM,SAAS,cAAc,OAA4B;AACxD,MAAI,MAAM,SAAS,EAAG,QAAO;AAC7B,QAAM,UAAU,OAAO;AAAA,IACrB,MAAM,CAAC;AAAA,IACP,MAAM,CAAC;AAAA,IACP,MAAM,CAAC;AAAA,IACP,MAAM,CAAC;AAAA,EACT;AACA,SAAO,YAAY;AACrB;AAEO,IAAM,iBAAN,MAAqB;AAAA,EAiB1B,YAAY,UAAkB,SAA+B,CAAC,GAAG;AAdjE,SAAQ,cAAgE;AACxE,SAAQ,eAAqC;AAC7C,SAAQ,mBAAsC;AAC9C,SAAQ,mBAAmB;AAQ3B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,SAAQ,qBAAwC;AAChD,SAAQ,kBAAqC;AAG3C,SAAK,oBAAoB;AACzB,SAAK,oBAAoB;AAAA,EAC3B;AAAA,EAEA,UAAgB;AACd,SAAK,aAAa,QAAQ;AAC1B,SAAK,cAAc;AACnB,SAAK,eAAe;AACpB,SAAK,mBAAmB;AACxB,SAAK,mBAAmB;AACxB,SAAK,qBAAqB;AAC1B,SAAK,kBAAkB;AAAA,EACzB;AAAA,EAEA,YAAY,UAAkB,cAAgC;AAC5D,WAAO,kBAAkB,KAAK,QAAQ;AAAA,EACxC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,cAAc,cAAsB,UAA0B;AAC5D,QAAI,CAAC,kBAAkB,KAAK,QAAQ,EAAG,QAAO;AAC9C,WAAO,sBAAsB,oBAAoB,QAAQ,CAAC;AAAA,EAC5D;AAAA,EAEA,sBAA8B;AAC5B,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAkBA,MAAM,SACJ,MACA,SACA,WACA,WACA,cACyB;AACzB,UAAM,QAAQ,QAAQ,IAAI;AAC1B,UAAM,SAAS,aAAa,QAAQ,cAAc,KAAK;AAEvD,QAAI,CAAC,KAAK,aAAa;AACrB,YAAM,YAAY,KAAK,kBAAkB;AACzC,UAAI,WAAW;AACb,cAAM,SAAS,IAAI,sBAAsB;AAAA,UACvC,GAAG,KAAK;AAAA,UACR;AAAA,QACF,CAAC;AACD,aAAK,cAAc;AACnB,aAAK,eAAe,OAAO,UAAU;AACrC,gBAAQ;AAAA,UACN,6DAA6D,SAAS;AAAA,QACxE;AAAA,MACF,OAAO;AACL,cAAM,QAAQ,IAAI,kBAAkB,KAAK,iBAAiB;AAC1D,aAAK,cAAc;AACnB,aAAK,eAAe,MAAM,KAAK;AAC/B,gBAAQ;AAAA,UACN;AAAA,QACF;AAAA,MACF;AAAA,IACF;AACA,UAAM,KAAK;AAEX,QAAI,QAAQ;AAOV,UAAI,KAAK,mBAAmB,WAAW,OAAO,KAAK,kBAAkB,GAAG;AACtE,cAAMA,QAAO,IAAI,WAAW,KAAK,gBAAgB,UAAU;AAC3D,QAAAA,MAAK,IAAI,KAAK,eAAe;AAC7B,eAAOA;AAAA,MACT;AAMA,YAAM,oBAAoB,IAAI,WAAW,MAAM,UAAU;AACzD,wBAAkB,IAAI,KAAK;AAE3B,YAAM,SAAS,MAAM,KAAK,YAAa,YAAY,KAAK;AACxD,WAAK,mBAAmB;AAIxB,YAAM,eAAe,IAAI,WAAW,OAAO,YAAY,UAAU;AACjE,mBAAa,IAAI,OAAO,WAAW;AACnC,WAAK,qBAAqB;AAC1B,WAAK,kBAAkB;AAIvB,YAAM,OAAO,IAAI,WAAW,aAAa,UAAU;AACnD,WAAK,IAAI,YAAY;AACrB,aAAO;AAAA,IACT;AAEA,UAAM,YAAY,MAAM,KAAK,YAAa,oBAAoB,KAAK;AACnE,QAAI,CAAC,WAAW;AAId,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT;AACF;AAEA,SAAS,WAAW,GAAe,GAA+B;AAChE,MAAI,CAAC,KAAK,EAAE,eAAe,EAAE,WAAY,QAAO;AAChD,WAAS,IAAI,GAAG,IAAI,EAAE,YAAY,KAAK;AACrC,QAAI,EAAE,CAAC,MAAM,EAAE,CAAC,EAAG,QAAO;AAAA,EAC5B;AACA,SAAO;AACT;AAEA,SAAS,QAAQ,MAAgC;AAC/C,MAAI,gBAAgB,WAAY,QAAO;AACvC,MAAI,gBAAgB,YAAa,QAAO,IAAI,WAAW,IAAI;AAC3D,SAAO,IAAI;AAAA,IACR,KAAyB;AAAA,IACzB,KAAyB;AAAA,IACzB,KAAyB;AAAA,EAC5B;AACF;;;AChMA,IAAM,kBAAkB;AAAA,EACtB;AAAA,EACA;AACF;AAaO,SAAS,uBACd,OACA,SAAgC,CAAC,GACrB;AACZ,QAAM,SAAS,OAAO,YAAY;AAClC,MAAI,CAAC,UAAU,OAAO,OAAO,uBAAuB,YAAY;AAC9D,YAAQ;AAAA,MACN;AAAA,IAEF;AACA,WAAO,MAAM;AAAA,IAAC;AAAA,EAChB;AAMA,QAAM,WACJ,OAAO,gBAAgB,eACvB,OAAO,gBAAgB,aACvB;AAEF,aAAW,YAAY,iBAAiB;AACtC,WAAO;AAAA,MACL;AAAA,MACA,MAAM,IAAI,eAAe,UAAU,MAAM;AAAA,MACzC;AAAA,IACF;AAAA,EACF;AAEA,SAAO,MAAM;AACX,QAAI,OAAO,OAAO,yBAAyB,YAAY;AACrD,iBAAW,YAAY,iBAAiB;AAGtC,eAAO,qBAAqB,UAAU,QAAQ;AAAA,MAChD;AAAA,IACF;AAAA,EACF;AACF;","names":["copy"]}
@@ -43,6 +43,8 @@ export declare class HevcTransmuxer {
43
43
  private initPromise_;
44
44
  private pendingHevcInit_;
45
45
  private h264InitEmitted_;
46
+ private lastHevcInitBytes_;
47
+ private cachedH264Init_;
46
48
  constructor(mimeType: string, config?: HevcTransmuxerConfig);
47
49
  destroy(): void;
48
50
  isSupported(mimeType: string, _contentType?: string): boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"transmuxer.d.ts","sourceRoot":"","sources":["../src/transmuxer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAOH,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAC;AAE5D;;;;GAIG;AACH,MAAM,WAAW,oBAAqB,SAAQ,uBAAuB;IACnE,sFAAsF;IACtF,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAID,KAAK,WAAW,GAAG,GAAG,CAAC;AAEvB,KAAK,qBAAqB,GAAG,GAAG,CAAC;AAEjC;;;;;;GAMG;AACH,MAAM,MAAM,cAAc,GAAG,UAAU,CAAC;AAgBxC;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CASxD;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;IAC3C,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAuB;IACzD,OAAO,CAAC,WAAW,CAA0D;IAC7E,OAAO,CAAC,YAAY,CAA8B;IAClD,OAAO,CAAC,gBAAgB,CAA2B;IACnD,OAAO,CAAC,gBAAgB,CAAS;gBAErB,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAE,oBAAyB;IAK/D,OAAO,IAAI,IAAI;IAQf,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,OAAO;IAI7D;;;;;;OAMG;IACH,aAAa,CAAC,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM;IAK7D,mBAAmB,IAAI,MAAM;IAI7B;;;;;;;;;;;;;;;OAeG;IACG,QAAQ,CACZ,IAAI,EAAE,YAAY,EAClB,OAAO,EAAE,WAAW,EACpB,SAAS,EAAE,qBAAqB,EAChC,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,cAAc,CAAC;CA8C3B"}
1
+ {"version":3,"file":"transmuxer.d.ts","sourceRoot":"","sources":["../src/transmuxer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAOH,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAC;AAE5D;;;;GAIG;AACH,MAAM,WAAW,oBAAqB,SAAQ,uBAAuB;IACnE,sFAAsF;IACtF,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAID,KAAK,WAAW,GAAG,GAAG,CAAC;AAEvB,KAAK,qBAAqB,GAAG,GAAG,CAAC;AAEjC;;;;;;GAMG;AACH,MAAM,MAAM,cAAc,GAAG,UAAU,CAAC;AAgBxC;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CASxD;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;IAC3C,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAuB;IACzD,OAAO,CAAC,WAAW,CAA0D;IAC7E,OAAO,CAAC,YAAY,CAA8B;IAClD,OAAO,CAAC,gBAAgB,CAA2B;IACnD,OAAO,CAAC,gBAAgB,CAAS;IAQjC,OAAO,CAAC,kBAAkB,CAA2B;IACrD,OAAO,CAAC,eAAe,CAA2B;gBAEtC,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAE,oBAAyB;IAK/D,OAAO,IAAI,IAAI;IAUf,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,OAAO;IAI7D;;;;;;OAMG;IACH,aAAa,CAAC,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM;IAK7D,mBAAmB,IAAI,MAAM;IAI7B;;;;;;;;;;;;;;;OAeG;IACG,QAAQ,CACZ,IAAI,EAAE,YAAY,EAClB,OAAO,EAAE,WAAW,EACpB,SAAS,EAAE,qBAAqB,EAChC,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,cAAc,CAAC;CAyE3B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hevcjs/shaka-plugin",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
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/privaloops/hevc.js/issues"
50
50
  },
51
51
  "dependencies": {
52
- "@hevcjs/core": "1.2.0"
52
+ "@hevcjs/core": "1.2.1"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "shaka-player": ">=4.0.0"