@platless/axom-cli 0.1.0 → 0.5.9
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 +1 -1
- package/dist/authScopes.js +19 -0
- package/dist/axom-patch/cli/README.md +132 -0
- package/dist/axom-patch/cli/account-limits/README.md +27 -0
- package/dist/axom-patch/cli/account-limits/antigravity-proxy.mjs +47 -0
- package/dist/axom-patch/cli/account-limits/antigravity-read.mjs +90 -0
- package/dist/axom-patch/cli/account-limits/antigravity.mjs +46 -0
- package/dist/axom-patch/cli/account-limits/generate.py +120 -0
- package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +64 -0
- package/dist/axom-patch/cli/account-limits/normalize.mjs +151 -0
- package/dist/axom-patch/cli/claude-account-limits.json +37 -0
- package/dist/axom-patch/cli/codex-account-limits.json +37 -0
- package/dist/axom-patch/cli/commandcode-account-limits.json +30 -0
- package/dist/axom-patch/cli/nightly-task.md +152 -0
- package/dist/axom-patch/cli/pi-steering.json +37 -0
- package/dist/axom-patch/cli/pi-subagent-subwork.json +23 -0
- package/dist/axom-patch/cli/probes/claude-late-async-terminal.mjs +38 -0
- package/dist/axom-patch/cli/probes/pi-steering.mjs +80 -0
- package/dist/axom-patch/cli/probes/pi-subagent-subwork.mjs +211 -0
- package/dist/axom-patch/cli/probes/qoder-steering.mjs +89 -0
- package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +61 -0
- package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +257 -0
- package/dist/axom-patch/cli/qoder-background-terminal.md +82 -0
- package/dist/axom-patch/cli/qoder-foreground-output.md +42 -0
- package/dist/axom-patch/cli/qoder-steering.json +52 -0
- package/dist/axom-patch/cli/qoder-steering.md +30 -0
- package/dist/axom-patch/cli/qoder-subagent-origin.json +73 -0
- package/dist/axom-patch/cli/qoder-subagent-subwork.md +73 -0
- package/dist/axom-patch/cli/runtime.json +63 -0
- package/dist/axom-patch/cli/steering/claude-steering-js.patch +89 -0
- package/dist/axom-patch/cli/steering/claude-steering-types.patch +28 -0
- package/dist/axom-patch/cli/steering/codex-steering.patch +57 -0
- package/dist/axom-patch/cli/steering/pi-steering.js +79 -0
- package/dist/axom-patch/cli/zcode-account-limits.json +31 -0
- package/dist/axom-patch/cli/zcode-models.json +38 -0
- package/dist/axom-patch/cli/zcode-subagent-subwork.json +73 -0
- package/dist/cli.js +36 -1
- package/dist/commands/auth.js +4 -13
- package/dist/commands/execute.js +4 -4
- package/dist/commands/record.js +6 -6
- package/dist/commands/struct.js +4 -4
- package/dist/commands/workstation.js +2 -2
- package/dist/credentials.js +2 -2
- package/dist/install/claude/commands.js +13 -13
- package/dist/keepers/acp/keeper.mjs +3083 -0
- package/dist/keepers/acp/manifest.json +9 -0
- package/dist/keepers/terminal/keeper.mjs +8469 -0
- package/dist/keepers/terminal/manifest.json +9 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/conpty_console_list_agent.js +25 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/eventEmitter2.js +47 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/index.js +52 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/interfaces.js +7 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/shared/conout.js +10 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/terminal.js +190 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/types.js +7 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/unixTerminal.js +351 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/utils.js +38 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/windowsConoutConnection.js +131 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/windowsPtyAgent.js +344 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/windowsTerminal.js +209 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/worker/conoutSocketWorker.js +22 -0
- package/dist/keepers/terminal/node_modules/node-pty/package.json +66 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/pty.node +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-arm64/pty.node +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-x64/pty.node +0 -0
- package/dist/mcp.js +32 -14
- package/dist/runtime-deps/qoder-sdk/package-lock.json +1189 -0
- package/dist/runtime-deps/qoder-sdk/package.json +8 -0
- package/dist/test/keeperKit.js +67 -0
- package/dist/workstation/acp/acpAgents.js +2514 -681
- package/dist/workstation/acp/acpClient.js +95 -318
- package/dist/workstation/acp/activityBusy.js +43 -0
- package/dist/workstation/acp/activityReporting.js +54 -0
- package/dist/workstation/acp/adoption.js +28 -356
- package/dist/workstation/acp/authUrlSniffer.js +36 -0
- package/dist/workstation/acp/binaryInstall.js +100 -27
- package/dist/workstation/acp/builds/gc.js +245 -0
- package/dist/workstation/acp/builds/install/binary.js +28 -0
- package/dist/workstation/acp/builds/install/npm.js +235 -0
- package/dist/workstation/acp/builds/install/uv.js +254 -0
- package/dist/workstation/acp/builds/migrate.js +195 -0
- package/dist/workstation/acp/builds/model.js +128 -0
- package/dist/workstation/acp/builds/nodeGate.js +100 -0
- package/dist/workstation/acp/builds/store.js +352 -0
- package/dist/workstation/acp/commandTableProbe.js +68 -0
- package/dist/workstation/acp/configOptions.js +103 -1
- package/dist/workstation/acp/delegationMcp.js +24 -0
- package/dist/workstation/acp/envSecrets.js +27 -0
- package/dist/workstation/acp/initializeResponse.js +285 -0
- package/dist/workstation/acp/installErrors.js +12 -2
- package/dist/workstation/acp/installProbe.js +1 -1
- package/dist/workstation/acp/managedPatches.js +144 -0
- package/dist/workstation/acp/outbox.js +330 -0
- package/dist/workstation/acp/patchAssets.js +80 -0
- package/dist/workstation/acp/permission.js +335 -37
- package/dist/workstation/acp/processor/checkpoint.js +87 -0
- package/dist/workstation/acp/processor/commands.js +35 -0
- package/dist/workstation/acp/processor/control.js +355 -0
- package/dist/workstation/acp/processor/emit.js +26 -0
- package/dist/workstation/acp/processor/jsonrpc.js +172 -0
- package/dist/workstation/acp/processor/memoryLog.js +49 -0
- package/dist/workstation/acp/processor/origins.js +48 -0
- package/dist/workstation/acp/processor/output.js +67 -0
- package/dist/workstation/acp/processor/projection.js +42 -0
- package/dist/workstation/acp/processor/record.js +128 -0
- package/dist/workstation/acp/processor/runtime.js +181 -0
- package/dist/workstation/acp/processor/sdkSchemas.js +61 -0
- package/dist/workstation/acp/processor/session.js +525 -0
- package/dist/workstation/acp/processor/sideChannel.js +80 -0
- package/dist/workstation/acp/processor/state.js +74 -0
- package/dist/workstation/acp/processor/step.js +459 -0
- package/dist/workstation/acp/processor/test/fixtureLog.js +65 -0
- package/dist/workstation/acp/processor/test/harness.js +120 -0
- package/dist/workstation/acp/processor/test/largeIdEcho.js +50 -0
- package/dist/workstation/acp/processor/test/rawAgent.js +70 -0
- package/dist/workstation/acp/processor/transport/generations.js +76 -0
- package/dist/workstation/acp/processor/transport/keeper.js +130 -0
- package/dist/workstation/acp/processor/transport/local.js +269 -0
- package/dist/workstation/acp/processor/transport/types.js +78 -0
- package/dist/workstation/acp/processor/updates.js +364 -0
- package/dist/workstation/acp/processorSession.js +1512 -0
- package/dist/workstation/acp/qoderStartup.js +45 -0
- package/dist/workstation/acp/registry/catalog.js +385 -4
- package/dist/workstation/acp/registry/detect.js +55 -0
- package/dist/workstation/acp/registry/launch.js +61 -119
- package/dist/workstation/acp/sessionUpdateMapper.js +56 -27
- package/dist/workstation/acp/steering.js +58 -0
- package/dist/workstation/acp/switching.js +114 -0
- package/dist/workstation/acp/terminalLogin.js +171 -0
- package/dist/workstation/acp/terminalLoginSpec.js +41 -0
- package/dist/workstation/acp/test/adapterFixtures.js +256 -0
- package/dist/workstation/acp/test/daemonLife.js +106 -0
- package/dist/workstation/acp/test/fakeAgent.js +185 -9
- package/dist/workstation/acp/test/keeperWorld.js +267 -0
- package/dist/workstation/acp/test/memoryOutbox.js +55 -0
- package/dist/workstation/acp/test/raw/fixtures.js +196 -0
- package/dist/workstation/acp/test/raw/qoderCaptures.js +125 -0
- package/dist/workstation/acp/test/raw/sanitize.js +344 -0
- package/dist/workstation/acp/test/raw/sources.js +249 -0
- package/dist/workstation/acp/test/realAdapter.js +377 -0
- package/dist/workstation/acp/test/scriptAgent.js +206 -0
- package/dist/workstation/acp/test/sessionFrames.js +42 -0
- package/dist/workstation/acp/timedCommand.js +4 -2
- package/dist/workstation/acp/uvxInstall.js +72 -122
- package/dist/workstation/acp/validate.js +33 -5
- package/dist/workstation/agentFlow.js +103 -0
- package/dist/workstation/agentSessions.js +1686 -348
- package/dist/workstation/channels/agent.js +14 -6
- package/dist/workstation/channels/computer.js +203 -7
- package/dist/workstation/channels/computerExecs.js +200 -0
- package/dist/workstation/channels/management.js +86 -6
- package/dist/workstation/config.js +3 -1
- package/dist/workstation/connection/encryptedChannel.js +88 -21
- package/dist/workstation/daemon.js +309 -6
- package/dist/workstation/daemonctl.js +28 -2
- package/dist/workstation/desktop/binding.js +36 -0
- package/dist/workstation/desktop/bridge.js +212 -0
- package/dist/workstation/desktop/flow-control.js +52 -0
- package/dist/workstation/desktop/focused-text.js +130 -0
- package/dist/workstation/desktop/protocol.js +35 -0
- package/dist/workstation/exec/attach.js +126 -0
- package/dist/workstation/exec/command.js +40 -0
- package/dist/workstation/exec/durable.js +517 -0
- package/dist/workstation/exec/fallback.js +106 -0
- package/dist/workstation/exec/policy.js +837 -0
- package/dist/workstation/exec/shaping.js +211 -0
- package/dist/workstation/exec.js +3 -112
- package/dist/workstation/files/adapters/computerFileFrames.js +51 -0
- package/dist/workstation/files/adapters/toolRequests.js +121 -0
- package/dist/workstation/files/client.js +455 -0
- package/dist/workstation/files/errors.js +28 -0
- package/dist/workstation/files/fence.js +350 -0
- package/dist/workstation/files/find/fileIndex.js +153 -0
- package/dist/workstation/files/find/fuzzy.js +155 -0
- package/dist/workstation/files/host.js +122 -0
- package/dist/workstation/files/hostProtocol.js +2 -0
- package/dist/workstation/files/media/cache.js +620 -0
- package/dist/workstation/files/media/encoding.js +308 -0
- package/dist/workstation/files/media/estimator.js +100 -0
- package/dist/workstation/files/media/media.js +325 -0
- package/dist/workstation/files/media/packedAudio.js +23 -0
- package/dist/workstation/files/media/scheduler.js +551 -0
- package/dist/workstation/files/media/types.js +5 -0
- package/dist/workstation/files/scope.js +45 -0
- package/dist/workstation/files/structured.js +284 -0
- package/dist/workstation/files/text.js +356 -0
- package/dist/workstation/files/uploads.js +351 -0
- package/dist/workstation/journal.js +65 -236
- package/dist/workstation/keepers/acp/client.js +101 -0
- package/dist/workstation/keepers/acp/envelope.js +62 -0
- package/dist/workstation/keepers/acp/host.js +419 -0
- package/dist/workstation/keepers/acp/main.js +18 -0
- package/dist/workstation/keepers/acp/memoryStream.js +121 -0
- package/dist/workstation/keepers/acp/params.js +137 -0
- package/dist/workstation/keepers/acp/protocol.js +66 -0
- package/dist/workstation/keepers/acp/recover.js +120 -0
- package/dist/workstation/keepers/acp/session.js +584 -0
- package/dist/workstation/keepers/acp/stderr.js +154 -0
- package/dist/workstation/keepers/kit/activity.js +103 -0
- package/dist/workstation/keepers/kit/bootstrap.js +164 -0
- package/dist/workstation/keepers/kit/buildVersion.js +51 -0
- package/dist/workstation/keepers/kit/bundle.js +120 -0
- package/dist/workstation/keepers/kit/client.js +432 -0
- package/dist/workstation/keepers/kit/identity.js +21 -0
- package/dist/workstation/keepers/kit/ipc.js +112 -0
- package/dist/workstation/keepers/kit/launchConfig.js +8 -0
- package/dist/workstation/keepers/kit/launcher.js +249 -0
- package/dist/workstation/keepers/kit/lock.js +123 -0
- package/dist/workstation/keepers/kit/orphanSweep.js +86 -0
- package/dist/workstation/keepers/kit/paths.js +75 -0
- package/dist/workstation/keepers/kit/protocol.js +43 -0
- package/dist/workstation/keepers/kit/server.js +518 -0
- package/dist/workstation/keepers/kit/streamLog.js +697 -0
- package/dist/workstation/keepers/legacy.js +296 -0
- package/dist/workstation/keepers/policy.js +576 -0
- package/dist/workstation/keepers/stop.js +34 -0
- package/dist/workstation/keepers/terminal/client.js +213 -0
- package/dist/workstation/keepers/terminal/exec.js +981 -0
- package/dist/workstation/keepers/terminal/generations.js +129 -0
- package/dist/workstation/keepers/terminal/keeper.js +62 -0
- package/dist/workstation/keepers/terminal/main.js +43 -0
- package/dist/workstation/keepers/terminal/orphanSweep.js +3 -0
- package/dist/workstation/keepers/terminal/params.js +27 -0
- package/dist/workstation/keepers/terminal/protocol.js +56 -0
- package/dist/workstation/keepers/terminal/pty.js +330 -0
- package/dist/workstation/oauth/deviceAuthorization.js +21 -12
- package/dist/workstation/protocol/accountLimits.js +47 -0
- package/dist/workstation/protocol/agent.js +201 -8
- package/dist/workstation/protocol/common.js +36 -0
- package/dist/workstation/protocol/computer.js +107 -7
- package/dist/workstation/protocol/management.js +50 -2
- package/dist/workstation/qoderAdapter/backgroundProcess.js +118 -0
- package/dist/workstation/qoderAdapter/backgroundTerminal.js +193 -0
- package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +372 -0
- package/dist/workstation/qoderAdapter/callbackContext.js +76 -0
- package/dist/workstation/qoderAdapter/completedContinuation.js +139 -0
- package/dist/workstation/qoderAdapter/contract.js +78 -0
- package/dist/workstation/qoderAdapter/foregroundOutput.js +36 -0
- package/dist/workstation/qoderAdapter/history.js +85 -0
- package/dist/workstation/qoderAdapter/identityIndex.js +191 -0
- package/dist/workstation/qoderAdapter/inputQueue.js +42 -0
- package/dist/workstation/qoderAdapter/main.js +95 -0
- package/dist/workstation/qoderAdapter/mapper.js +117 -0
- package/dist/workstation/qoderAdapter/observerPlugin.js +245 -0
- package/dist/workstation/qoderAdapter/observerReceiver.js +187 -0
- package/dist/workstation/qoderAdapter/permissions.js +61 -0
- package/dist/workstation/qoderAdapter/profile.js +93 -0
- package/dist/workstation/qoderAdapter/sdkDriver.js +48 -0
- package/dist/workstation/qoderAdapter/sdkRuntime.js +93 -0
- package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +145 -0
- package/dist/workstation/qoderAdapter/session.js +319 -0
- package/dist/workstation/qoderAdapter/subagentMeta.js +28 -0
- package/dist/workstation/qoderAdapter/subwork.js +85 -0
- package/dist/workstation/qoderAdapter/taskControl.js +38 -0
- package/dist/workstation/qoderAdapter/taskLedger.js +163 -0
- package/dist/workstation/qoderAdapter/types.js +1 -0
- package/dist/workstation/qoderAdapter/usage.js +37 -0
- package/dist/workstation/register.js +4 -0
- package/dist/workstation/systemMetrics.js +110 -1
- package/dist/workstation/tools/browser.js +394 -0
- package/dist/workstation/tools/client.js +277 -0
- package/dist/workstation/tools/info.js +50 -0
- package/dist/workstation/tools/keeperProcess.js +124 -0
- package/dist/workstation/tools/router.js +55 -0
- package/dist/workstation/tools/terminalRouter.js +222 -0
- package/dist/workstation/tools/validation.js +86 -0
- package/dist/workstation/update/coordinator.js +32 -0
- package/dist/workstation/update/npmInstaller.js +2 -0
- package/dist/workstation/update/ptyPreflight.js +45 -0
- package/dist/workstation/update/taskRegistry.js +141 -23
- package/dist/workstation/update/watchdog.js +66 -0
- package/package.json +20 -7
- package/skills/axom/SKILL.md +55 -17
- package/skills/axom-definition/SKILL.md +32 -32
- package/skills/axom-platform/SKILL.md +39 -39
- package/dist/workstation/acp/acpSession.js +0 -1047
- package/dist/workstation/acp/contract.js +0 -494
- package/dist/workstation/acp/draftInterceptor.js +0 -154
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import { stampPackedAudio } from './packedAudio.js';
|
|
7
|
+
const FORMATS = 'mov,matroska,webm,mp3,wav,flac,ogg,aac,avi,aiff,png_pipe,jpeg_pipe,webp_pipe,bmp_pipe,tiff_pipe,gif';
|
|
8
|
+
const BOUNDS = ['-v', 'error', '-max_alloc', '134217728', '-protocol_whitelist', 'file', '-format_whitelist', FORMATS];
|
|
9
|
+
const MAX_SEGMENT_BYTES = 8 * 1024 * 1024;
|
|
10
|
+
const MAX_OBJECT_BYTES = 512 * 1024 * 1024;
|
|
11
|
+
const NO_PROGRESS_MS = 30_000;
|
|
12
|
+
/** Where relative AXOM_FFMPEG / AXOM_FFPROBE values are anchored: the process cwd at startup. Decoders run with
|
|
13
|
+
* `cwd: '/'` (a pinned file operation may own the process cwd meanwhile), so a relative path cannot stay relative. */
|
|
14
|
+
const STARTUP_CWD = globalThis.process.cwd();
|
|
15
|
+
/** The decoder named by `variable` (or `fallback`); a bare name is a PATH lookup, a relative path is made absolute. */
|
|
16
|
+
export function decoderCommand(variable, fallback, env = globalThis.process.env) {
|
|
17
|
+
const value = env[variable] ?? fallback;
|
|
18
|
+
return value.includes('/') && !path.isAbsolute(value) ? path.resolve(STARTUP_CWD, value) : value;
|
|
19
|
+
}
|
|
20
|
+
const identifier = () => randomUUID().replaceAll('-', '');
|
|
21
|
+
async function identity(input) {
|
|
22
|
+
const s = await input.stat({ bigint: true });
|
|
23
|
+
if (!s.isFile() || s.size > 32n * 1024n * 1024n * 1024n)
|
|
24
|
+
throw new Error('Not a supported regular media file');
|
|
25
|
+
return [s.dev, s.ino, s.size, s.mtimeNs, s.ctimeNs].join(':');
|
|
26
|
+
}
|
|
27
|
+
function cancelled(signal) { if (signal.aborted)
|
|
28
|
+
throw new Error('Media preparation cancelled'); }
|
|
29
|
+
/** Only local self-contained inputs. Diagnostics containing user paths never reach callers. */
|
|
30
|
+
async function runDecoder(command, args, input, signal, progress) {
|
|
31
|
+
cancelled(signal);
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
// An explicit cwd: the files host changes its own cwd inside pinned() sections, which a child would otherwise inherit.
|
|
34
|
+
const child = spawn(command, args, { cwd: '/', stdio: input ? ['ignore', 'pipe', 'pipe', input.fd] : ['ignore', 'pipe', 'pipe'] });
|
|
35
|
+
let output = '';
|
|
36
|
+
let line = '';
|
|
37
|
+
let error;
|
|
38
|
+
let lastProgress = Date.now();
|
|
39
|
+
let previous = -1;
|
|
40
|
+
let kill;
|
|
41
|
+
const cancel = (reason = 'Media preparation cancelled') => {
|
|
42
|
+
error ??= new Error(reason);
|
|
43
|
+
child.kill('SIGTERM');
|
|
44
|
+
if (!kill) {
|
|
45
|
+
kill = setTimeout(() => child.kill('SIGKILL'), 2000);
|
|
46
|
+
kill.unref();
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const abort = () => cancel();
|
|
50
|
+
const timer = setInterval(() => { if (Date.now() - lastProgress > NO_PROGRESS_MS)
|
|
51
|
+
cancel('Media preparation made no progress'); }, 1000);
|
|
52
|
+
timer.unref();
|
|
53
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
54
|
+
if (signal.aborted)
|
|
55
|
+
abort();
|
|
56
|
+
child.stdout.on('data', chunk => {
|
|
57
|
+
if (!progress) {
|
|
58
|
+
output += chunk.toString();
|
|
59
|
+
if (output.length > 1024 * 1024)
|
|
60
|
+
cancel('Media metadata exceeds limit');
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
line += chunk.toString();
|
|
64
|
+
if (line.length > 64 * 1024) {
|
|
65
|
+
cancel('Invalid media progress');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const lines = line.split('\n');
|
|
69
|
+
line = lines.pop();
|
|
70
|
+
for (const value of lines)
|
|
71
|
+
if (value.startsWith('out_time_us=')) {
|
|
72
|
+
const ms = Number(value.slice(12)) / 1000;
|
|
73
|
+
if (Number.isFinite(ms) && ms > previous) {
|
|
74
|
+
previous = ms;
|
|
75
|
+
lastProgress = Date.now();
|
|
76
|
+
progress(Math.max(0, ms));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
child.stderr.resume();
|
|
82
|
+
child.on('error', e => { error = new Error(e.code === 'ENOENT' ? 'Install FFmpeg and ffprobe on this workstation (or set AXOM_FFMPEG / AXOM_FFPROBE).' : 'Unable to start workstation media decoder'); });
|
|
83
|
+
child.on('close', code => {
|
|
84
|
+
clearInterval(timer);
|
|
85
|
+
if (kill)
|
|
86
|
+
clearTimeout(kill);
|
|
87
|
+
signal.removeEventListener('abort', abort);
|
|
88
|
+
if (error)
|
|
89
|
+
reject(error);
|
|
90
|
+
else if (code !== 0)
|
|
91
|
+
reject(new Error('Unable to prepare this media file. It may be damaged or unsupported.'));
|
|
92
|
+
else
|
|
93
|
+
resolve(output);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
export async function probeSource(input, signal) {
|
|
98
|
+
if (process.platform === 'win32')
|
|
99
|
+
throw new Error('Workstation media preparation currently requires macOS or Linux FFmpeg');
|
|
100
|
+
const raw = await runDecoder(decoderCommand('AXOM_FFPROBE', 'ffprobe'), [...BOUNDS, '-show_format', '-show_streams', '-of', 'json', '/dev/fd/3'], input, signal);
|
|
101
|
+
const data = JSON.parse(raw);
|
|
102
|
+
const video = data.streams.find(s => s.codec_type === 'video' && !s.disposition?.attached_pic);
|
|
103
|
+
const audio = data.streams.find(s => s.codec_type === 'audio');
|
|
104
|
+
const seconds = Number(data.format.duration ?? video?.duration ?? audio?.duration);
|
|
105
|
+
const image = !!video && !audio && !Number.isFinite(seconds);
|
|
106
|
+
if ((!video && !audio) || (!image && (!Number.isFinite(seconds) || seconds < 0 || seconds > 21600)))
|
|
107
|
+
throw new Error('Unsupported media or duration exceeds six hours');
|
|
108
|
+
if (video && (!video.width || !video.height || video.width * video.height > 40_000_000))
|
|
109
|
+
throw new Error('Image/video dimensions exceed preview limits');
|
|
110
|
+
const rate = video?.avg_frame_rate?.split('/').map(Number);
|
|
111
|
+
const ffmpegVersion = (await runDecoder(decoderCommand('AXOM_FFMPEG', 'ffmpeg'), ['-version'], null, signal)).split('\n')[0];
|
|
112
|
+
return { kind: image ? 'image' : video ? 'video' : 'audio', durationMs: image ? 0 : Math.round(seconds * 1000), durationAccuracy: 'exact',
|
|
113
|
+
sourceBytes: Number((await input.stat()).size), codec: video?.codec_name ?? audio.codec_name, audioCodec: audio?.codec_name,
|
|
114
|
+
pixelFormat: video?.pix_fmt, width: video?.width, height: video?.height, frameRate: rate && rate[1] ? rate[0] / rate[1] : undefined, ffmpegVersion };
|
|
115
|
+
}
|
|
116
|
+
export async function encodeGeneration(input, probe, startMs, directory, signal, onObject, onProgress) {
|
|
117
|
+
cancelled(signal);
|
|
118
|
+
if (!Number.isSafeInteger(startMs) || startMs < 0 || probe.durationMs == null || startMs > probe.durationMs)
|
|
119
|
+
throw new Error('Invalid media seek');
|
|
120
|
+
if (probe.kind === 'image')
|
|
121
|
+
throw new Error('Images use completed finite preparation');
|
|
122
|
+
const stamp = await identity(input);
|
|
123
|
+
await fs.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
124
|
+
const stat = await fs.lstat(directory);
|
|
125
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || (process.getuid && stat.uid !== process.getuid()))
|
|
126
|
+
throw new Error('Unsafe media cache');
|
|
127
|
+
const generation = { id: /^[a-f0-9]{32}$/.test(path.basename(directory)) ? path.basename(directory) : identifier(), sourceOffsetMs: startMs, timestampOriginMs: 0, targetDurationMs: 4000, objects: [], complete: false, sourceComplete: false, createdAtMs: Date.now() };
|
|
128
|
+
const video = probe.kind === 'video';
|
|
129
|
+
const extension = video ? 'ts' : 'mp3';
|
|
130
|
+
const list = path.join(directory, video ? 'encoder.m3u8' : 'encoder.csv');
|
|
131
|
+
const args = ['-nostdin', ...BOUNDS, '-threads', '2', '-filter_threads', '2', '-ss', String(startMs / 1000), '-i', '/dev/fd/3', '-map_metadata', '-1', '-progress', 'pipe:1', '-stats_period', '0.1'];
|
|
132
|
+
if (video)
|
|
133
|
+
args.push('-map', '0:v:0', '-map', '0:a:0?', '-vf', "fps=30,scale=w='min(1280,iw)':h='min(720,ih)':force_original_aspect_ratio=decrease:force_divisible_by=2", '-c:v', 'libx264', '-preset', 'veryfast', '-crf', '26', '-maxrate', '1800k', '-bufsize', '3600k', '-pix_fmt', 'yuv420p', '-threads', '2', '-g', '120', '-keyint_min', '120', '-sc_threshold', '0', '-flags', '+cgop', '-c:a', 'aac', '-b:a', '128k', '-ac', '2',
|
|
134
|
+
// Keep B-frame/AAC preroll positive with one fixed origin. Automatic MPEG-TS
|
|
135
|
+
// timestamp shifts differ after seeks and cause a repeated/missing boundary frame.
|
|
136
|
+
'-avoid_negative_ts', 'disabled', '-output_ts_offset', String(startMs / 1000 + 2), '-f', 'hls', '-hls_segment_options', 'mpegts_copyts=1', '-hls_time', '4', '-hls_list_size', '0', '-hls_flags', 'independent_segments+temp_file', '-hls_segment_filename', path.join(directory, 'raw_%06d.ts'), '-y', list);
|
|
137
|
+
else
|
|
138
|
+
args.push('-map', '0:a:0', '-vn', '-c:a', 'libmp3lame', '-b:a', '128k', '-ac', '2', '-ar', '44100', '-reservoir', '0', '-f', 'segment', '-segment_time', '4', '-segment_list', list, '-segment_list_type', 'csv', '-segment_format', 'mp3', '-segment_format_options', 'write_xing=0:id3v2_version=0', '-y', path.join(directory, 'raw_%06d.mp3'));
|
|
139
|
+
const controller = new AbortController();
|
|
140
|
+
const abort = () => controller.abort();
|
|
141
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
142
|
+
if (signal.aborted)
|
|
143
|
+
abort();
|
|
144
|
+
let finished = false;
|
|
145
|
+
let failure;
|
|
146
|
+
const encoding = runDecoder(decoderCommand('AXOM_FFMPEG', 'ffmpeg'), args, input, controller.signal, ms => onProgress(Math.min(probe.durationMs, startMs + ms)))
|
|
147
|
+
.then(() => { finished = true; }, error => { failure = error; finished = true; });
|
|
148
|
+
const published = new Set();
|
|
149
|
+
const publish = async () => {
|
|
150
|
+
cancelled(signal);
|
|
151
|
+
let text;
|
|
152
|
+
try {
|
|
153
|
+
const info = await fs.lstat(list);
|
|
154
|
+
if (info.size > 2 * 1024 * 1024 || !info.isFile() || info.isSymbolicLink())
|
|
155
|
+
throw new Error('Invalid media segment list');
|
|
156
|
+
text = await fs.readFile(list, 'utf8');
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
if (error.code === 'ENOENT')
|
|
160
|
+
return;
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
const entries = [];
|
|
164
|
+
if (video) {
|
|
165
|
+
let duration = null;
|
|
166
|
+
let offset = 0;
|
|
167
|
+
for (const line of text.split('\n')) {
|
|
168
|
+
if (line.startsWith('#EXTINF:'))
|
|
169
|
+
duration = Number(line.slice(8).split(',')[0]);
|
|
170
|
+
else if (/^raw_\d{6}\.ts$/.test(line) && duration != null && Number.isFinite(duration) && duration > 0) {
|
|
171
|
+
entries.push({ name: line, start: offset, end: offset + duration });
|
|
172
|
+
offset += duration;
|
|
173
|
+
duration = null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
else
|
|
178
|
+
for (const line of text.split('\n').slice(0, -1)) {
|
|
179
|
+
const m = /^(raw_\d{6}\.mp3),([\d.]+),([\d.]+)$/.exec(line);
|
|
180
|
+
if (m)
|
|
181
|
+
entries.push({ name: m[1], start: Number(m[2]), end: Number(m[3]) });
|
|
182
|
+
}
|
|
183
|
+
for (const entry of entries) {
|
|
184
|
+
if (published.has(entry.name))
|
|
185
|
+
continue;
|
|
186
|
+
cancelled(signal);
|
|
187
|
+
if (stamp !== await identity(input))
|
|
188
|
+
throw new Error('Media source changed; reopen its preview');
|
|
189
|
+
const raw = path.join(directory, entry.name);
|
|
190
|
+
const handle = await fs.open(raw, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
191
|
+
let bytes;
|
|
192
|
+
try {
|
|
193
|
+
const stat = await handle.stat();
|
|
194
|
+
if (!stat.isFile() || !stat.size || stat.size >= MAX_SEGMENT_BYTES)
|
|
195
|
+
throw new Error('Media object exceeds preview limits');
|
|
196
|
+
bytes = await handle.readFile();
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
await handle.close();
|
|
200
|
+
}
|
|
201
|
+
const timestamp90k = Math.round((startMs / 1000 + entry.start) * 90000);
|
|
202
|
+
if (!video)
|
|
203
|
+
bytes = stampPackedAudio(bytes, BigInt(timestamp90k));
|
|
204
|
+
const id = identifier();
|
|
205
|
+
const file = path.join(directory, `${id}.${extension}`);
|
|
206
|
+
const temp = `${file}.tmp`;
|
|
207
|
+
try {
|
|
208
|
+
await fs.writeFile(temp, bytes, { mode: 0o600, flag: 'wx' });
|
|
209
|
+
cancelled(signal);
|
|
210
|
+
await fs.rename(temp, file);
|
|
211
|
+
}
|
|
212
|
+
finally {
|
|
213
|
+
await fs.unlink(temp).catch(() => { });
|
|
214
|
+
}
|
|
215
|
+
const start = Math.min(probe.durationMs, startMs + Math.round(entry.start * 1000));
|
|
216
|
+
const end = Math.min(probe.durationMs, startMs + Math.round(entry.end * 1000));
|
|
217
|
+
if (end <= start) {
|
|
218
|
+
await fs.unlink(file);
|
|
219
|
+
published.add(entry.name);
|
|
220
|
+
await fs.unlink(raw);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
const object = { id, type: 'segment', extension, mimeType: video ? 'video/mp2t' : 'audio/mpeg', size: bytes.length,
|
|
224
|
+
etag: createHash('sha256').update(bytes).digest('hex'), startMs: start, endMs: end, sequence: generation.objects.length, timestamp90k, file };
|
|
225
|
+
await onObject(object);
|
|
226
|
+
generation.objects.push(object);
|
|
227
|
+
published.add(entry.name);
|
|
228
|
+
await fs.unlink(raw);
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
try {
|
|
232
|
+
while (!finished) {
|
|
233
|
+
await publish();
|
|
234
|
+
await new Promise(resolve => setTimeout(resolve, 25));
|
|
235
|
+
}
|
|
236
|
+
await encoding;
|
|
237
|
+
if (failure)
|
|
238
|
+
throw failure;
|
|
239
|
+
await publish();
|
|
240
|
+
cancelled(signal);
|
|
241
|
+
if (stamp !== await identity(input))
|
|
242
|
+
throw new Error('Media source changed; reopen its preview');
|
|
243
|
+
if (!generation.objects.length)
|
|
244
|
+
throw new Error('No playable media segments');
|
|
245
|
+
generation.complete = true;
|
|
246
|
+
generation.sourceComplete = true;
|
|
247
|
+
return generation;
|
|
248
|
+
}
|
|
249
|
+
finally {
|
|
250
|
+
controller.abort();
|
|
251
|
+
await encoding;
|
|
252
|
+
signal.removeEventListener('abort', abort);
|
|
253
|
+
for (const name of await fs.readdir(directory))
|
|
254
|
+
if (/^raw_\d{6}\.(ts|mp3)(\.tmp)?$/.test(name) || name === 'encoder.m3u8.tmp')
|
|
255
|
+
await fs.unlink(path.join(directory, name)).catch(() => { });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
export async function remuxRange(generation, destination, signal) {
|
|
259
|
+
cancelled(signal);
|
|
260
|
+
if (!generation.complete || !generation.objects.length)
|
|
261
|
+
throw new Error('Media generation is incomplete');
|
|
262
|
+
const video = generation.objects[0].extension === 'ts';
|
|
263
|
+
const extension = video ? 'mp4' : 'mp3';
|
|
264
|
+
if (path.extname(destination) !== `.${extension}`)
|
|
265
|
+
throw new Error('Invalid finite media output');
|
|
266
|
+
const directory = path.dirname(generation.objects[0].file);
|
|
267
|
+
// Only generated UUID objects in one private directory may enter the internal concat demuxer.
|
|
268
|
+
for (const object of generation.objects)
|
|
269
|
+
if (path.dirname(object.file) !== directory || !/^[a-f0-9]{32}\.(ts|mp3)$/.test(path.basename(object.file)))
|
|
270
|
+
throw new Error('Invalid remux object');
|
|
271
|
+
const list = path.join(directory, `${identifier()}.ffconcat`);
|
|
272
|
+
const temp = `${destination}.${identifier()}.tmp`;
|
|
273
|
+
const total = generation.objects.reduce((sum, item) => sum + item.size, 0);
|
|
274
|
+
if (total >= MAX_OBJECT_BYTES)
|
|
275
|
+
throw new Error('Media preview exceeds 512 MiB');
|
|
276
|
+
try {
|
|
277
|
+
await fs.writeFile(list, 'ffconcat version 1.0\n' + generation.objects.map(o => `file ${path.basename(o.file)}\n`).join(''), { mode: 0o600, flag: 'wx' });
|
|
278
|
+
const args = ['-nostdin', '-v', 'error', '-max_alloc', '134217728', '-protocol_whitelist', 'file', '-format_whitelist', 'concat,mpegts,mp3', '-safe', '1', '-f', 'concat', '-i', list,
|
|
279
|
+
'-map_metadata', '-1', '-c', 'copy', ...(video ? ['-movflags', '+faststart'] : []), '-fs', String(MAX_OBJECT_BYTES), '-f', extension, '-y', temp];
|
|
280
|
+
await runDecoder(decoderCommand('AXOM_FFMPEG', 'ffmpeg'), args, null, signal);
|
|
281
|
+
cancelled(signal);
|
|
282
|
+
const stat = await fs.lstat(temp);
|
|
283
|
+
if (!stat.isFile() || !stat.size || stat.size >= MAX_OBJECT_BYTES)
|
|
284
|
+
throw new Error('Media preview exceeds 512 MiB');
|
|
285
|
+
const etag = await hashFile(temp, signal);
|
|
286
|
+
await fs.rename(temp, destination);
|
|
287
|
+
return { id: identifier(), type: 'range', extension, mimeType: video ? 'video/mp4' : 'audio/mpeg', size: stat.size, etag,
|
|
288
|
+
startMs: generation.sourceOffsetMs, endMs: generation.objects.at(-1).endMs, sequence: 0, timestamp90k: 0, file: destination };
|
|
289
|
+
}
|
|
290
|
+
finally {
|
|
291
|
+
await fs.unlink(list).catch(() => { });
|
|
292
|
+
await fs.unlink(temp).catch(() => { });
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
async function hashFile(file, signal) {
|
|
296
|
+
const handle = await fs.open(file, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
297
|
+
const hash = createHash('sha256');
|
|
298
|
+
try {
|
|
299
|
+
for await (const chunk of handle.createReadStream({ autoClose: false, highWaterMark: 64 * 1024 })) {
|
|
300
|
+
cancelled(signal);
|
|
301
|
+
hash.update(chunk);
|
|
302
|
+
}
|
|
303
|
+
return hash.digest('hex');
|
|
304
|
+
}
|
|
305
|
+
finally {
|
|
306
|
+
await handle.close();
|
|
307
|
+
}
|
|
308
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
const unknown = () => ({ serviceMs: null, firstPlayableMs: null, confidence: 'low', producerSpeed: null });
|
|
6
|
+
const p75 = (values) => values.sort((a, b) => a - b)[Math.ceil(values.length * .75) - 1];
|
|
7
|
+
function valid(o) {
|
|
8
|
+
return Number.isFinite(o.sourceMs) && o.sourceMs > 0 && o.sourceMs <= 21600000 && Number.isFinite(o.elapsedMs) && o.elapsedMs > 0
|
|
9
|
+
&& Number.isSafeInteger(o.sourceBytes) && o.sourceBytes >= 0 && Number.isFinite(o.finalizationMs) && o.finalizationMs >= 0
|
|
10
|
+
&& (o.startupMs === undefined || (Number.isFinite(o.startupMs) && o.startupMs >= 0 && o.startupMs < o.elapsedMs));
|
|
11
|
+
}
|
|
12
|
+
/** Local bounded history. Queueing is intentionally absent from measured service time. */
|
|
13
|
+
export class MediaEstimator {
|
|
14
|
+
directory;
|
|
15
|
+
history;
|
|
16
|
+
queue = Promise.resolve();
|
|
17
|
+
constructor(directory, history) {
|
|
18
|
+
this.directory = directory;
|
|
19
|
+
this.history = history;
|
|
20
|
+
}
|
|
21
|
+
static async load(directory, ffmpegVersion, recipe) {
|
|
22
|
+
await fs.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
23
|
+
const stat = await fs.lstat(directory);
|
|
24
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || (process.getuid && stat.uid !== process.getuid()))
|
|
25
|
+
throw new Error('Unsafe estimate directory');
|
|
26
|
+
let history = { ffmpegVersion, recipe, buckets: [] };
|
|
27
|
+
try {
|
|
28
|
+
const handle = await fs.open(path.join(directory, 'estimates.json'), constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
29
|
+
try {
|
|
30
|
+
const s = await handle.stat();
|
|
31
|
+
if (s.isFile() && s.size <= 1024 * 1024) {
|
|
32
|
+
const data = JSON.parse(await handle.readFile('utf8'));
|
|
33
|
+
if (data.ffmpegVersion === ffmpegVersion && data.recipe === recipe && Array.isArray(data.buckets) && data.buckets.length <= 64
|
|
34
|
+
&& data.buckets.every(b => typeof b.key === 'string' && b.key.length < 1024 && Array.isArray(b.observations) && b.observations.length <= 32 && b.observations.every(valid)))
|
|
35
|
+
history = data;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
await handle.close();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch { /* Missing/incompatible history calibrates from the real encode. */ }
|
|
43
|
+
return new MediaEstimator(directory, history);
|
|
44
|
+
}
|
|
45
|
+
bucket(probe) {
|
|
46
|
+
const mode = probe.preparationPath ?? (probe.kind === 'audio' ? 'audio' : (probe.width ?? 0) > 1280 || (probe.height ?? 0) > 720 ? 'downscale' : 'video');
|
|
47
|
+
const pixels = (probe.width ?? 0) * (probe.height ?? 0);
|
|
48
|
+
const pixelClass = pixels === 0 ? 'audio' : pixels <= 1280 * 720 ? '720p' : pixels <= 1920 * 1080 ? '1080p' : pixels <= 3840 * 2160 ? '4k' : 'large';
|
|
49
|
+
return JSON.stringify([this.history.recipe, mode, pixelClass, probe.codec, probe.audioCodec ?? '', probe.pixelFormat ?? '', (probe.frameRate ?? 0) > 30 ? 'high-fps' : 'standard-fps']);
|
|
50
|
+
}
|
|
51
|
+
predict(probe) {
|
|
52
|
+
if (probe.ffmpegVersion !== this.history.ffmpegVersion || !probe.durationMs || probe.durationMs < 0)
|
|
53
|
+
return unknown();
|
|
54
|
+
const observations = this.history.buckets.find(b => b.key === this.bucket(probe))?.observations;
|
|
55
|
+
if (!observations?.length)
|
|
56
|
+
return unknown();
|
|
57
|
+
const encode = p75(observations.map(o => (o.elapsedMs - (o.startupMs ?? 0)) / o.sourceMs));
|
|
58
|
+
const read = p75(observations.filter(o => o.sourceBytes > 0).map(o => (o.elapsedMs - (o.startupMs ?? 0)) / o.sourceBytes));
|
|
59
|
+
const startup = p75(observations.map(o => o.startupMs ?? 0));
|
|
60
|
+
const finish = p75(observations.map(o => o.finalizationMs));
|
|
61
|
+
const serviceMs = Math.ceil(startup + Math.max(probe.durationMs * encode, Number.isFinite(read) ? probe.sourceBytes * read : 0) + finish);
|
|
62
|
+
const firstPlayableMs = Math.ceil(startup + Math.min(12000, probe.durationMs) * encode);
|
|
63
|
+
return { serviceMs, firstPlayableMs, confidence: observations.length >= 2 ? 'high' : 'low', producerSpeed: 1 / encode };
|
|
64
|
+
}
|
|
65
|
+
async observe(probe, observation) {
|
|
66
|
+
if (!valid(observation) || probe.ffmpegVersion !== this.history.ffmpegVersion)
|
|
67
|
+
throw new Error('Invalid media timing observation');
|
|
68
|
+
const task = this.queue.then(async () => {
|
|
69
|
+
const key = this.bucket(probe);
|
|
70
|
+
let bucket = this.history.buckets.find(b => b.key === key);
|
|
71
|
+
this.history.buckets = this.history.buckets.filter(b => b !== bucket);
|
|
72
|
+
bucket ??= { key, observations: [] };
|
|
73
|
+
bucket.observations.push({ ...observation });
|
|
74
|
+
bucket.observations = bucket.observations.slice(-32);
|
|
75
|
+
this.history.buckets.push(bucket);
|
|
76
|
+
this.history.buckets = this.history.buckets.slice(-64);
|
|
77
|
+
const temp = path.join(this.directory, `estimates-${randomUUID()}.tmp`);
|
|
78
|
+
try {
|
|
79
|
+
await fs.writeFile(temp, JSON.stringify(this.history), { mode: 0o600, flag: 'wx' });
|
|
80
|
+
await fs.rename(temp, path.join(this.directory, 'estimates.json'));
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
await fs.unlink(temp).catch(() => { });
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
this.queue = task.catch(() => { });
|
|
87
|
+
return task;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export function choosePreparation(estimate, cacheReady, hls, elapsedServiceMs) {
|
|
91
|
+
if (cacheReady || !hls)
|
|
92
|
+
return 'range';
|
|
93
|
+
if (elapsedServiceMs <= 10000) {
|
|
94
|
+
if (estimate.serviceMs === null)
|
|
95
|
+
return 'calibrating';
|
|
96
|
+
if (estimate.serviceMs <= 10000)
|
|
97
|
+
return 'range';
|
|
98
|
+
}
|
|
99
|
+
return estimate.confidence === 'high' && (estimate.producerSpeed ?? 0) >= 1.1 ? 'hls-growing' : 'hls-window';
|
|
100
|
+
}
|