dsh-vidi 0.7.6 → 0.7.7

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.
Files changed (3) hide show
  1. package/lib/client.js +195 -132
  2. package/lib/index.mjs +14 -4
  3. 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
- /** Subtitle a camera (or the screen) and a microphone on this computer directly: pick both, press start. */
35191
- function DeviceCard({ t, onStart }) {
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
- if (!devices) return null;
35207
- const cams = devices.filter((d) => d.kind !== "mic"), mics = devices.filter((d) => d.kind === "mic");
35208
- if (!mics.length) return null;
35209
- const v = video === "none" || cams.some((d) => d.id === video) ? video : cams.find((d) => d.kind === "camera")?.id ?? "none";
35210
- const a = mics.some((d) => d.id === audio) ? audio : mics[0].id;
35211
- const videoOptions = [...cams.map((d) => ({
35212
- value: d.id,
35213
- label: d.kind === "screen" ? d.name.replace(/^Screen (\d+)$/, (_, n) => t("dev.screen").replace("{n}", String(Number(n) + 1))) : d.name,
35214
- group: t(d.kind === "screen" ? "dev.screens" : "dev.cameras")
35215
- })), {
35216
- value: "none",
35217
- label: t("dev.noVideo"),
35218
- group: t("dev.other")
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,23 @@ 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)(() => store.get("source", "link"));
35393
+ const setSrcTab = (v) => {
35394
+ setSrcTabRaw(v);
35395
+ store.set("source", v);
35396
+ setError("");
35397
+ };
35398
+ const devices = useDevices(srcTab === "camera");
35399
+ const [camVideo, setCamVideoRaw] = (0, react.useState)(() => store.get("device.video", ""));
35400
+ const [camAudio, setCamAudioRaw] = (0, react.useState)(() => store.get("device.audio", ""));
35401
+ const setCamVideo = (v) => {
35402
+ setCamVideoRaw(v);
35403
+ store.set("device.video", v);
35404
+ };
35405
+ const setCamAudio = (v) => {
35406
+ setCamAudioRaw(v);
35407
+ store.set("device.audio", v);
35408
+ };
35443
35409
  const ui = t("view.live") === "Video" ? "en" : "zh";
35444
35410
  const langOptions = (0, react.useMemo)(() => Object.entries(langs).filter(([k]) => k !== "auto").map(([k, v]) => [k, langLabel(k, ui, v)]), [langs, ui]);
35445
35411
  const langName = (0, react.useCallback)((k) => k === "src" ? t("sub.source") : langLabel(k, ui, langs[k]), [
@@ -35908,27 +35874,45 @@ function LiveView(props) {
35908
35874
  const missing = deps.filter((d) => !d.ok && (d.name === "ffmpeg" || d.name === "yt-dlp" || (engine || "local") === "local" && d.name.startsWith("whisper")));
35909
35875
  const needsModel = !hasTranslator && shownLines.some((l) => l !== "src");
35910
35876
  const hasMain = !!job && !(job.status === "error" && !job.files.length);
35877
+ const cams = (devices ?? []).filter((d) => d.kind !== "mic"), mics = (devices ?? []).filter((d) => d.kind === "mic");
35878
+ const camV = camVideo === "none" || cams.some((d) => d.id === camVideo) ? camVideo : cams.find((d) => d.kind === "camera")?.id ?? "none";
35879
+ const camA = mics.some((d) => d.id === camAudio) ? camAudio : mics[0]?.id ?? "";
35880
+ const videoOptions = [...cams.map((d) => ({
35881
+ value: d.id,
35882
+ label: d.kind === "screen" ? t("dev.screen").replace("{n}", String(Number(/\d+/.exec(d.name)?.[0] ?? 0) + 1)) : d.name,
35883
+ group: t(d.kind === "screen" ? "dev.screens" : "dev.cameras")
35884
+ })), {
35885
+ value: "none",
35886
+ label: t("dev.noVideo"),
35887
+ group: t("dev.other")
35888
+ }];
35889
+ const startDevice = async () => {
35890
+ if (!camA) return;
35891
+ const title = camV === "none" ? mics.find((d) => d.id === camA)?.name ?? "" : videoOptions.find((o) => o.value === camV)?.label ?? "";
35892
+ await askDeviceAccess(camV !== "none" && cams.find((d) => d.id === camV)?.kind === "camera");
35893
+ start(JSON.stringify({
35894
+ video: camV === "none" ? void 0 : camV,
35895
+ audio: camA,
35896
+ title
35897
+ }), "device");
35898
+ };
35911
35899
  const emptyBox = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
35912
35900
  className: "dshl-drop",
35913
35901
  children: [
35914
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", { children: t("empty.title") }),
35915
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("empty.body") }),
35916
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeviceCard, {
35917
- t,
35918
- onStart: (c) => void start(JSON.stringify(c), "device")
35902
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", { children: t(srcTab === "file" ? "src.file.title" : srcTab === "camera" ? "src.cam.title" : srcTab === "push" ? "src.push.title" : "empty.title") }),
35903
+ /* @__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") }),
35904
+ srcTab === "file" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
35905
+ className: "dshl-btn",
35906
+ onClick: () => fileInput.current?.click(),
35907
+ children: [
35908
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.folder, {}),
35909
+ " ",
35910
+ t("src.file.pick")
35911
+ ]
35919
35912
  }),
35920
- armed?.rtmp && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
35913
+ srcTab === "push" && armed?.rtmp && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
35921
35914
  className: "dshl-pushinfo",
35922
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
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, {
35915
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dl", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopyRow, {
35932
35916
  label: t("push.server"),
35933
35917
  value: armed.rtmp,
35934
35918
  copy: t("push.copy"),
@@ -35937,7 +35921,7 @@ function LiveView(props) {
35937
35921
  value: armed.key,
35938
35922
  t,
35939
35923
  onChanged: () => void post("arm", options()).then((a) => setArmed(a.busy ? null : a)).catch(() => {})
35940
- })] })]
35924
+ })] })
35941
35925
  })
35942
35926
  ]
35943
35927
  });
@@ -36181,46 +36165,89 @@ function LiveView(props) {
36181
36165
  title: t("page.back"),
36182
36166
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.arrow, {})
36183
36167
  }),
36184
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
36185
- className: "dshl-field",
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
- ]
36168
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pick, {
36169
+ className: "srcpick",
36170
+ value: srcTab,
36171
+ onChange: (v) => setSrcTab(v),
36172
+ title: t("src.tip"),
36173
+ options: [
36174
+ "link",
36175
+ "file",
36176
+ "camera",
36177
+ "push"
36178
+ ].map((k) => ({
36179
+ value: k,
36180
+ label: t(`src.${k}`)
36181
+ }))
36218
36182
  }),
36219
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
36183
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
36184
+ ref: fileInput,
36185
+ type: "file",
36186
+ accept: "video/*,audio/*,.mkv,.ts,.flv",
36187
+ hidden: true,
36188
+ onChange: (e) => {
36189
+ const f = e.target.files?.[0];
36190
+ if (f) onFile(f);
36191
+ e.target.value = "";
36192
+ }
36193
+ }),
36194
+ (srcTab === "link" || srcTab === "file") && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
36195
+ className: "dshl-field",
36196
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
36197
+ type: "text",
36198
+ value: input,
36199
+ placeholder: t(srcTab === "file" ? "src.file.placeholder" : "in.placeholder"),
36200
+ onChange: (e) => setInput(e.target.value),
36201
+ onKeyDown: (e) => {
36202
+ if (e.key === "Enter") start();
36203
+ }
36204
+ }), srcTab === "file" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
36205
+ className: "ghost",
36206
+ title: t("in.file.tip"),
36207
+ onClick: () => fileInput.current?.click(),
36208
+ children: uploading !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
36209
+ className: "pct",
36210
+ children: t("in.uploading").replace("{p}", String(uploading))
36211
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.folder, {})
36212
+ })]
36213
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
36220
36214
  className: "dshl-btn primary",
36221
36215
  onClick: () => start(),
36222
36216
  disabled: !classify(input),
36223
36217
  children: t("in.open")
36218
+ })] }),
36219
+ srcTab === "camera" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
36220
+ className: "dshl-devbar",
36221
+ children: devices === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
36222
+ className: "dshl-muted",
36223
+ children: t("src.cam.loading")
36224
+ }) : mics.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pick, {
36225
+ value: camV,
36226
+ options: videoOptions,
36227
+ onChange: setCamVideo,
36228
+ title: t("dev.video")
36229
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pick, {
36230
+ value: camA,
36231
+ options: mics.map((d) => ({
36232
+ value: d.id,
36233
+ label: d.name
36234
+ })),
36235
+ onChange: setCamAudio,
36236
+ title: t("dev.audio")
36237
+ })] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
36238
+ className: "dshl-muted",
36239
+ children: t("src.cam.none")
36240
+ })
36241
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
36242
+ className: "dshl-btn primary",
36243
+ onClick: () => void startDevice(),
36244
+ disabled: !camA,
36245
+ children: t("dev.start")
36246
+ })] }),
36247
+ srcTab === "push" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
36248
+ className: "dshl-field static",
36249
+ title: t("src.push.body"),
36250
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(armed?.rtmp ? "src.push.bar" : active ? "src.push.busy" : "src.push.wait") })
36224
36251
  }),
36225
36252
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Settings, {
36226
36253
  engine: engine || "local",
@@ -36272,7 +36299,7 @@ function LiveView(props) {
36272
36299
  }),
36273
36300
  (error || job?.status === "error") && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
36274
36301
  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", {
36302
+ 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
36303
  className: "dshl-link",
36277
36304
  onClick: () => start(job.url, job.source),
36278
36305
  children: t("st.retry")
@@ -37141,7 +37168,7 @@ const en = {
37141
37168
  "in.uploading": "Uploading {p}%",
37142
37169
  "in.drop": "Drop to start",
37143
37170
  "empty.title": "Subtitles for any video",
37144
- "empty.body": "YouTube, Bilibili and more — or drop in a video or audio file.",
37171
+ "empty.body": "Paste a link from YouTube, Bilibili and more — videos and live streams.",
37145
37172
  "st.resolving": "Opening…",
37146
37173
  "st.login": "Using your {b} login… if macOS asks, choose “Always Allow”.",
37147
37174
  "st.live": "Live",
@@ -37154,8 +37181,25 @@ const en = {
37154
37181
  "note.noModel": "No model set up, so subtitles stay in the original language.",
37155
37182
  "note.install": "Install first:",
37156
37183
  "push.title": "Stream here from OBS",
37157
- "dev.title": "Use a camera on this computer",
37158
- "dev.hint": "Subtitles as you speak",
37184
+ "src.tip": "Where the video comes from",
37185
+ "src.link": "Link",
37186
+ "src.file": "Local file",
37187
+ "src.camera": "Camera",
37188
+ "src.push": "OBS stream",
37189
+ "src.file.placeholder": "Path of a video or audio file",
37190
+ "src.file.title": "Open a video on this computer",
37191
+ "src.file.body": "Drop a video or audio file here.",
37192
+ "src.file.pick": "Choose a file",
37193
+ "src.cam.title": "Talk to the camera",
37194
+ "src.cam.body": "Pick the picture and the microphone above and press Start: subtitles appear as you speak.",
37195
+ "src.cam.none": "No camera or microphone found on this computer.",
37196
+ "src.cam.loading": "Looking for cameras…",
37197
+ "src.push.title": "Stream from OBS",
37198
+ "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.",
37199
+ "src.push.bar": "Waiting for OBS: server and key below",
37200
+ "src.push.wait": "Getting the address ready…",
37201
+ "src.push.busy": "Close the current video to receive a stream",
37202
+ "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
37203
  "dev.video": "Picture",
37160
37204
  "dev.audio": "Microphone",
37161
37205
  "dev.cameras": "Cameras",
@@ -37542,7 +37586,7 @@ const zh = {
37542
37586
  "in.uploading": "上传中 {p}%",
37543
37587
  "in.drop": "松手开始",
37544
37588
  "empty.title": "给任何视频配字幕",
37545
- "empty.body": "支持 YouTube、B 站等网站,也可以拖入视频或音频文件",
37589
+ "empty.body": "粘贴 YouTube、B 站等网站的视频或直播链接",
37546
37590
  "st.resolving": "正在打开…",
37547
37591
  "st.login": "正在用 {b} 的登录状态…如果系统弹窗询问,点「始终允许」",
37548
37592
  "st.live": "直播中",
@@ -37555,8 +37599,25 @@ const zh = {
37555
37599
  "note.noModel": "还没配置模型,字幕只显示原文",
37556
37600
  "note.install": "需要先安装:",
37557
37601
  "push.title": "用 OBS 推流到这里",
37558
- "dev.title": "用这台电脑的摄像头",
37559
- "dev.hint": "边说边出字幕",
37602
+ "src.tip": "视频从哪里来",
37603
+ "src.link": "链接",
37604
+ "src.file": "本地文件",
37605
+ "src.camera": "摄像头",
37606
+ "src.push": "OBS 推流",
37607
+ "src.file.placeholder": "视频或音频文件的路径",
37608
+ "src.file.title": "打开这台电脑上的视频",
37609
+ "src.file.body": "把视频或音频文件拖到这里。",
37610
+ "src.file.pick": "选择文件",
37611
+ "src.cam.title": "对着摄像头说话",
37612
+ "src.cam.body": "在上面选好画面和麦克风,点开始,字幕边说边出。",
37613
+ "src.cam.none": "这台电脑上没有找到摄像头或麦克风。",
37614
+ "src.cam.loading": "正在找摄像头…",
37615
+ "src.push.title": "用 OBS 推流过来",
37616
+ "src.push.body": "OBS 里服务选「自定义」,填下面的服务器和密钥,画面一到字幕就开始。",
37617
+ "src.push.bar": "等 OBS 推流过来,服务器和密钥在下面",
37618
+ "src.push.wait": "正在准备推流地址…",
37619
+ "src.push.busy": "关掉当前视频后可以接收推流",
37620
+ "dev.blocked": "设备没有传来画面和声音。到 系统设置 → 隐私与安全性 → 摄像头、麦克风(用屏幕时是 屏幕录制)里打开 DeepSeek Harness,再点开始。",
37560
37621
  "dev.video": "画面",
37561
37622
  "dev.audio": "麦克风",
37562
37623
  "dev.cameras": "摄像头",
@@ -37967,10 +38028,12 @@ const CSS = `
37967
38028
  .dshl-pushinfo dd .err { flex-basis: 100%; color: #d1434b; font-size: 12px; }
37968
38029
  .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
38030
  .dshl-pushinfo .dshl-link { flex: none; font-size: 12.5px; }
37970
- .dshl-devices { display: grid; width: fit-content; max-width: 100%; margin: 32px auto 0; }
37971
- .dshl-devices + .dshl-pushinfo { margin-top: 12px; }
37972
- .dshl-devices dd .dshl-select { width: 240px; max-width: 100%; height: 30px; }
37973
- .dshl-devices > .dshl-btn { justify-self: end; height: 32px; padding: 0 18px; border-radius: 8px; font-size: 13px; }
38031
+ .dshl-bar .dshl-select.srcpick { flex: none; width: auto; height: 40px; padding: 0 10px 0 14px; border-radius: 10px; font-weight: 550; }
38032
+ .dshl-devbar { display: flex; align-items: center; gap: 8px; flex: 1 1 260px; min-width: 0; }
38033
+ .dshl-devbar .dshl-select { flex: 1 1 0; min-width: 0; height: 40px; border-radius: 10px; }
38034
+ .dshl-field.static { color: var(--muted); cursor: default; }
38035
+ .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; }
38036
+ .dshl-drop > .dshl-btn { margin-top: 20px; }
37974
38037
 
37975
38038
  /* ── Layout ── */
37976
38039
  .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") return send(res, 200, {
6387
- devices: await listDevices(),
6388
- platform: process.platform
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.6",
3
+ "version": "0.7.7",
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",