@hevcjs/shaka-plugin 0.2.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/LICENSE +21 -0
- package/README.md +49 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +162 -0
- package/dist/index.js.map +1 -0
- package/dist/transmuxer.d.ts +77 -0
- package/dist/transmuxer.d.ts.map +1 -0
- package/package.json +73 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Thibaut Lion
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# @hevcjs/shaka-plugin
|
|
2
|
+
|
|
3
|
+
> **Status: experimental skeleton.** Package layout, registration, and Shaka Transmuxer interface are in place. The actual HEVC → H.264 conversion is a TODO that will plug into [`@hevcjs/core`](../core).
|
|
4
|
+
|
|
5
|
+
HEVC/H.265 playback plugin for [Shaka Player](https://github.com/shaka-project/shaka-player). Registers a custom Shaka `Transmuxer` that ingests HEVC fMP4 segments and (eventually) emits H.264 fMP4 that any browser can play via MSE.
|
|
6
|
+
|
|
7
|
+
Tracks issue [#101](https://github.com/privaloops/hevc.js/issues/101).
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @hevcjs/shaka-plugin shaka-player
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage (target API)
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
import shaka from 'shaka-player';
|
|
19
|
+
import { registerHevcTransmuxer } from '@hevcjs/shaka-plugin';
|
|
20
|
+
|
|
21
|
+
const cleanup = registerHevcTransmuxer(shaka, {
|
|
22
|
+
workerUrl: '/transcode-worker.js',
|
|
23
|
+
wasmUrl: '/hevc-decode.js',
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const player = new shaka.Player();
|
|
27
|
+
await player.attach(document.querySelector('video'));
|
|
28
|
+
await player.load('https://example.com/stream/manifest.mpd');
|
|
29
|
+
|
|
30
|
+
// later: cleanup();
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## How It Works
|
|
34
|
+
|
|
35
|
+
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`:
|
|
36
|
+
|
|
37
|
+
1. `registerHevcTransmuxer(shaka)` calls `shaka.transmuxer.TransmuxerEngine.registerTransmuxer()` for `video/mp4; codecs="hev1"` and `hvc1`.
|
|
38
|
+
2. When Shaka encounters HEVC content it can't play natively, it instantiates `HevcTransmuxer` and feeds it segments via `transmux(data, stream, reference, duration)`.
|
|
39
|
+
3. The transmuxer decodes HEVC via `@hevcjs/core` (WASM) and re-encodes to H.264 via WebCodecs, returning an MSE-ready fMP4 segment.
|
|
40
|
+
|
|
41
|
+
## Requirements
|
|
42
|
+
|
|
43
|
+
- Chrome 94+, Edge 94+, or Firefox with WebCodecs H.264 encoding support
|
|
44
|
+
- Secure Context (HTTPS or localhost)
|
|
45
|
+
- shaka-player >= 4.0.0
|
|
46
|
+
|
|
47
|
+
## License
|
|
48
|
+
|
|
49
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shaka Player HEVC Plugin — public entry point.
|
|
3
|
+
*
|
|
4
|
+
* Usage (main thread, no Worker):
|
|
5
|
+
* ```ts
|
|
6
|
+
* import shaka from 'shaka-player';
|
|
7
|
+
* import { registerHevcTransmuxer } from '@hevcjs/shaka-plugin';
|
|
8
|
+
*
|
|
9
|
+
* registerHevcTransmuxer(shaka, { wasmUrl: '/hevc-decode.js' });
|
|
10
|
+
* const player = new shaka.Player();
|
|
11
|
+
* await player.attach(videoElement);
|
|
12
|
+
* await player.load(manifestUrl);
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* Usage (off-main-thread via Web Worker — recommended for 4K / smoothness):
|
|
16
|
+
* ```ts
|
|
17
|
+
* registerHevcTransmuxer(shaka, {
|
|
18
|
+
* wasmUrl: '/hevc-decode.js',
|
|
19
|
+
* workerUrl: '/transcode-worker.js',
|
|
20
|
+
* });
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* To force the transmuxer even on browsers with native HEVC support
|
|
24
|
+
* (Safari, recent Chrome on macOS), use Shaka's built-in config rather
|
|
25
|
+
* than patching MSE yourself:
|
|
26
|
+
*
|
|
27
|
+
* ```ts
|
|
28
|
+
* player.configure({ mediaSource: { forceTransmux: true } });
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
import type { HevcTransmuxerConfig } from "./transmuxer.js";
|
|
32
|
+
export { HevcTransmuxer } from "./transmuxer.js";
|
|
33
|
+
export type { TransmuxOutput, HevcTransmuxerConfig } from "./transmuxer.js";
|
|
34
|
+
type ShakaNamespace = any;
|
|
35
|
+
/**
|
|
36
|
+
* Plugin configuration. Forwarded as-is to `HevcTransmuxer`. Supports the
|
|
37
|
+
* `SegmentTranscoderConfig` fields (`wasmUrl`, `wasmBinaryUrl`, `fps`,
|
|
38
|
+
* `bitrate`) plus an optional `workerUrl` that, when set, routes the
|
|
39
|
+
* HEVC decode + H.264 encode pipeline through a Web Worker.
|
|
40
|
+
*/
|
|
41
|
+
export type HevcShakaPluginConfig = HevcTransmuxerConfig;
|
|
42
|
+
/**
|
|
43
|
+
* Register the HEVC transmuxer with Shaka's TransmuxerEngine.
|
|
44
|
+
*
|
|
45
|
+
* Must be called before `player.load()`. Registers a factory for both
|
|
46
|
+
* `hev1` and `hvc1` MIME types at APPLICATION priority so Shaka picks
|
|
47
|
+
* our transmuxer over any default fallback.
|
|
48
|
+
*
|
|
49
|
+
* @param shaka the global `shaka` namespace (import or window.shaka)
|
|
50
|
+
* @param config forwarded to `HevcTransmuxer` (wasmUrl, wasmBinaryUrl, fps, bitrate, workerUrl)
|
|
51
|
+
* @returns A cleanup function that unregisters the transmuxer.
|
|
52
|
+
*/
|
|
53
|
+
export declare function registerHevcTransmuxer(shaka: ShakaNamespace, config?: HevcShakaPluginConfig): () => void;
|
|
54
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAGH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAE5D,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjD,YAAY,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAG5E,KAAK,cAAc,GAAG,GAAG,CAAC;AAE1B;;;;;GAKG;AACH,MAAM,MAAM,qBAAqB,GAAG,oBAAoB,CAAC;AAOzD;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,cAAc,EACrB,MAAM,GAAE,qBAA0B,GACjC,MAAM,IAAI,CAoCZ"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// src/transmuxer.ts
|
|
2
|
+
import {
|
|
3
|
+
SegmentTranscoder,
|
|
4
|
+
TranscodeWorkerClient,
|
|
5
|
+
hevcMimeToH264Codec
|
|
6
|
+
} from "@hevcjs/core";
|
|
7
|
+
var HEVC_MIME_PATTERN = /^video\/mp4\s*;.*codecs="?(hev1|hvc1)/i;
|
|
8
|
+
var FREE_BOX_8B = new Uint8Array([
|
|
9
|
+
0,
|
|
10
|
+
0,
|
|
11
|
+
0,
|
|
12
|
+
8,
|
|
13
|
+
// size = 8
|
|
14
|
+
102,
|
|
15
|
+
114,
|
|
16
|
+
101,
|
|
17
|
+
101
|
|
18
|
+
// 'free'
|
|
19
|
+
]);
|
|
20
|
+
function isInitSegment(bytes) {
|
|
21
|
+
if (bytes.length < 8) return false;
|
|
22
|
+
const boxType = String.fromCharCode(
|
|
23
|
+
bytes[4],
|
|
24
|
+
bytes[5],
|
|
25
|
+
bytes[6],
|
|
26
|
+
bytes[7]
|
|
27
|
+
);
|
|
28
|
+
return boxType === "ftyp";
|
|
29
|
+
}
|
|
30
|
+
var HevcTransmuxer = class {
|
|
31
|
+
constructor(mimeType, config = {}) {
|
|
32
|
+
this.transcoder_ = null;
|
|
33
|
+
this.initPromise_ = null;
|
|
34
|
+
this.pendingHevcInit_ = null;
|
|
35
|
+
this.h264InitEmitted_ = false;
|
|
36
|
+
this.originalMimeType_ = mimeType;
|
|
37
|
+
this.transcoderConfig_ = config;
|
|
38
|
+
}
|
|
39
|
+
destroy() {
|
|
40
|
+
this.transcoder_?.destroy();
|
|
41
|
+
this.transcoder_ = null;
|
|
42
|
+
this.initPromise_ = null;
|
|
43
|
+
this.pendingHevcInit_ = null;
|
|
44
|
+
this.h264InitEmitted_ = false;
|
|
45
|
+
}
|
|
46
|
+
isSupported(mimeType, _contentType) {
|
|
47
|
+
return HEVC_MIME_PATTERN.test(mimeType);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Output mime advertised to Shaka before any frame has been encoded.
|
|
51
|
+
* Best-effort mapping based on the HEVC level declared in the input
|
|
52
|
+
* (see `@hevcjs/core/codec-mapping`). The actual encoded stream may
|
|
53
|
+
* use a slightly different profile/level if `H264Encoder` decides
|
|
54
|
+
* differently from the encoded resolution.
|
|
55
|
+
*/
|
|
56
|
+
convertCodecs(_contentType, mimeType) {
|
|
57
|
+
if (!HEVC_MIME_PATTERN.test(mimeType)) return mimeType;
|
|
58
|
+
return `video/mp4; codecs="${hevcMimeToH264Codec(mimeType)}"`;
|
|
59
|
+
}
|
|
60
|
+
getOriginalMimeType() {
|
|
61
|
+
return this.originalMimeType_;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Convert one HEVC fMP4 segment into an MSE-ready H.264 fMP4 segment.
|
|
65
|
+
*
|
|
66
|
+
* Shaka calls this once per segment with `reference === null` for the
|
|
67
|
+
* init segment and a non-null `reference` for media segments.
|
|
68
|
+
*
|
|
69
|
+
* - Init segment: warm up the H.264 encoder eagerly (encodes a single
|
|
70
|
+
* black frame to obtain a valid avcC) and return a complete H.264
|
|
71
|
+
* init segment that MSE can immediately ingest.
|
|
72
|
+
* - Media segment: decode HEVC, re-encode to H.264, mux fMP4, return.
|
|
73
|
+
*
|
|
74
|
+
* Returns a raw `Uint8Array` rather than `{data, init}` so the same
|
|
75
|
+
* code path works on Shaka 4.x (which expects a `Uint8Array` directly)
|
|
76
|
+
* and on Shaka 5+ (which accepts either via an `ArrayBuffer.isView`
|
|
77
|
+
* check). Init/media segmentation is implicit in the call sequence.
|
|
78
|
+
*/
|
|
79
|
+
async transmux(data, _stream, reference, _duration, _contentType) {
|
|
80
|
+
const bytes = toUint8(data);
|
|
81
|
+
const isInit = reference == null || isInitSegment(bytes);
|
|
82
|
+
if (!this.transcoder_) {
|
|
83
|
+
const workerUrl = this.transcoderConfig_.workerUrl;
|
|
84
|
+
if (workerUrl) {
|
|
85
|
+
const worker = new TranscodeWorkerClient({
|
|
86
|
+
...this.transcoderConfig_,
|
|
87
|
+
workerUrl
|
|
88
|
+
});
|
|
89
|
+
this.transcoder_ = worker;
|
|
90
|
+
this.initPromise_ = worker.waitReady();
|
|
91
|
+
console.log(
|
|
92
|
+
`[hevc.js/shaka] HEVC transcoding routed through Worker at ${workerUrl}`
|
|
93
|
+
);
|
|
94
|
+
} else {
|
|
95
|
+
const local = new SegmentTranscoder(this.transcoderConfig_);
|
|
96
|
+
this.transcoder_ = local;
|
|
97
|
+
this.initPromise_ = local.init();
|
|
98
|
+
console.log(
|
|
99
|
+
"[hevc.js/shaka] HEVC transcoding runs on main thread (no workerUrl provided)"
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
await this.initPromise_;
|
|
104
|
+
if (isInit) {
|
|
105
|
+
const result = await this.transcoder_.prepareInit(bytes);
|
|
106
|
+
this.h264InitEmitted_ = true;
|
|
107
|
+
const copy = new Uint8Array(result.initSegment.byteLength);
|
|
108
|
+
copy.set(result.initSegment);
|
|
109
|
+
return copy;
|
|
110
|
+
}
|
|
111
|
+
const h264Media = await this.transcoder_.processMediaSegment(bytes);
|
|
112
|
+
if (!h264Media) {
|
|
113
|
+
return FREE_BOX_8B;
|
|
114
|
+
}
|
|
115
|
+
return h264Media;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
function toUint8(data) {
|
|
119
|
+
if (data instanceof Uint8Array) return data;
|
|
120
|
+
if (data instanceof ArrayBuffer) return new Uint8Array(data);
|
|
121
|
+
return new Uint8Array(
|
|
122
|
+
data.buffer,
|
|
123
|
+
data.byteOffset,
|
|
124
|
+
data.byteLength
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/index.ts
|
|
129
|
+
var HEVC_MIME_TYPES = [
|
|
130
|
+
'video/mp4; codecs="hev1"',
|
|
131
|
+
'video/mp4; codecs="hvc1"'
|
|
132
|
+
];
|
|
133
|
+
function registerHevcTransmuxer(shaka, config = {}) {
|
|
134
|
+
const engine = shaka?.transmuxer?.TransmuxerEngine;
|
|
135
|
+
if (!engine || typeof engine.registerTransmuxer !== "function") {
|
|
136
|
+
console.warn(
|
|
137
|
+
"[hevc.js/shaka] shaka.transmuxer.TransmuxerEngine.registerTransmuxer not found. Make sure shaka-player >= 4.0 is loaded before calling registerHevcTransmuxer()."
|
|
138
|
+
);
|
|
139
|
+
return () => {
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const priority = engine.PluginPriority?.APPLICATION ?? engine.PluginPriority?.PREFERRED ?? 4;
|
|
143
|
+
for (const mimeType of HEVC_MIME_TYPES) {
|
|
144
|
+
engine.registerTransmuxer(
|
|
145
|
+
mimeType,
|
|
146
|
+
() => new HevcTransmuxer(mimeType, config),
|
|
147
|
+
priority
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
return () => {
|
|
151
|
+
if (typeof engine.unregisterTransmuxer === "function") {
|
|
152
|
+
for (const mimeType of HEVC_MIME_TYPES) {
|
|
153
|
+
engine.unregisterTransmuxer(mimeType, priority);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
export {
|
|
159
|
+
HevcTransmuxer,
|
|
160
|
+
registerHevcTransmuxer
|
|
161
|
+
};
|
|
162
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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":[]}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HEVC Transmuxer for Shaka Player.
|
|
3
|
+
*
|
|
4
|
+
* Implements the `shaka.extern.Transmuxer` interface so Shaka can ingest
|
|
5
|
+
* HEVC/H.265 fMP4 segments on browsers that lack native HEVC support.
|
|
6
|
+
* Uses `@hevcjs/core` SegmentTranscoder to decode HEVC and re-encode to
|
|
7
|
+
* H.264 fMP4 that the browser's MSE can play.
|
|
8
|
+
*
|
|
9
|
+
* Modeled after `lib/transmuxer/aac_transmuxer.js` in shaka-player.
|
|
10
|
+
*/
|
|
11
|
+
import type { SegmentTranscoderConfig } from "@hevcjs/core";
|
|
12
|
+
/**
|
|
13
|
+
* Config accepted by `HevcTransmuxer` (and forwarded by `registerHevcTransmuxer`).
|
|
14
|
+
* When `workerUrl` is set, transcoding runs inside a Web Worker; otherwise
|
|
15
|
+
* the HEVC decode + H.264 encode pipeline runs on the main thread.
|
|
16
|
+
*/
|
|
17
|
+
export interface HevcTransmuxerConfig extends SegmentTranscoderConfig {
|
|
18
|
+
/** URL to the transcode worker script. When set, transcoding runs off main thread. */
|
|
19
|
+
workerUrl?: string;
|
|
20
|
+
}
|
|
21
|
+
type ShakaStream = any;
|
|
22
|
+
type ShakaSegmentReference = any;
|
|
23
|
+
/**
|
|
24
|
+
* Return type of `HevcTransmuxer.transmux`. Compatible with both Shaka 4.x
|
|
25
|
+
* (which expects a raw `Uint8Array` and passes it straight to MSE) and 5+
|
|
26
|
+
* (which checks `ArrayBuffer.isView` and falls back to `{data, init}`
|
|
27
|
+
* when the value is a plain object). Returning a `Uint8Array` is the
|
|
28
|
+
* common subset that works on every supported Shaka version.
|
|
29
|
+
*/
|
|
30
|
+
export type TransmuxOutput = Uint8Array;
|
|
31
|
+
/**
|
|
32
|
+
* Sniff whether a buffer starts with an ISO BMFF init segment.
|
|
33
|
+
* Init segments begin with the `ftyp` box; media segments begin with
|
|
34
|
+
* `moof` (or `styp` followed by `moof`).
|
|
35
|
+
*
|
|
36
|
+
* Box header layout: 4 bytes big-endian size, 4 bytes ASCII type.
|
|
37
|
+
*/
|
|
38
|
+
export declare function isInitSegment(bytes: Uint8Array): boolean;
|
|
39
|
+
export declare class HevcTransmuxer {
|
|
40
|
+
private readonly originalMimeType_;
|
|
41
|
+
private readonly transcoderConfig_;
|
|
42
|
+
private transcoder_;
|
|
43
|
+
private initPromise_;
|
|
44
|
+
private pendingHevcInit_;
|
|
45
|
+
private h264InitEmitted_;
|
|
46
|
+
constructor(mimeType: string, config?: HevcTransmuxerConfig);
|
|
47
|
+
destroy(): void;
|
|
48
|
+
isSupported(mimeType: string, _contentType?: string): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Output mime advertised to Shaka before any frame has been encoded.
|
|
51
|
+
* Best-effort mapping based on the HEVC level declared in the input
|
|
52
|
+
* (see `@hevcjs/core/codec-mapping`). The actual encoded stream may
|
|
53
|
+
* use a slightly different profile/level if `H264Encoder` decides
|
|
54
|
+
* differently from the encoded resolution.
|
|
55
|
+
*/
|
|
56
|
+
convertCodecs(_contentType: string, mimeType: string): string;
|
|
57
|
+
getOriginalMimeType(): string;
|
|
58
|
+
/**
|
|
59
|
+
* Convert one HEVC fMP4 segment into an MSE-ready H.264 fMP4 segment.
|
|
60
|
+
*
|
|
61
|
+
* Shaka calls this once per segment with `reference === null` for the
|
|
62
|
+
* init segment and a non-null `reference` for media segments.
|
|
63
|
+
*
|
|
64
|
+
* - Init segment: warm up the H.264 encoder eagerly (encodes a single
|
|
65
|
+
* black frame to obtain a valid avcC) and return a complete H.264
|
|
66
|
+
* init segment that MSE can immediately ingest.
|
|
67
|
+
* - Media segment: decode HEVC, re-encode to H.264, mux fMP4, return.
|
|
68
|
+
*
|
|
69
|
+
* Returns a raw `Uint8Array` rather than `{data, init}` so the same
|
|
70
|
+
* code path works on Shaka 4.x (which expects a `Uint8Array` directly)
|
|
71
|
+
* and on Shaka 5+ (which accepts either via an `ArrayBuffer.isView`
|
|
72
|
+
* check). Init/media segmentation is implicit in the call sequence.
|
|
73
|
+
*/
|
|
74
|
+
transmux(data: BufferSource, _stream: ShakaStream, reference: ShakaSegmentReference, _duration: number, _contentType: string): Promise<TransmuxOutput>;
|
|
75
|
+
}
|
|
76
|
+
export {};
|
|
77
|
+
//# sourceMappingURL=transmuxer.d.ts.map
|
|
@@ -0,0 +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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hevcjs/shaka-plugin",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Shaka Player plugin for HEVC/H.265 playback — registers a Shaka Transmuxer that decodes HEVC streams via @hevcjs/core",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist/",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"keywords": [
|
|
20
|
+
"hevc",
|
|
21
|
+
"h265",
|
|
22
|
+
"h.265",
|
|
23
|
+
"webassembly",
|
|
24
|
+
"wasm",
|
|
25
|
+
"codec",
|
|
26
|
+
"video",
|
|
27
|
+
"browser",
|
|
28
|
+
"streaming",
|
|
29
|
+
"shaka",
|
|
30
|
+
"shaka-player",
|
|
31
|
+
"shaka-plugin",
|
|
32
|
+
"transmuxer",
|
|
33
|
+
"dash",
|
|
34
|
+
"hls",
|
|
35
|
+
"mse",
|
|
36
|
+
"mediasource",
|
|
37
|
+
"ott",
|
|
38
|
+
"media-source-extensions"
|
|
39
|
+
],
|
|
40
|
+
"author": "privaloops",
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"homepage": "https://hevcjs.dev",
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "https://github.com/privaloops/hevc.js.git",
|
|
46
|
+
"directory": "packages/shaka-plugin"
|
|
47
|
+
},
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://github.com/privaloops/hevc.js/issues"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@hevcjs/core": "1.2.0"
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"shaka-player": ">=4.0.0"
|
|
56
|
+
},
|
|
57
|
+
"peerDependenciesMeta": {
|
|
58
|
+
"shaka-player": {
|
|
59
|
+
"optional": true
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"shaka-player": "^4.11.0",
|
|
64
|
+
"tsup": "^8.0.0",
|
|
65
|
+
"typescript": "^6.0.3",
|
|
66
|
+
"vitest": "^3.0.0"
|
|
67
|
+
},
|
|
68
|
+
"scripts": {
|
|
69
|
+
"build": "tsup && tsc -p tsconfig.build.json",
|
|
70
|
+
"dev": "tsup --watch",
|
|
71
|
+
"test": "vitest run"
|
|
72
|
+
}
|
|
73
|
+
}
|