@muhkoo/theater-transcoder 0.2.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -1
- package/package.json +1 -1
- package/src/ffmpeg.js +96 -0
- package/src/transcode.js +126 -12
- package/src/worker.js +2 -1
package/README.md
CHANGED
|
@@ -15,7 +15,19 @@ npm i -g @muhkoo/theater-transcoder
|
|
|
15
15
|
MUHKOO_USERNAME=you MUHKOO_PASSWORD=… theater-transcoder
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
You'll be prompted for your password if it isn't in `$MUHKOO_PASSWORD`. Credentials never leave the machine — it runs the same **zero-knowledge login** the browser uses.
|
|
18
|
+
You'll be prompted for your password if it isn't in `$MUHKOO_PASSWORD`. Credentials never leave the machine — it runs the same **zero-knowledge login** the browser uses. Requires **Node ≥ 20** (≥ 22 recommended).
|
|
19
|
+
|
|
20
|
+
### ffmpeg (hardware acceleration)
|
|
21
|
+
|
|
22
|
+
The transcoder prefers a **system-installed `ffmpeg`** and offloads work to the GPU when one is available — **Apple VideoToolbox**, **NVIDIA NVENC**, **Intel QuickSync**, or **VAAPI**. It runs the whole pipeline on-GPU where it can (**hardware decode → scale → encode**, frames never leaving the GPU), which on NVENC/QuickSync boxes hits many times realtime; on Apple Silicon the encoder is the ceiling but GPU decode still roughly halves CPU use. It automatically steps down — full-GPU → GPU-encode-only → software libx264 — to whatever actually runs on the machine.
|
|
23
|
+
|
|
24
|
+
- **Best:** install ffmpeg so hardware encoding kicks in automatically:
|
|
25
|
+
- macOS: `brew install ffmpeg`
|
|
26
|
+
- Debian/Ubuntu: `sudo apt install ffmpeg`
|
|
27
|
+
- (or point at a specific build with `FFMPEG_PATH=/path/to/ffmpeg`)
|
|
28
|
+
- **Fallback:** if no system ffmpeg is found, the bundled `ffmpeg-static` binary is used (software libx264 — works everywhere, just slower). You'll see a one-time warning nudging you to install ffmpeg.
|
|
29
|
+
|
|
30
|
+
On startup the worker logs exactly which binary and encoder it picked, e.g. `✓ hardware encoder: Apple VideoToolbox (h264_videotoolbox)`. If a GPU path is present but fails on a given file, it logs the fallback and keeps going — down to software libx264 if needed.
|
|
19
31
|
|
|
20
32
|
Leave it running. Upload videos in the app and this box starts chewing through the queue. Ctrl-C to stop.
|
|
21
33
|
|
|
@@ -37,3 +49,5 @@ Offline + P2P are auto-disabled in Node (no IndexedDB / WebRTC), so its shard an
|
|
|
37
49
|
| `--password` | `MUHKOO_PASSWORD` | prompt |
|
|
38
50
|
| `--base` | `MUHKOO_BASE_URL` | `https://api.muhkoo.dev` |
|
|
39
51
|
| `--app-key` | `MUHKOO_THEATER_KEY` | public prod key |
|
|
52
|
+
| — | `FFMPEG_PATH` | system `ffmpeg` → bundled static |
|
|
53
|
+
| — | `VAAPI_DEVICE` | `/dev/dri/renderD128` |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@muhkoo/theater-transcoder",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Drain a Muhkoo Theater transcode queue with NATIVE ffmpeg. Run it on any machine signed in as you to add transcoding power to your library.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/ffmpeg.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve which ffmpeg binary to run and which H.264 encoder to use.
|
|
3
|
+
*
|
|
4
|
+
* Preference order, best → worst:
|
|
5
|
+
* 1. A **system-installed ffmpeg** (via `FFMPEG_PATH` or on `PATH`) — usually
|
|
6
|
+
* built with hardware encoders (Apple VideoToolbox, NVIDIA NVENC, Intel
|
|
7
|
+
* QuickSync, VAAPI). These offload encoding to dedicated silicon and are
|
|
8
|
+
* dramatically faster than software libx264.
|
|
9
|
+
* 2. The bundled **`ffmpeg-static`** binary (software libx264 only). Works
|
|
10
|
+
* everywhere with zero install, but slow — so we warn and nudge the user to
|
|
11
|
+
* `brew/apt install ffmpeg` for hardware acceleration.
|
|
12
|
+
*
|
|
13
|
+
* We probe the chosen binary's encoder list AND run a tiny functional test
|
|
14
|
+
* encode (an encoder can be listed but fail at runtime — e.g. NVENC with no GPU),
|
|
15
|
+
* then pick the fastest encoder that actually works.
|
|
16
|
+
*/
|
|
17
|
+
import { spawnSync } from "node:child_process";
|
|
18
|
+
import ffmpegStatic from "ffmpeg-static";
|
|
19
|
+
|
|
20
|
+
const log = (...a) => console.log(new Date().toISOString().slice(11, 19), ...a);
|
|
21
|
+
|
|
22
|
+
/** Hardware H.264 encoders, in preference order. */
|
|
23
|
+
const HW_ENCODERS = [
|
|
24
|
+
{ codec: "h264_videotoolbox", label: "Apple VideoToolbox" },
|
|
25
|
+
{ codec: "h264_nvenc", label: "NVIDIA NVENC" },
|
|
26
|
+
{ codec: "h264_qsv", label: "Intel QuickSync" },
|
|
27
|
+
{ codec: "h264_vaapi", label: "VAAPI" },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
function runnable(bin) {
|
|
31
|
+
try {
|
|
32
|
+
const r = spawnSync(bin, ["-hide_banner", "-version"], { encoding: "utf8", timeout: 5000 });
|
|
33
|
+
return r.status === 0 && /ffmpeg version/i.test(r.stdout || "");
|
|
34
|
+
} catch { return false; }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Find a runnable ffmpeg: FFMPEG_PATH → system PATH → bundled static. */
|
|
38
|
+
function resolveBinary() {
|
|
39
|
+
const explicit = process.env.FFMPEG_PATH;
|
|
40
|
+
if (explicit) {
|
|
41
|
+
if (runnable(explicit)) return { path: explicit, system: true, source: `FFMPEG_PATH (${explicit})` };
|
|
42
|
+
log(`⚠︎ FFMPEG_PATH="${explicit}" is not a runnable ffmpeg — ignoring it.`);
|
|
43
|
+
}
|
|
44
|
+
if (runnable("ffmpeg")) return { path: "ffmpeg", system: true, source: "system install" };
|
|
45
|
+
return { path: ffmpegStatic, system: false, source: "ffmpeg-static (bundled)" };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function listEncoders(bin) {
|
|
49
|
+
try {
|
|
50
|
+
const r = spawnSync(bin, ["-hide_banner", "-encoders"], { encoding: "utf8", timeout: 5000 });
|
|
51
|
+
return r.status === 0 ? (r.stdout || "") : "";
|
|
52
|
+
} catch { return ""; }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Encode a couple of synthetic frames with `codec` to /dev/null; true if it works. */
|
|
56
|
+
function functionalTest(bin, codec) {
|
|
57
|
+
const common = ["-hide_banner", "-loglevel", "error", "-f", "lavfi", "-i", "testsrc=size=192x108:rate=5:duration=1"];
|
|
58
|
+
const args = codec === "h264_vaapi"
|
|
59
|
+
? ["-vaapi_device", process.env.VAAPI_DEVICE || "/dev/dri/renderD128", ...common,
|
|
60
|
+
"-vf", "format=nv12,hwupload", "-c:v", codec, "-frames:v", "3", "-f", "null", "-"]
|
|
61
|
+
: [...common, "-c:v", codec, "-frames:v", "3", "-f", "null", "-"];
|
|
62
|
+
try {
|
|
63
|
+
const r = spawnSync(bin, args, { encoding: "utf8", timeout: 12000 });
|
|
64
|
+
return r.status === 0;
|
|
65
|
+
} catch { return false; }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @returns {{ ffmpegPath: string, encoder: { codec: string, label: string, hw: boolean } }}
|
|
70
|
+
*/
|
|
71
|
+
export function resolveEncoder() {
|
|
72
|
+
const bin = resolveBinary();
|
|
73
|
+
const encoders = listEncoders(bin.path);
|
|
74
|
+
|
|
75
|
+
let chosen = null;
|
|
76
|
+
for (const cand of HW_ENCODERS) {
|
|
77
|
+
if (encoders.includes(cand.codec) && functionalTest(bin.path, cand.codec)) { chosen = cand; break; }
|
|
78
|
+
}
|
|
79
|
+
const encoder = chosen
|
|
80
|
+
? { ...chosen, hw: true }
|
|
81
|
+
: { codec: "libx264", label: "libx264 (software)", hw: false };
|
|
82
|
+
|
|
83
|
+
if (bin.system) {
|
|
84
|
+
log(`✓ ffmpeg: ${bin.source}`);
|
|
85
|
+
} else {
|
|
86
|
+
log("⚠︎ Using the bundled ffmpeg (no system install found). This encodes in software");
|
|
87
|
+
log(" and is much slower. Install ffmpeg for hardware-accelerated transcoding:");
|
|
88
|
+
log(" macOS: brew install ffmpeg");
|
|
89
|
+
log(" Debian/Ubuntu: sudo apt install ffmpeg");
|
|
90
|
+
log(" The transcoder auto-detects it on next start (or set FFMPEG_PATH).");
|
|
91
|
+
}
|
|
92
|
+
if (encoder.hw) log(`✓ hardware encoder: ${encoder.label} (${encoder.codec})`);
|
|
93
|
+
else log(`ℹ no usable hardware H.264 encoder — using software libx264 (${bin.source}).`);
|
|
94
|
+
|
|
95
|
+
return { ffmpegPath: bin.path, encoder };
|
|
96
|
+
}
|
package/src/transcode.js
CHANGED
|
@@ -2,20 +2,101 @@
|
|
|
2
2
|
* Native-ffmpeg transcode → HLS, producing the SAME structure the browser writes
|
|
3
3
|
* so playback is identical: each ~6s segment is stored as an encrypted shard via
|
|
4
4
|
* `space.putFile` (→ manifest), and an HLS index `{variants:[{height,bandwidth,
|
|
5
|
-
* segments:[{duration,manifest}]}],duration}` is stored as its own file.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* segments:[{duration,manifest}]}],duration}` is stored as its own file.
|
|
6
|
+
*
|
|
7
|
+
* The binary + encoder are resolved once (`ffmpeg.js`): a system ffmpeg with a
|
|
8
|
+
* hardware encoder (VideoToolbox/NVENC/QuickSync/VAAPI) when available, else the
|
|
9
|
+
* bundled software libx264. Hardware encoding offloads to dedicated silicon and
|
|
10
|
+
* is far faster than the browser's wasm build; software libx264 uses all cores.
|
|
8
11
|
*/
|
|
9
12
|
import { spawn } from "node:child_process";
|
|
10
13
|
import { mkdtemp, readFile, writeFile, rm } from "node:fs/promises";
|
|
11
14
|
import { tmpdir } from "node:os";
|
|
12
15
|
import { join } from "node:path";
|
|
13
|
-
import
|
|
16
|
+
import { resolveEncoder } from "./ffmpeg.js";
|
|
17
|
+
|
|
18
|
+
const log = (...a) => console.log(new Date().toISOString().slice(11, 19), ...a);
|
|
19
|
+
|
|
20
|
+
// Resolved once, lazily (probing spawns a few ffmpeg processes). `ensureEncoder`
|
|
21
|
+
// lets the worker trigger + log the choice at startup instead of on first job.
|
|
22
|
+
let _resolved;
|
|
23
|
+
export function ensureEncoder() {
|
|
24
|
+
return (_resolved ??= resolveEncoder());
|
|
25
|
+
}
|
|
14
26
|
|
|
15
27
|
// Match the browser's rendition ladder (single 720p for now).
|
|
16
28
|
const RENDITIONS = [{ height: 720, videoBitrate: "2500k", audioBitrate: "128k", bandwidth: 2_800_000 }];
|
|
17
29
|
const SEGMENT_SECONDS = 6;
|
|
18
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Ordered encode strategies for a rendition on the resolved encoder — fastest
|
|
33
|
+
* first, each a full recipe of { input flags before -i, -vf filter, codec args }:
|
|
34
|
+
* 1. full GPU pipeline: hardware decode + scale + encode, frames never leave the
|
|
35
|
+
* GPU (huge win on NVENC/QuickSync/VAAPI);
|
|
36
|
+
* 2. hardware decode + encode, CPU scale (frames round-trip system memory);
|
|
37
|
+
* 3. software libx264 (works everywhere).
|
|
38
|
+
* `encodeRendition` tries them in order and remembers which one runs on this box.
|
|
39
|
+
*/
|
|
40
|
+
function buildStrategies(codec, rend) {
|
|
41
|
+
const kf = ["-force_key_frames", `expr:gte(t,n_forced*${SEGMENT_SECONDS})`];
|
|
42
|
+
const scale = `scale=-2:${rend.height}`;
|
|
43
|
+
const bv = ["-b:v", rend.videoBitrate];
|
|
44
|
+
const vaapiDev = process.env.VAAPI_DEVICE || "/dev/dri/renderD128";
|
|
45
|
+
|
|
46
|
+
const software = {
|
|
47
|
+
label: "software libx264", input: [], filter: scale,
|
|
48
|
+
video: ["-threads", "0", "-c:v", "libx264", "-preset", "veryfast", ...bv,
|
|
49
|
+
"-pix_fmt", "yuv420p", "-profile:v", "main", "-level", "4.0", ...kf, "-sc_threshold", "0"],
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
switch (codec) {
|
|
53
|
+
case "h264_videotoolbox":
|
|
54
|
+
// VideoToolbox on Apple Silicon is encoder-bound, so a GPU scale buys little;
|
|
55
|
+
// GPU *decode* still halves CPU. One hw tier, then software.
|
|
56
|
+
return [
|
|
57
|
+
{ label: "VideoToolbox (GPU decode+encode)", input: ["-hwaccel", "videotoolbox"], filter: scale,
|
|
58
|
+
video: ["-c:v", "h264_videotoolbox", ...bv, "-profile:v", "main", "-pix_fmt", "yuv420p", "-realtime", "1", ...kf] },
|
|
59
|
+
software,
|
|
60
|
+
];
|
|
61
|
+
case "h264_nvenc":
|
|
62
|
+
return [
|
|
63
|
+
{ label: "NVENC (full GPU pipeline)", input: ["-hwaccel", "cuda", "-hwaccel_output_format", "cuda"],
|
|
64
|
+
filter: `scale_cuda=-2:${rend.height}:format=yuv420p`,
|
|
65
|
+
video: ["-c:v", "h264_nvenc", "-preset", "p4", "-tune", "hq", "-rc", "vbr", ...bv, "-profile:v", "main", ...kf] },
|
|
66
|
+
{ label: "NVENC (GPU encode, CPU scale)", input: ["-hwaccel", "cuda"], filter: scale,
|
|
67
|
+
video: ["-c:v", "h264_nvenc", "-preset", "p4", "-tune", "hq", "-rc", "vbr", ...bv, "-profile:v", "main", "-pix_fmt", "yuv420p", ...kf] },
|
|
68
|
+
software,
|
|
69
|
+
];
|
|
70
|
+
case "h264_qsv":
|
|
71
|
+
return [
|
|
72
|
+
{ label: "QuickSync (full GPU pipeline)", input: ["-hwaccel", "qsv", "-hwaccel_output_format", "qsv"],
|
|
73
|
+
filter: `scale_qsv=-2:${rend.height}`,
|
|
74
|
+
video: ["-c:v", "h264_qsv", "-preset", "veryfast", ...bv, "-profile:v", "main", ...kf] },
|
|
75
|
+
{ label: "QuickSync (GPU encode, CPU scale)", input: ["-hwaccel", "qsv"], filter: scale,
|
|
76
|
+
video: ["-c:v", "h264_qsv", "-preset", "veryfast", ...bv, "-profile:v", "main", "-pix_fmt", "nv12", ...kf] },
|
|
77
|
+
software,
|
|
78
|
+
];
|
|
79
|
+
case "h264_vaapi":
|
|
80
|
+
return [
|
|
81
|
+
{ label: "VAAPI (full GPU pipeline)", input: ["-hwaccel", "vaapi", "-hwaccel_output_format", "vaapi", "-vaapi_device", vaapiDev],
|
|
82
|
+
filter: `scale_vaapi=-2:${rend.height}`,
|
|
83
|
+
video: ["-c:v", "h264_vaapi", ...bv, "-profile:v", "main", ...kf] },
|
|
84
|
+
{ label: "VAAPI (GPU encode, CPU scale)", input: ["-vaapi_device", vaapiDev],
|
|
85
|
+
filter: `${scale},format=nv12,hwupload`,
|
|
86
|
+
video: ["-c:v", "h264_vaapi", ...bv, "-profile:v", "main", ...kf] },
|
|
87
|
+
software,
|
|
88
|
+
];
|
|
89
|
+
default:
|
|
90
|
+
return [software];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Index into the encoder's strategy list to START from — advanced permanently
|
|
95
|
+
// only when a strategy fails *fast* (unavailable on this machine), so a one-off
|
|
96
|
+
// bad file doesn't downgrade every subsequent job. See encodeRendition.
|
|
97
|
+
let _stratStart = 0;
|
|
98
|
+
const FAST_FAIL_MS = 8000;
|
|
99
|
+
|
|
19
100
|
function parseDurations(m3u8) {
|
|
20
101
|
const out = [];
|
|
21
102
|
for (const line of m3u8.split("\n")) {
|
|
@@ -30,16 +111,15 @@ function hhmmssToSec(s) {
|
|
|
30
111
|
return m ? Number(m[1]) * 3600 + Number(m[2]) * 60 + Number(m[3]) : 0;
|
|
31
112
|
}
|
|
32
113
|
|
|
33
|
-
/** Run one ffmpeg rendition to HLS; report fraction via
|
|
34
|
-
|
|
114
|
+
/** Run one ffmpeg rendition to HLS with a given strategy; report fraction via
|
|
115
|
+
* stderr `time=` vs total. */
|
|
116
|
+
function runFfmpeg(ffmpegPath, strat, inPath, dir, rend, playlist, onFrac, signal) {
|
|
35
117
|
return new Promise((resolve, reject) => {
|
|
36
118
|
const args = [
|
|
119
|
+
...strat.input,
|
|
37
120
|
"-i", inPath,
|
|
38
|
-
"-vf",
|
|
39
|
-
|
|
40
|
-
"-c:v", "libx264", "-preset", "veryfast", "-b:v", rend.videoBitrate,
|
|
41
|
-
"-pix_fmt", "yuv420p", "-profile:v", "main", "-level", "4.0",
|
|
42
|
-
"-force_key_frames", `expr:gte(t,n_forced*${SEGMENT_SECONDS})`, "-sc_threshold", "0",
|
|
121
|
+
"-vf", strat.filter,
|
|
122
|
+
...strat.video,
|
|
43
123
|
"-c:a", "aac", "-ac", "2", "-b:a", rend.audioBitrate,
|
|
44
124
|
"-hls_time", String(SEGMENT_SECONDS), "-hls_playlist_type", "vod",
|
|
45
125
|
"-hls_segment_filename", join(dir, `r${rend.height}_%03d.ts`),
|
|
@@ -65,11 +145,44 @@ function runFfmpeg(inPath, dir, rend, playlist, onFrac, signal) {
|
|
|
65
145
|
});
|
|
66
146
|
}
|
|
67
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Encode one rendition, trying strategies fastest→slowest and falling back on
|
|
150
|
+
* failure. If a strategy fails *fast* (before real encoding — i.e. it's not
|
|
151
|
+
* available on this machine, like a missing GPU filter), we advance the starting
|
|
152
|
+
* point permanently so future jobs skip it. A *slow* failure (mid-encode, likely
|
|
153
|
+
* file-specific) falls back for this file only, leaving fast paths for next time.
|
|
154
|
+
*/
|
|
155
|
+
async function encodeRendition(ffmpegPath, encoder, inPath, dir, rend, playlist, onFrac, signal) {
|
|
156
|
+
const strategies = buildStrategies(encoder.codec, rend);
|
|
157
|
+
const start = Math.min(_stratStart, strategies.length - 1);
|
|
158
|
+
let lastErr;
|
|
159
|
+
for (let i = start; i < strategies.length; i++) {
|
|
160
|
+
const strat = strategies[i];
|
|
161
|
+
const t0 = Date.now();
|
|
162
|
+
try {
|
|
163
|
+
await runFfmpeg(ffmpegPath, strat, inPath, dir, rend, playlist, onFrac, signal);
|
|
164
|
+
if (i > start) log(`✓ "${strat.label}" — using it for subsequent jobs.`);
|
|
165
|
+
return;
|
|
166
|
+
} catch (e) {
|
|
167
|
+
if (signal?.aborted) throw e;
|
|
168
|
+
lastErr = e;
|
|
169
|
+
// Fast failure ⇒ this strategy isn't available on this box; skip it for good.
|
|
170
|
+
if (Date.now() - t0 < FAST_FAIL_MS) _stratStart = Math.max(_stratStart, i + 1);
|
|
171
|
+
const next = strategies[i + 1];
|
|
172
|
+
if (next) {
|
|
173
|
+
log(`⚠︎ "${strat.label}" failed (${String(e?.message || e).slice(0, 140)}) — falling back to "${next.label}".`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
throw lastErr;
|
|
178
|
+
}
|
|
179
|
+
|
|
68
180
|
/**
|
|
69
181
|
* Transcode raw bytes into HLS shards stored in `space`.
|
|
70
182
|
* @returns {{ hls_index: unknown, duration: number, size: number }}
|
|
71
183
|
*/
|
|
72
184
|
export async function transcodeToHls({ rawBytes, filename, space, onProgress, signal }) {
|
|
185
|
+
const { ffmpegPath, encoder } = ensureEncoder();
|
|
73
186
|
const dir = await mkdtemp(join(tmpdir(), "tt-"));
|
|
74
187
|
try {
|
|
75
188
|
const ext = filename.match(/\.\w+$/)?.[0] || ".mp4";
|
|
@@ -82,8 +195,9 @@ export async function transcodeToHls({ rawBytes, filename, space, onProgress, si
|
|
|
82
195
|
for (let r = 0; r < nR; r++) {
|
|
83
196
|
const rend = RENDITIONS[r];
|
|
84
197
|
const playlist = join(dir, `r${rend.height}.m3u8`);
|
|
198
|
+
const onFrac = (f) => onProgress?.({ fraction: (r + f) / nR, label: `Encoding ${rend.height}p…` });
|
|
85
199
|
onProgress?.({ fraction: r / nR, label: `Encoding ${rend.height}p…` });
|
|
86
|
-
await
|
|
200
|
+
await encodeRendition(ffmpegPath, encoder, inPath, dir, rend, playlist, onFrac, signal);
|
|
87
201
|
|
|
88
202
|
const durations = parseDurations(await readFile(playlist, "utf8"));
|
|
89
203
|
const segments = [];
|
package/src/worker.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* background, and the browser can also process it locally from memory.
|
|
7
7
|
*/
|
|
8
8
|
import { hostname, cpus } from "node:os";
|
|
9
|
-
import { transcodeToHls } from "./transcode.js";
|
|
9
|
+
import { transcodeToHls, ensureEncoder } from "./transcode.js";
|
|
10
10
|
|
|
11
11
|
const WORKER_ID = `node-${Math.random().toString(36).slice(2, 10)}`;
|
|
12
12
|
const WORKER_LABEL = `the transcoder (${hostname()})`;
|
|
@@ -108,6 +108,7 @@ export function startWorker({ client, space, appKey, baseUrl }) {
|
|
|
108
108
|
let stopped = false;
|
|
109
109
|
(async () => {
|
|
110
110
|
log(`worker ${WORKER_ID} draining queue as ${client.auth.zk.user.username} (${WORKER_LABEL}, score ${MY_SCORE})`);
|
|
111
|
+
ensureEncoder(); // probe + log the ffmpeg binary / hardware encoder up front
|
|
111
112
|
// Announce capability so browsers defer manifest-ready jobs to us.
|
|
112
113
|
const hello = () => void postSignal({ kind: "hello", score: MY_SCORE, worker: WORKER_LABEL, native: true });
|
|
113
114
|
hello(); setInterval(hello, 4_000);
|