dsh-vidi 0.7.6 → 0.7.8
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/lib/client.js +233 -169
- package/lib/index.mjs +14 -4
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -35187,78 +35187,27 @@ function upload(file, onProgress) {
|
|
|
35187
35187
|
xhr.send(file);
|
|
35188
35188
|
});
|
|
35189
35189
|
}
|
|
35190
|
-
/**
|
|
35191
|
-
function
|
|
35190
|
+
/** Cameras, screens and microphones on the computer Vidi runs on (null while loading). */
|
|
35191
|
+
function useDevices(enabled) {
|
|
35192
35192
|
const [devices, setDevices] = (0, react.useState)(null);
|
|
35193
|
-
const [video, setVideoRaw] = (0, react.useState)(() => store.get("device.video", ""));
|
|
35194
|
-
const [audio, setAudioRaw] = (0, react.useState)(() => store.get("device.audio", ""));
|
|
35195
|
-
const setVideo = (v) => {
|
|
35196
|
-
setVideoRaw(v);
|
|
35197
|
-
store.set("device.video", v);
|
|
35198
|
-
};
|
|
35199
|
-
const setAudio = (v) => {
|
|
35200
|
-
setAudioRaw(v);
|
|
35201
|
-
store.set("device.audio", v);
|
|
35202
|
-
};
|
|
35203
35193
|
(0, react.useEffect)(() => {
|
|
35194
|
+
if (!enabled || devices) return;
|
|
35204
35195
|
fetch("/live/api/devices").then((r) => r.json()).then((d) => setDevices(d.devices ?? [])).catch(() => setDevices([]));
|
|
35205
|
-
}, []);
|
|
35206
|
-
|
|
35207
|
-
|
|
35208
|
-
|
|
35209
|
-
|
|
35210
|
-
|
|
35211
|
-
|
|
35212
|
-
|
|
35213
|
-
|
|
35214
|
-
|
|
35215
|
-
|
|
35216
|
-
|
|
35217
|
-
|
|
35218
|
-
|
|
35219
|
-
}
|
|
35220
|
-
const title = v === "none" ? mics.find((d) => d.id === a)?.name ?? "" : videoOptions.find((o) => o.value === v)?.label ?? "";
|
|
35221
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
35222
|
-
className: "dshl-pushinfo dshl-devices",
|
|
35223
|
-
children: [
|
|
35224
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
35225
|
-
className: "title",
|
|
35226
|
-
children: [
|
|
35227
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.camera, {}),
|
|
35228
|
-
" ",
|
|
35229
|
-
t("dev.title"),
|
|
35230
|
-
" ",
|
|
35231
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("dev.hint") })
|
|
35232
|
-
]
|
|
35233
|
-
}),
|
|
35234
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dl", { children: [
|
|
35235
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("dev.video") }),
|
|
35236
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pick, {
|
|
35237
|
-
value: v,
|
|
35238
|
-
options: videoOptions,
|
|
35239
|
-
onChange: setVideo
|
|
35240
|
-
}) }),
|
|
35241
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("dev.audio") }),
|
|
35242
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pick, {
|
|
35243
|
-
value: a,
|
|
35244
|
-
options: mics.map((d) => ({
|
|
35245
|
-
value: d.id,
|
|
35246
|
-
label: d.name
|
|
35247
|
-
})),
|
|
35248
|
-
onChange: setAudio
|
|
35249
|
-
}) })
|
|
35250
|
-
] }),
|
|
35251
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
35252
|
-
className: "dshl-btn primary",
|
|
35253
|
-
onClick: () => onStart({
|
|
35254
|
-
video: v === "none" ? void 0 : v,
|
|
35255
|
-
audio: a,
|
|
35256
|
-
title
|
|
35257
|
-
}),
|
|
35258
|
-
children: t("dev.start")
|
|
35259
|
-
})
|
|
35260
|
-
]
|
|
35261
|
-
});
|
|
35196
|
+
}, [enabled, devices]);
|
|
35197
|
+
return devices;
|
|
35198
|
+
}
|
|
35199
|
+
/**
|
|
35200
|
+
* Ask for the camera / microphone from the page first when the page runs on the same computer: the system
|
|
35201
|
+
* then asks the user once for this app, and the capture that follows is allowed.
|
|
35202
|
+
*/
|
|
35203
|
+
async function askDeviceAccess(video) {
|
|
35204
|
+
if (!(location.protocol === "dsh-app:" || /^(localhost|127\.0\.0\.1|\[::1\])$/.test(location.hostname)) || !navigator.mediaDevices?.getUserMedia) return;
|
|
35205
|
+
try {
|
|
35206
|
+
(await navigator.mediaDevices.getUserMedia({
|
|
35207
|
+
audio: true,
|
|
35208
|
+
video
|
|
35209
|
+
})).getTracks().forEach((tr) => tr.stop());
|
|
35210
|
+
} catch {}
|
|
35262
35211
|
}
|
|
35263
35212
|
/** The push stream key: copy it, or change it (typed, or a new random one). */
|
|
35264
35213
|
function KeyRow({ value, t, onChanged }) {
|
|
@@ -35440,6 +35389,22 @@ function LiveView(props) {
|
|
|
35440
35389
|
const linesBox = (0, react.useRef)(null);
|
|
35441
35390
|
const fileInput = (0, react.useRef)(null);
|
|
35442
35391
|
const active = !!job && (job.status === "resolving" || job.status === "running" || job.status === "stopping");
|
|
35392
|
+
const [srcTab, setSrcTabRaw] = (0, react.useState)("link");
|
|
35393
|
+
const setSrcTab = (v) => {
|
|
35394
|
+
setSrcTabRaw(v);
|
|
35395
|
+
setError("");
|
|
35396
|
+
};
|
|
35397
|
+
const devices = useDevices(srcTab === "camera");
|
|
35398
|
+
const [camVideo, setCamVideoRaw] = (0, react.useState)(() => store.get("device.video", ""));
|
|
35399
|
+
const [camAudio, setCamAudioRaw] = (0, react.useState)(() => store.get("device.audio", ""));
|
|
35400
|
+
const setCamVideo = (v) => {
|
|
35401
|
+
setCamVideoRaw(v);
|
|
35402
|
+
store.set("device.video", v);
|
|
35403
|
+
};
|
|
35404
|
+
const setCamAudio = (v) => {
|
|
35405
|
+
setCamAudioRaw(v);
|
|
35406
|
+
store.set("device.audio", v);
|
|
35407
|
+
};
|
|
35443
35408
|
const ui = t("view.live") === "Video" ? "en" : "zh";
|
|
35444
35409
|
const langOptions = (0, react.useMemo)(() => Object.entries(langs).filter(([k]) => k !== "auto").map(([k, v]) => [k, langLabel(k, ui, v)]), [langs, ui]);
|
|
35445
35410
|
const langName = (0, react.useCallback)((k) => k === "src" ? t("sub.source") : langLabel(k, ui, langs[k]), [
|
|
@@ -35908,27 +35873,40 @@ function LiveView(props) {
|
|
|
35908
35873
|
const missing = deps.filter((d) => !d.ok && (d.name === "ffmpeg" || d.name === "yt-dlp" || (engine || "local") === "local" && d.name.startsWith("whisper")));
|
|
35909
35874
|
const needsModel = !hasTranslator && shownLines.some((l) => l !== "src");
|
|
35910
35875
|
const hasMain = !!job && !(job.status === "error" && !job.files.length);
|
|
35911
|
-
const
|
|
35876
|
+
const cams = (devices ?? []).filter((d) => d.kind !== "mic"), mics = (devices ?? []).filter((d) => d.kind === "mic");
|
|
35877
|
+
const camV = camVideo === "none" || cams.some((d) => d.id === camVideo) ? camVideo : cams.find((d) => d.kind === "camera")?.id ?? "none";
|
|
35878
|
+
const camA = mics.some((d) => d.id === camAudio) ? camAudio : mics[0]?.id ?? "";
|
|
35879
|
+
const videoOptions = [...cams.map((d) => ({
|
|
35880
|
+
value: d.id,
|
|
35881
|
+
label: d.kind === "screen" ? t("dev.screen").replace("{n}", String(Number(/\d+/.exec(d.name)?.[0] ?? 0) + 1)) : d.name,
|
|
35882
|
+
group: t(d.kind === "screen" ? "dev.screens" : "dev.cameras")
|
|
35883
|
+
})), {
|
|
35884
|
+
value: "none",
|
|
35885
|
+
label: t("dev.noVideo"),
|
|
35886
|
+
group: t("dev.other")
|
|
35887
|
+
}];
|
|
35888
|
+
const startDevice = async () => {
|
|
35889
|
+
if (!camA) return;
|
|
35890
|
+
const title = camV === "none" ? mics.find((d) => d.id === camA)?.name ?? "" : videoOptions.find((o) => o.value === camV)?.label ?? "";
|
|
35891
|
+
await askDeviceAccess(camV !== "none" && cams.find((d) => d.id === camV)?.kind === "camera");
|
|
35892
|
+
start(JSON.stringify({
|
|
35893
|
+
video: camV === "none" ? void 0 : camV,
|
|
35894
|
+
audio: camA,
|
|
35895
|
+
title
|
|
35896
|
+
}), "device");
|
|
35897
|
+
};
|
|
35898
|
+
const emptyBox = () => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
35912
35899
|
className: "dshl-drop",
|
|
35913
35900
|
children: [
|
|
35914
35901
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", { children: t("empty.title") }),
|
|
35915
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
35916
|
-
|
|
35917
|
-
|
|
35918
|
-
onStart: (c) => void start(JSON.stringify(c), "device")
|
|
35902
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
35903
|
+
className: "dshl-bar center",
|
|
35904
|
+
children: sourceBar
|
|
35919
35905
|
}),
|
|
35920
|
-
|
|
35906
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: srcTab === "camera" && devices && !mics.length ? t("src.cam.none") : t(srcTab === "file" ? "src.file.body" : srcTab === "camera" ? "src.cam.body" : srcTab === "push" ? "src.push.body" : "empty.body") }),
|
|
35907
|
+
srcTab === "push" && armed?.rtmp && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
35921
35908
|
className: "dshl-pushinfo",
|
|
35922
|
-
children:
|
|
35923
|
-
className: "title",
|
|
35924
|
-
children: [
|
|
35925
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.push, {}),
|
|
35926
|
-
" ",
|
|
35927
|
-
t("push.title"),
|
|
35928
|
-
" ",
|
|
35929
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("push.hint") })
|
|
35930
|
-
]
|
|
35931
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dl", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopyRow, {
|
|
35909
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dl", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopyRow, {
|
|
35932
35910
|
label: t("push.server"),
|
|
35933
35911
|
value: armed.rtmp,
|
|
35934
35912
|
copy: t("push.copy"),
|
|
@@ -35937,7 +35915,7 @@ function LiveView(props) {
|
|
|
35937
35915
|
value: armed.key,
|
|
35938
35916
|
t,
|
|
35939
35917
|
onChanged: () => void post("arm", options()).then((a) => setArmed(a.busy ? null : a)).catch(() => {})
|
|
35940
|
-
})] })
|
|
35918
|
+
})] })
|
|
35941
35919
|
})
|
|
35942
35920
|
]
|
|
35943
35921
|
});
|
|
@@ -36143,6 +36121,118 @@ function LiveView(props) {
|
|
|
36143
36121
|
children: t("lag.settings")
|
|
36144
36122
|
})]
|
|
36145
36123
|
}) : null;
|
|
36124
|
+
const sourceBar = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
36125
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pick, {
|
|
36126
|
+
className: "srcpick",
|
|
36127
|
+
value: srcTab,
|
|
36128
|
+
onChange: (v) => setSrcTab(v),
|
|
36129
|
+
title: t("src.tip"),
|
|
36130
|
+
options: [
|
|
36131
|
+
"link",
|
|
36132
|
+
"file",
|
|
36133
|
+
"camera",
|
|
36134
|
+
"push"
|
|
36135
|
+
].map((k) => ({
|
|
36136
|
+
value: k,
|
|
36137
|
+
label: t(`src.${k}`)
|
|
36138
|
+
}))
|
|
36139
|
+
}),
|
|
36140
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
36141
|
+
ref: fileInput,
|
|
36142
|
+
type: "file",
|
|
36143
|
+
accept: "video/*,audio/*,.mkv,.ts,.flv",
|
|
36144
|
+
hidden: true,
|
|
36145
|
+
onChange: (e) => {
|
|
36146
|
+
const f = e.target.files?.[0];
|
|
36147
|
+
if (f) onFile(f);
|
|
36148
|
+
e.target.value = "";
|
|
36149
|
+
}
|
|
36150
|
+
}),
|
|
36151
|
+
(srcTab === "link" || srcTab === "file") && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
36152
|
+
className: "dshl-field",
|
|
36153
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
36154
|
+
type: "text",
|
|
36155
|
+
value: input,
|
|
36156
|
+
placeholder: t(srcTab === "file" ? "src.file.placeholder" : "in.placeholder"),
|
|
36157
|
+
onChange: (e) => setInput(e.target.value),
|
|
36158
|
+
onKeyDown: (e) => {
|
|
36159
|
+
if (e.key === "Enter") start();
|
|
36160
|
+
}
|
|
36161
|
+
}), srcTab === "file" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
36162
|
+
className: "ghost",
|
|
36163
|
+
title: t("in.file.tip"),
|
|
36164
|
+
onClick: () => fileInput.current?.click(),
|
|
36165
|
+
children: uploading !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
36166
|
+
className: "pct",
|
|
36167
|
+
children: t("in.uploading").replace("{p}", String(uploading))
|
|
36168
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.folder, {})
|
|
36169
|
+
})]
|
|
36170
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
36171
|
+
className: "dshl-btn primary",
|
|
36172
|
+
onClick: () => start(),
|
|
36173
|
+
disabled: !classify(input),
|
|
36174
|
+
children: t("in.open")
|
|
36175
|
+
})] }),
|
|
36176
|
+
srcTab === "camera" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
36177
|
+
className: "dshl-devbar",
|
|
36178
|
+
children: devices === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
36179
|
+
className: "dshl-muted",
|
|
36180
|
+
children: t("src.cam.loading")
|
|
36181
|
+
}) : mics.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pick, {
|
|
36182
|
+
value: camV,
|
|
36183
|
+
options: videoOptions,
|
|
36184
|
+
onChange: setCamVideo,
|
|
36185
|
+
title: t("dev.video")
|
|
36186
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pick, {
|
|
36187
|
+
value: camA,
|
|
36188
|
+
options: mics.map((d) => ({
|
|
36189
|
+
value: d.id,
|
|
36190
|
+
label: d.name
|
|
36191
|
+
})),
|
|
36192
|
+
onChange: setCamAudio,
|
|
36193
|
+
title: t("dev.audio")
|
|
36194
|
+
})] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
36195
|
+
className: "dshl-muted",
|
|
36196
|
+
children: t("src.cam.none")
|
|
36197
|
+
})
|
|
36198
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
36199
|
+
className: "dshl-btn primary",
|
|
36200
|
+
onClick: () => void startDevice(),
|
|
36201
|
+
disabled: !camA,
|
|
36202
|
+
children: t("dev.start")
|
|
36203
|
+
})] }),
|
|
36204
|
+
srcTab === "push" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
36205
|
+
className: "dshl-field static",
|
|
36206
|
+
title: t("src.push.body"),
|
|
36207
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(armed?.rtmp ? "src.push.bar" : active ? "src.push.busy" : "src.push.wait") })
|
|
36208
|
+
}),
|
|
36209
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Settings, {
|
|
36210
|
+
engine: engine || "local",
|
|
36211
|
+
onEngine: setEngine,
|
|
36212
|
+
mode,
|
|
36213
|
+
onMode: changeMode,
|
|
36214
|
+
spoken,
|
|
36215
|
+
onSpoken: setSpoken,
|
|
36216
|
+
langOptions,
|
|
36217
|
+
login,
|
|
36218
|
+
onLogin: chooseLogin,
|
|
36219
|
+
browsers,
|
|
36220
|
+
providers,
|
|
36221
|
+
shared: keysShared,
|
|
36222
|
+
name: engineName,
|
|
36223
|
+
onKey: (id, k) => {
|
|
36224
|
+
setProviders((ps) => ps.map((p) => p.id === id ? {
|
|
36225
|
+
...p,
|
|
36226
|
+
key: k,
|
|
36227
|
+
ready: k.set
|
|
36228
|
+
} : p));
|
|
36229
|
+
if (k.set) setError("");
|
|
36230
|
+
},
|
|
36231
|
+
open: settingsOpen,
|
|
36232
|
+
onOpen: setSettingsOpen,
|
|
36233
|
+
t
|
|
36234
|
+
})
|
|
36235
|
+
] });
|
|
36146
36236
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
36147
36237
|
className: `dshl ${theater && !activeBench ? "theater" : ""} ${pageMode ? "page" : ""} ${activeBench ? "bench" : ""} ${pageMode && !sideOpen && !activeBench ? "noside" : ""}`,
|
|
36148
36238
|
style: pageMode ? { ["--side-w"]: `${sideW}px` } : void 0,
|
|
@@ -36170,7 +36260,7 @@ function LiveView(props) {
|
|
|
36170
36260
|
if (f && !active) onFile(f);
|
|
36171
36261
|
},
|
|
36172
36262
|
children: [
|
|
36173
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("header", {
|
|
36263
|
+
(hasMain || onClose) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("header", {
|
|
36174
36264
|
className: "dshl-top",
|
|
36175
36265
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
36176
36266
|
className: "dshl-bar",
|
|
@@ -36181,74 +36271,8 @@ function LiveView(props) {
|
|
|
36181
36271
|
title: t("page.back"),
|
|
36182
36272
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.arrow, {})
|
|
36183
36273
|
}),
|
|
36184
|
-
|
|
36185
|
-
|
|
36186
|
-
children: [
|
|
36187
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.link, {}),
|
|
36188
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
36189
|
-
type: "text",
|
|
36190
|
-
value: input,
|
|
36191
|
-
placeholder: t("in.placeholder"),
|
|
36192
|
-
onChange: (e) => setInput(e.target.value),
|
|
36193
|
-
onKeyDown: (e) => {
|
|
36194
|
-
if (e.key === "Enter") start();
|
|
36195
|
-
}
|
|
36196
|
-
}),
|
|
36197
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
36198
|
-
className: "ghost",
|
|
36199
|
-
title: t("in.file.tip"),
|
|
36200
|
-
onClick: () => fileInput.current?.click(),
|
|
36201
|
-
children: uploading !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
36202
|
-
className: "pct",
|
|
36203
|
-
children: t("in.uploading").replace("{p}", String(uploading))
|
|
36204
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.folder, {})
|
|
36205
|
-
}),
|
|
36206
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
36207
|
-
ref: fileInput,
|
|
36208
|
-
type: "file",
|
|
36209
|
-
accept: "video/*,audio/*,.mkv,.ts,.flv",
|
|
36210
|
-
hidden: true,
|
|
36211
|
-
onChange: (e) => {
|
|
36212
|
-
const f = e.target.files?.[0];
|
|
36213
|
-
if (f) onFile(f);
|
|
36214
|
-
e.target.value = "";
|
|
36215
|
-
}
|
|
36216
|
-
})
|
|
36217
|
-
]
|
|
36218
|
-
}),
|
|
36219
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
36220
|
-
className: "dshl-btn primary",
|
|
36221
|
-
onClick: () => start(),
|
|
36222
|
-
disabled: !classify(input),
|
|
36223
|
-
children: t("in.open")
|
|
36224
|
-
}),
|
|
36225
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Settings, {
|
|
36226
|
-
engine: engine || "local",
|
|
36227
|
-
onEngine: setEngine,
|
|
36228
|
-
mode,
|
|
36229
|
-
onMode: changeMode,
|
|
36230
|
-
spoken,
|
|
36231
|
-
onSpoken: setSpoken,
|
|
36232
|
-
langOptions,
|
|
36233
|
-
login,
|
|
36234
|
-
onLogin: chooseLogin,
|
|
36235
|
-
browsers,
|
|
36236
|
-
providers,
|
|
36237
|
-
shared: keysShared,
|
|
36238
|
-
name: engineName,
|
|
36239
|
-
onKey: (id, k) => {
|
|
36240
|
-
setProviders((ps) => ps.map((p) => p.id === id ? {
|
|
36241
|
-
...p,
|
|
36242
|
-
key: k,
|
|
36243
|
-
ready: k.set
|
|
36244
|
-
} : p));
|
|
36245
|
-
if (k.set) setError("");
|
|
36246
|
-
},
|
|
36247
|
-
open: settingsOpen,
|
|
36248
|
-
onOpen: setSettingsOpen,
|
|
36249
|
-
t
|
|
36250
|
-
}),
|
|
36251
|
-
pageMode && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
36274
|
+
hasMain && sourceBar,
|
|
36275
|
+
pageMode && hasMain && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
36252
36276
|
className: `dshl-ctl icon ${sideOpen ? "on" : ""}`,
|
|
36253
36277
|
title: t(sideOpen ? "side.hide" : "side.show"),
|
|
36254
36278
|
"aria-pressed": sideOpen,
|
|
@@ -36272,21 +36296,21 @@ function LiveView(props) {
|
|
|
36272
36296
|
}),
|
|
36273
36297
|
(error || job?.status === "error") && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
36274
36298
|
className: "dshl-note warn",
|
|
36275
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: error || job?.message }), job && !active && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
36299
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: error || (job?.message === "device-blocked" ? t("dev.blocked") : job?.message) }), job && !active && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
36276
36300
|
className: "dshl-link",
|
|
36277
36301
|
onClick: () => start(job.url, job.source),
|
|
36278
36302
|
children: t("st.retry")
|
|
36279
36303
|
})]
|
|
36280
36304
|
}),
|
|
36281
|
-
!hasMain
|
|
36305
|
+
!hasMain ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
|
|
36282
36306
|
className: "dshl-empty",
|
|
36283
|
-
children: emptyBox
|
|
36307
|
+
children: emptyBox()
|
|
36284
36308
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("main", {
|
|
36285
36309
|
className: "dshl-main",
|
|
36286
36310
|
ref: mainRef,
|
|
36287
36311
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
36288
36312
|
className: "dshl-col",
|
|
36289
|
-
children: !hasMain || !job ? emptyBox : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
36313
|
+
children: !hasMain || !job ? emptyBox() : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
36290
36314
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Player, {
|
|
36291
36315
|
ref: player,
|
|
36292
36316
|
src: job.status === "resolving" || job.status === "error" ? null : `/live/media/${encodeURIComponent(job.id)}/${job.hls ?? "hls"}/index.m3u8`,
|
|
@@ -37141,7 +37165,7 @@ const en = {
|
|
|
37141
37165
|
"in.uploading": "Uploading {p}%",
|
|
37142
37166
|
"in.drop": "Drop to start",
|
|
37143
37167
|
"empty.title": "Subtitles for any video",
|
|
37144
|
-
"empty.body": "YouTube, Bilibili and
|
|
37168
|
+
"empty.body": "YouTube, Bilibili and other sites, videos and live streams.",
|
|
37145
37169
|
"st.resolving": "Opening…",
|
|
37146
37170
|
"st.login": "Using your {b} login… if macOS asks, choose “Always Allow”.",
|
|
37147
37171
|
"st.live": "Live",
|
|
@@ -37154,8 +37178,25 @@ const en = {
|
|
|
37154
37178
|
"note.noModel": "No model set up, so subtitles stay in the original language.",
|
|
37155
37179
|
"note.install": "Install first:",
|
|
37156
37180
|
"push.title": "Stream here from OBS",
|
|
37157
|
-
"
|
|
37158
|
-
"
|
|
37181
|
+
"src.tip": "Where the video comes from",
|
|
37182
|
+
"src.link": "Link",
|
|
37183
|
+
"src.file": "Local file",
|
|
37184
|
+
"src.camera": "Camera",
|
|
37185
|
+
"src.push": "OBS stream",
|
|
37186
|
+
"src.file.placeholder": "Path of a video or audio file",
|
|
37187
|
+
"src.file.title": "Open a video on this computer",
|
|
37188
|
+
"src.file.body": "Choose a file with the folder button, or drop it anywhere on this page.",
|
|
37189
|
+
"src.file.pick": "Choose a file",
|
|
37190
|
+
"src.cam.title": "Talk to the camera",
|
|
37191
|
+
"src.cam.body": "Pick the picture and the microphone, press Start: subtitles appear as you speak.",
|
|
37192
|
+
"src.cam.none": "No camera or microphone found on this computer.",
|
|
37193
|
+
"src.cam.loading": "Looking for cameras…",
|
|
37194
|
+
"src.push.title": "Stream from OBS",
|
|
37195
|
+
"src.push.body": "In OBS choose the Custom service and fill in the server and key below; subtitles start as soon as the picture arrives.",
|
|
37196
|
+
"src.push.bar": "Waiting for OBS: server and key below",
|
|
37197
|
+
"src.push.wait": "Getting the address ready…",
|
|
37198
|
+
"src.push.busy": "Close the current video to receive a stream",
|
|
37199
|
+
"dev.blocked": "No picture or sound from the device. Allow DeepSeek Harness under System Settings → Privacy & Security → Camera and Microphone (Screen Recording for the screen), then start again.",
|
|
37159
37200
|
"dev.video": "Picture",
|
|
37160
37201
|
"dev.audio": "Microphone",
|
|
37161
37202
|
"dev.cameras": "Cameras",
|
|
@@ -37542,7 +37583,7 @@ const zh = {
|
|
|
37542
37583
|
"in.uploading": "上传中 {p}%",
|
|
37543
37584
|
"in.drop": "松手开始",
|
|
37544
37585
|
"empty.title": "给任何视频配字幕",
|
|
37545
|
-
"empty.body": "支持 YouTube、B
|
|
37586
|
+
"empty.body": "支持 YouTube、B 站等网站的视频和直播",
|
|
37546
37587
|
"st.resolving": "正在打开…",
|
|
37547
37588
|
"st.login": "正在用 {b} 的登录状态…如果系统弹窗询问,点「始终允许」",
|
|
37548
37589
|
"st.live": "直播中",
|
|
@@ -37555,8 +37596,25 @@ const zh = {
|
|
|
37555
37596
|
"note.noModel": "还没配置模型,字幕只显示原文",
|
|
37556
37597
|
"note.install": "需要先安装:",
|
|
37557
37598
|
"push.title": "用 OBS 推流到这里",
|
|
37558
|
-
"
|
|
37559
|
-
"
|
|
37599
|
+
"src.tip": "视频从哪里来",
|
|
37600
|
+
"src.link": "链接",
|
|
37601
|
+
"src.file": "本地文件",
|
|
37602
|
+
"src.camera": "摄像头",
|
|
37603
|
+
"src.push": "OBS 推流",
|
|
37604
|
+
"src.file.placeholder": "视频或音频文件的路径",
|
|
37605
|
+
"src.file.title": "打开这台电脑上的视频",
|
|
37606
|
+
"src.file.body": "点文件夹按钮选择,或者直接把文件拖进来。",
|
|
37607
|
+
"src.file.pick": "选择文件",
|
|
37608
|
+
"src.cam.title": "对着摄像头说话",
|
|
37609
|
+
"src.cam.body": "选好画面和麦克风,点开始,字幕边说边出。",
|
|
37610
|
+
"src.cam.none": "这台电脑上没有找到摄像头或麦克风。",
|
|
37611
|
+
"src.cam.loading": "正在找摄像头…",
|
|
37612
|
+
"src.push.title": "用 OBS 推流过来",
|
|
37613
|
+
"src.push.body": "OBS 里服务选「自定义」,填下面的服务器和密钥,画面一到字幕就开始。",
|
|
37614
|
+
"src.push.bar": "等 OBS 推流过来,服务器和密钥在下面",
|
|
37615
|
+
"src.push.wait": "正在准备推流地址…",
|
|
37616
|
+
"src.push.busy": "关掉当前视频后可以接收推流",
|
|
37617
|
+
"dev.blocked": "设备没有传来画面和声音。到 系统设置 → 隐私与安全性 → 摄像头、麦克风(用屏幕时是 屏幕录制)里打开 DeepSeek Harness,再点开始。",
|
|
37560
37618
|
"dev.video": "画面",
|
|
37561
37619
|
"dev.audio": "麦克风",
|
|
37562
37620
|
"dev.cameras": "摄像头",
|
|
@@ -37949,6 +38007,10 @@ const CSS = `
|
|
|
37949
38007
|
|
|
37950
38008
|
/* ── Empty state ── */
|
|
37951
38009
|
.dshl-empty { padding: 28px 20px; }
|
|
38010
|
+
.dshl.page .dshl-empty { flex: 1; display: flex; align-items: center; justify-content: center; min-height: 0; overflow: auto; }
|
|
38011
|
+
.dshl.page .dshl-empty > .dshl-drop { border: 0; padding-top: 0; }
|
|
38012
|
+
.dshl-drop > .dshl-bar.center { display: flex; align-items: center; gap: 8px; width: 100%; max-width: 680px; margin: 22px 0 12px; text-align: left; }
|
|
38013
|
+
.dshl-drop > .dshl-bar.center + p { font-size: 13px; color: var(--faint); }
|
|
37952
38014
|
.dshl-drop { display: flex; flex-direction: column; align-items: center; justify-content: center; width: 100%; max-width: 760px; margin: 0 auto; padding: 56px 24px 48px;
|
|
37953
38015
|
border: 1.5px dashed var(--line); border-radius: 16px; text-align: center; }
|
|
37954
38016
|
.dshl-col > .dshl-drop { max-width: none; min-height: 320px; aspect-ratio: 16 / 9; padding: 32px 24px; border-radius: 12px; }
|
|
@@ -37967,10 +38029,12 @@ const CSS = `
|
|
|
37967
38029
|
.dshl-pushinfo dd .err { flex-basis: 100%; color: #d1434b; font-size: 12px; }
|
|
37968
38030
|
.dshl-pushinfo .val { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ink); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px; user-select: all; }
|
|
37969
38031
|
.dshl-pushinfo .dshl-link { flex: none; font-size: 12.5px; }
|
|
37970
|
-
.dshl-
|
|
37971
|
-
.dshl-
|
|
37972
|
-
.dshl-
|
|
37973
|
-
.dshl-
|
|
38032
|
+
.dshl-bar .dshl-select.srcpick { flex: none; width: auto; height: 40px; padding: 0 10px 0 14px; border-radius: 10px; font-weight: 550; }
|
|
38033
|
+
.dshl-devbar { display: flex; align-items: center; gap: 8px; flex: 1 1 260px; min-width: 0; }
|
|
38034
|
+
.dshl-devbar .dshl-select { flex: 1 1 0; min-width: 0; height: 40px; border-radius: 10px; }
|
|
38035
|
+
.dshl-field.static { color: var(--muted); cursor: default; }
|
|
38036
|
+
.dshl-field.static .mono { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ink); font: 13px ui-monospace, SFMono-Regular, Menlo, monospace; user-select: all; }
|
|
38037
|
+
.dshl-drop > .dshl-btn { margin-top: 20px; }
|
|
37974
38038
|
|
|
37975
38039
|
/* ── Layout ── */
|
|
37976
38040
|
.dshl-main { display: grid; grid-template-columns: minmax(0, 1fr) clamp(300px, 28vw, 400px); align-items: start; gap: 18px; padding: 18px 20px 28px; }
|
package/lib/index.mjs
CHANGED
|
@@ -2426,6 +2426,12 @@ var LiveJob = class LiveJob extends EventEmitter {
|
|
|
2426
2426
|
"pipe"
|
|
2427
2427
|
] });
|
|
2428
2428
|
this.ffmpeg = ff;
|
|
2429
|
+
if (this.opts.source === "device") setTimeout(() => {
|
|
2430
|
+
if (this.ffmpeg !== ff || this.segments().length) return;
|
|
2431
|
+
this.ffmpeg = void 0;
|
|
2432
|
+
ff.kill("SIGKILL");
|
|
2433
|
+
this.setStatus("error", "device-blocked");
|
|
2434
|
+
}, 12e3);
|
|
2429
2435
|
if (this.opts.source === "push") {
|
|
2430
2436
|
const wait = setInterval(() => {
|
|
2431
2437
|
if (this.ffmpeg !== ff) {
|
|
@@ -6383,10 +6389,14 @@ function apply(ctx, config) {
|
|
|
6383
6389
|
}
|
|
6384
6390
|
return send(res, 200, job.summary());
|
|
6385
6391
|
}
|
|
6386
|
-
if (route === "devices")
|
|
6387
|
-
devices
|
|
6388
|
-
|
|
6389
|
-
|
|
6392
|
+
if (route === "devices") {
|
|
6393
|
+
const devices = await listDevices();
|
|
6394
|
+
log(`devices: ${devices.filter((d) => d.kind !== "mic").length} picture, ${devices.filter((d) => d.kind === "mic").length} microphone`);
|
|
6395
|
+
return send(res, 200, {
|
|
6396
|
+
devices,
|
|
6397
|
+
platform: process.platform
|
|
6398
|
+
});
|
|
6399
|
+
}
|
|
6390
6400
|
if (route === "pushkey" && req.method === "POST") {
|
|
6391
6401
|
if ([...jobs.values()].some((j) => j.opts.source === "push" && j.status === "running" && !j.message)) return send(res, 409, { error: "a stream is coming in; change the key after it ends" });
|
|
6392
6402
|
const b = await readJson(req);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vidi",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.8",
|
|
4
4
|
"description": "A video tool for DeepSeek Harness: watch live streams, videos and podcasts with real-time subtitles in any language, then clip and download",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "PolyForm-Noncommercial-1.0.0",
|