dsh-vidi 0.8.2 → 0.8.4

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 CHANGED
@@ -35281,19 +35281,6 @@ function useDevices(enabled) {
35281
35281
  }, [enabled, devices]);
35282
35282
  return devices;
35283
35283
  }
35284
- /**
35285
- * Ask for the camera / microphone from the page first when the page runs on the same computer: the system
35286
- * then asks the user once for this app, and the capture that follows is allowed.
35287
- */
35288
- async function askDeviceAccess(video) {
35289
- if (!(location.protocol === "dsh-app:" || /^(localhost|127\.0\.0\.1|\[::1\])$/.test(location.hostname)) || !navigator.mediaDevices?.getUserMedia) return;
35290
- try {
35291
- (await navigator.mediaDevices.getUserMedia({
35292
- audio: true,
35293
- video
35294
- })).getTracks().forEach((tr) => tr.stop());
35295
- } catch {}
35296
- }
35297
35284
  /** The push stream key: copy it, or change it (typed, or a new random one). */
35298
35285
  function KeyRow({ value, t, onChanged }) {
35299
35286
  const [edit, setEdit] = (0, react.useState)(null);
@@ -35488,7 +35475,9 @@ function LiveView(props) {
35488
35475
  setSrcTabRaw(v);
35489
35476
  setError("");
35490
35477
  };
35491
- const devices = useDevices(srcTab === "camera");
35478
+ const devices = useDevices(srcTab === "camera" || job?.source === "device");
35479
+ const [accessIssue, setAccessIssue] = (0, react.useState)(null);
35480
+ const [asking, setAsking] = (0, react.useState)(false);
35492
35481
  const [camVideo, setCamVideoRaw] = (0, react.useState)(() => store.get("device.video", ""));
35493
35482
  const [camAudio, setCamAudioRaw] = (0, react.useState)(() => store.get("device.audio", ""));
35494
35483
  const setCamVideo = (v) => {
@@ -35966,10 +35955,13 @@ function LiveView(props) {
35966
35955
  const statusDetail = job?.status === "running" && !job.message && !job.vod ? CLOCK(Math.max(0, (tick - job.startedAt) / 1e3)) : "";
35967
35956
  const missing = deps.filter((d) => !d.ok && (d.name === "ffmpeg" || d.name === "yt-dlp" || (engine || "local") === "local" && d.name.startsWith("whisper")));
35968
35957
  const needsModel = !hasTranslator && shownLines.some((l) => l !== "src");
35969
- const hasMain = !!job && !(job.status === "error" && !job.files.length);
35958
+ const hasMain = !!job && !(job.status === "error" && !job.files.some((f) => /\.(mp4|m4a|srt|vtt)$/.test(f)));
35959
+ (0, react.useEffect)(() => {
35960
+ if (job && !hasMain) setSrcTabRaw(job.source === "device" ? "camera" : job.source === "push" ? "push" : job.source === "file" ? "file" : "link");
35961
+ }, [job?.id, hasMain]);
35970
35962
  const cams = (devices ?? []).filter((d) => d.kind !== "mic"), mics = (devices ?? []).filter((d) => d.kind === "mic");
35971
- const camV = camVideo === "none" || cams.some((d) => d.id === camVideo) ? camVideo : cams.find((d) => d.kind === "camera")?.id ?? "none";
35972
- const camA = mics.some((d) => d.id === camAudio) ? camAudio : mics[0]?.id ?? "";
35963
+ const camV = camVideo === "none" || cams.some((d) => d.id === camVideo) ? camVideo : cams.find((d) => d.preferred)?.id ?? cams.find((d) => d.kind === "camera")?.id ?? "none";
35964
+ const camA = mics.some((d) => d.id === camAudio) ? camAudio : mics.find((d) => d.preferred)?.id ?? mics[0]?.id ?? "";
35973
35965
  const videoOptions = [...cams.map((d) => ({
35974
35966
  value: d.id,
35975
35967
  label: d.kind === "screen" ? t("dev.screen").replace("{n}", String(Number(/\d+/.exec(d.name)?.[0] ?? 0) + 1)) : d.name,
@@ -35982,13 +35974,48 @@ function LiveView(props) {
35982
35974
  const startDevice = async () => {
35983
35975
  if (!camA) return;
35984
35976
  const title = camV === "none" ? mics.find((d) => d.id === camA)?.name ?? "" : videoOptions.find((o) => o.value === camV)?.label ?? "";
35985
- await askDeviceAccess(camV !== "none" && cams.find((d) => d.id === camV)?.kind === "camera");
35977
+ const kinds = ["microphone", ...cams.find((d) => d.id === camV)?.kind === "camera" ? ["camera"] : []];
35978
+ setAccessIssue(null);
35979
+ try {
35980
+ let st = await (await fetch("/live/api/access")).json();
35981
+ for (const k of kinds) {
35982
+ if (st[k] !== "not-determined") continue;
35983
+ setAsking(true);
35984
+ st = {
35985
+ ...st,
35986
+ [k]: (await post("access", {
35987
+ kind: k,
35988
+ action: "ask"
35989
+ })).state
35990
+ };
35991
+ }
35992
+ setAsking(false);
35993
+ const blocked = kinds.filter((k) => st[k] === "denied" || st[k] === "restricted");
35994
+ if (blocked.length) {
35995
+ setAccessIssue(blocked);
35996
+ return;
35997
+ }
35998
+ } catch {
35999
+ setAsking(false);
36000
+ }
35986
36001
  start(JSON.stringify({
35987
36002
  video: camV === "none" ? void 0 : camV,
35988
36003
  audio: camA,
35989
36004
  title
35990
36005
  }), "device");
35991
36006
  };
36007
+ const openPrivacy = (kind) => void post("access", {
36008
+ kind,
36009
+ action: "settings"
36010
+ }).catch(() => {});
36011
+ const blockedKind = () => {
36012
+ try {
36013
+ const c = JSON.parse(job?.url ?? "{}");
36014
+ return cams.find((d) => d.id === c.video)?.kind === "screen" ? "screen" : "camera";
36015
+ } catch {
36016
+ return "camera";
36017
+ }
36018
+ };
35992
36019
  const emptyBox = () => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
35993
36020
  className: "dshl-drop",
35994
36021
  children: [
@@ -36292,8 +36319,8 @@ function LiveView(props) {
36292
36319
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
36293
36320
  className: "dshl-btn primary",
36294
36321
  onClick: () => void startDevice(),
36295
- disabled: !camA,
36296
- children: t("dev.start")
36322
+ disabled: !camA || asking,
36323
+ children: asking ? t("dev.asking") : t("dev.start")
36297
36324
  })] }),
36298
36325
  srcTab === "push" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
36299
36326
  className: "dshl-field static",
@@ -36390,13 +36417,37 @@ function LiveView(props) {
36390
36417
  className: "dshl-note",
36391
36418
  children: t("note.noModel")
36392
36419
  }),
36393
- (error || job?.status === "error") && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
36420
+ accessIssue && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
36394
36421
  className: "dshl-note warn",
36395
- 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", {
36396
- className: "dshl-link",
36397
- onClick: () => start(job.url, job.source),
36398
- children: t("st.retry")
36399
- })]
36422
+ children: [
36423
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("dev.denied").replace("{d}", accessIssue.map((k) => t(`dev.k.${k}`)).join(t("dev.and"))) }),
36424
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
36425
+ className: "dshl-link",
36426
+ onClick: () => openPrivacy(accessIssue[0]),
36427
+ children: t("dev.openSettings")
36428
+ }),
36429
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
36430
+ className: "dshl-link",
36431
+ onClick: () => void startDevice(),
36432
+ children: t("st.retry")
36433
+ })
36434
+ ]
36435
+ }),
36436
+ !accessIssue && (error || job?.status === "error") && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
36437
+ className: "dshl-note warn",
36438
+ children: [
36439
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: error || (job?.message === "device-blocked" ? t("dev.blocked") : job?.message === "restored" ? t("st.failedEarlier") : job?.message) }),
36440
+ job?.message === "device-blocked" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
36441
+ className: "dshl-link",
36442
+ onClick: () => openPrivacy(blockedKind()),
36443
+ children: t("dev.openSettings")
36444
+ }),
36445
+ job && !active && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
36446
+ className: "dshl-link",
36447
+ onClick: () => start(job.url, job.source),
36448
+ children: t("st.retry")
36449
+ })
36450
+ ]
36400
36451
  }),
36401
36452
  !hasMain ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
36402
36453
  className: "dshl-empty",
@@ -37305,7 +37356,15 @@ const en = {
37305
37356
  "src.push.bar": "Waiting for OBS: server and key below",
37306
37357
  "src.push.wait": "Getting the address ready…",
37307
37358
  "src.push.busy": "Close the current video to receive a stream",
37308
- "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.",
37359
+ "dev.blocked": "No picture or sound from the device: DeepSeek Harness needs the system’s permission. Open System Settings, switch DeepSeek Harness on, then retry.",
37360
+ "dev.denied": "DeepSeek Harness needs permission to use the {d}. Open System Settings, switch it on in the list, then retry.",
37361
+ "dev.and": " and ",
37362
+ "st.failedEarlier": "This one did not finish last time.",
37363
+ "dev.k.camera": "camera",
37364
+ "dev.k.microphone": "microphone",
37365
+ "dev.k.screen": "screen",
37366
+ "dev.openSettings": "Open System Settings",
37367
+ "dev.asking": "Allow it in the system dialog…",
37309
37368
  "dev.video": "Picture",
37310
37369
  "dev.audio": "Microphone",
37311
37370
  "dev.cameras": "Cameras",
@@ -37736,7 +37795,15 @@ const zh = {
37736
37795
  "src.push.bar": "等 OBS 推流过来,服务器和密钥在下面",
37737
37796
  "src.push.wait": "正在准备推流地址…",
37738
37797
  "src.push.busy": "关掉当前视频后可以接收推流",
37739
- "dev.blocked": "设备没有传来画面和声音。到 系统设置 → 隐私与安全性 → 摄像头、麦克风(用屏幕时是 屏幕录制)里打开 DeepSeek Harness,再点开始。",
37798
+ "dev.blocked": "设备没有传来画面和声音:DeepSeek Harness 需要系统授权。点「打开系统设置」,在列表里打开 DeepSeek Harness,再点重试。",
37799
+ "dev.denied": "DeepSeek Harness 需要{d}权限。点「打开系统设置」,在列表里打开它,再点重试。",
37800
+ "dev.and": "和",
37801
+ "st.failedEarlier": "上次这个没有成功。",
37802
+ "dev.k.camera": "摄像头",
37803
+ "dev.k.microphone": "麦克风",
37804
+ "dev.k.screen": "屏幕录制",
37805
+ "dev.openSettings": "打开系统设置",
37806
+ "dev.asking": "请在系统弹窗里点允许…",
37740
37807
  "dev.video": "画面",
37741
37808
  "dev.audio": "麦克风",
37742
37809
  "dev.cameras": "摄像头",
package/lib/index.mjs CHANGED
@@ -1829,7 +1829,7 @@ async function listDevices() {
1829
1829
  kind: /^Capture screen/.test(name) ? "screen" : "camera"
1830
1830
  });
1831
1831
  }
1832
- return out.filter((d) => !(d.kind === "mic" && /speakers|audio device|blackhole|soundflower|loopback/i.test(d.name)));
1832
+ return preferLocal(out.filter((d) => !(d.kind === "mic" && /speakers|audio device|streaming|virtual|blackhole|soundflower|loopback/i.test(d.name))), await defaultInput());
1833
1833
  }
1834
1834
  if (process.platform === "win32") {
1835
1835
  const text = await ffmpegList([
@@ -1873,6 +1873,39 @@ async function listDevices() {
1873
1873
  kind: "mic"
1874
1874
  }];
1875
1875
  }
1876
+ /** The system's default input device (macOS), by name. */
1877
+ function defaultInput() {
1878
+ return new Promise((resolve) => {
1879
+ execFile("system_profiler", ["SPAudioDataType", "-json"], { timeout: 8e3 }, (err, out) => {
1880
+ if (err) return resolve(void 0);
1881
+ try {
1882
+ resolve((JSON.parse(String(out)).SPAudioDataType?.[0]?._items ?? []).find((i) => i.coreaudio_default_audio_input_device === "spaudio_yes")?._name);
1883
+ } catch {
1884
+ resolve(void 0);
1885
+ }
1886
+ });
1887
+ });
1888
+ }
1889
+ /**
1890
+ * Mark (and list first) this computer's own camera and the default microphone, ahead of a phone's camera
1891
+ * through Continuity ("“Name”的相机", "Name's Camera") or a desk-view camera.
1892
+ */
1893
+ function preferLocal(list, defaultMic) {
1894
+ const phone = (d) => /^[“"].*[”"]/.test(d.name) || /['’]s (camera|microphone)/i.test(d.name);
1895
+ const deskView = (d) => /桌上视角|desk view/i.test(d.name);
1896
+ const cams = list.filter((d) => d.kind === "camera");
1897
+ const cam = cams.find((d) => !phone(d) && !deskView(d) && /macbook|imac|facetime|built-in|内建|display/i.test(d.name)) ?? cams.find((d) => !phone(d) && !deskView(d)) ?? cams[0];
1898
+ const mics = list.filter((d) => d.kind === "mic");
1899
+ const mic = mics.find((d) => d.name === defaultMic) ?? mics.find((d) => !phone(d)) ?? mics[0];
1900
+ for (const d of [cam, mic]) if (d) d.preferred = true;
1901
+ const rank = (d) => d.preferred ? 0 : phone(d) || deskView(d) ? 2 : 1;
1902
+ const group = {
1903
+ camera: 0,
1904
+ screen: 1,
1905
+ mic: 2
1906
+ };
1907
+ return [...list].sort((a, b) => group[a.kind] - group[b.kind] || rank(a) - rank(b));
1908
+ }
1876
1909
  /** ffmpeg input for the chosen devices: arguments plus which streams carry the picture and the sound. */
1877
1910
  function deviceInput(c, q) {
1878
1911
  if (!c.audio) throw new Error("choose a microphone");
@@ -2086,7 +2119,8 @@ var LiveJob = class LiveJob extends EventEmitter {
2086
2119
  heights: this.heights,
2087
2120
  startedAt: this.startedAt,
2088
2121
  subs: this.subs,
2089
- status: this.status
2122
+ status: this.status,
2123
+ ...this.status === "error" ? { message: this.message } : {}
2090
2124
  }, null, 2));
2091
2125
  } catch {}
2092
2126
  }
@@ -2100,7 +2134,7 @@ var LiveJob = class LiveJob extends EventEmitter {
2100
2134
  job.duration = j.duration;
2101
2135
  job.captureDone = true;
2102
2136
  job.status = j.status === "error" ? "error" : "stopped";
2103
- job.message = "restored";
2137
+ job.message = j.status === "error" && j.message ? j.message : "restored";
2104
2138
  const seen = /* @__PURE__ */ new Map();
2105
2139
  if (existsSync(join(dir, "cues.jsonl"))) for (const line of readFileSync(join(dir, "cues.jsonl"), "utf8").split("\n")) {
2106
2140
  if (!line.trim()) continue;
@@ -5778,6 +5812,82 @@ function metered(llm, meter) {
5778
5812
  }
5779
5813
  };
5780
5814
  }
5815
+ // src/access.ts
5816
+ const LOAD = "$.NSBundle.bundleWithPath(\"/System/Library/Frameworks/AVFoundation.framework\").load; const D = $.NSClassFromString(\"AVCaptureDevice\");";
5817
+ const MEDIA = {
5818
+ camera: "vide",
5819
+ microphone: "soun"
5820
+ };
5821
+ const STATES = [
5822
+ "not-determined",
5823
+ "restricted",
5824
+ "denied",
5825
+ "granted"
5826
+ ];
5827
+ function jxa(script, timeout) {
5828
+ return new Promise((resolve) => {
5829
+ execFile("osascript", [
5830
+ "-l",
5831
+ "JavaScript",
5832
+ "-e",
5833
+ script
5834
+ ], { timeout }, (err, out) => resolve(err ? "" : String(out).trim()));
5835
+ });
5836
+ }
5837
+ /** This app's camera and microphone permission (macOS); 'unknown' elsewhere. */
5838
+ async function accessStatus() {
5839
+ if (process.platform !== "darwin") return {
5840
+ camera: "unknown",
5841
+ microphone: "unknown"
5842
+ };
5843
+ const out = await jxa(`${LOAD} JSON.stringify({ camera: D.authorizationStatusForMediaType("vide"), microphone: D.authorizationStatusForMediaType("soun") })`, 1e4);
5844
+ try {
5845
+ const j = JSON.parse(out);
5846
+ return {
5847
+ camera: STATES[Number(j.camera)] ?? "unknown",
5848
+ microphone: STATES[Number(j.microphone)] ?? "unknown"
5849
+ };
5850
+ } catch {
5851
+ return {
5852
+ camera: "unknown",
5853
+ microphone: "unknown"
5854
+ };
5855
+ }
5856
+ }
5857
+ /** Ask once (the system dialog); answers the state afterwards. Already decided: just the state. */
5858
+ async function askAccess(kind) {
5859
+ if (process.platform !== "darwin") return "unknown";
5860
+ const before = (await accessStatus())[kind];
5861
+ if (before !== "not-determined") return before;
5862
+ await jxa(`${LOAD} let done = null; D.requestAccessForMediaTypeCompletionHandler("${MEDIA[kind]}", ok => { done = ok });
5863
+ const end = Date.now() + 120000;
5864
+ while (done === null && Date.now() < end) $.NSRunLoop.currentRunLoop.runUntilDate($.NSDate.dateWithTimeIntervalSinceNow(0.2));
5865
+ String(done)`, 13e4);
5866
+ return (await accessStatus())[kind];
5867
+ }
5868
+ /** Open the system's privacy page for this kind of device, where the app can be switched on. */
5869
+ function openPrivacySettings(kind) {
5870
+ if (process.platform === "darwin") {
5871
+ spawn("open", [`x-apple.systempreferences:com.apple.preference.security?${kind === "camera" ? "Privacy_Camera" : kind === "microphone" ? "Privacy_Microphone" : "Privacy_ScreenCapture"}`], {
5872
+ stdio: "ignore",
5873
+ detached: true
5874
+ }).unref();
5875
+ return true;
5876
+ }
5877
+ if (process.platform === "win32") {
5878
+ spawn("cmd", [
5879
+ "/c",
5880
+ "start",
5881
+ "",
5882
+ `ms-settings:${kind === "camera" ? "privacy-webcam" : kind === "microphone" ? "privacy-microphone" : "privacy"}`
5883
+ ], {
5884
+ stdio: "ignore",
5885
+ detached: true
5886
+ }).unref();
5887
+ return true;
5888
+ }
5889
+ return false;
5890
+ }
5781
5891
  // src/updates.ts
5782
5892
  const DAY = 864e5;
5783
5893
  /** pnpm holds back releases younger than a day: offer only versions it will install. */
@@ -6492,6 +6602,14 @@ function apply(ctx, config) {
6492
6602
  }
6493
6603
  if (route === "version") return send(res, 200, req.method === "POST" ? await updates.check() : updates.state());
6494
6604
  if (route === "update" && req.method === "POST") return send(res, 200, await updates.install());
6605
+ if (route === "access") {
6606
+ if (req.method !== "POST") return send(res, 200, await accessStatus());
6607
+ const b = await readJson(req);
6608
+ const kind = String(b.kind);
6609
+ if (b.action === "settings") return send(res, 200, { opened: openPrivacySettings(kind) });
6610
+ if (kind !== "camera" && kind !== "microphone") return send(res, 400, { error: "camera or microphone" });
6611
+ return send(res, 200, { state: await askAccess(kind) });
6612
+ }
6495
6613
  if (route === "devices") {
6496
6614
  const devices = await listDevices();
6497
6615
  log(`devices: ${devices.filter((d) => d.kind !== "mic").length} picture, ${devices.filter((d) => d.kind === "mic").length} microphone`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vidi",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
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",