dsh-vidi 0.8.2 → 0.8.3

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) => {
@@ -35968,8 +35957,8 @@ function LiveView(props) {
35968
35957
  const needsModel = !hasTranslator && shownLines.some((l) => l !== "src");
35969
35958
  const hasMain = !!job && !(job.status === "error" && !job.files.length);
35970
35959
  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 ?? "";
35960
+ 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";
35961
+ const camA = mics.some((d) => d.id === camAudio) ? camAudio : mics.find((d) => d.preferred)?.id ?? mics[0]?.id ?? "";
35973
35962
  const videoOptions = [...cams.map((d) => ({
35974
35963
  value: d.id,
35975
35964
  label: d.kind === "screen" ? t("dev.screen").replace("{n}", String(Number(/\d+/.exec(d.name)?.[0] ?? 0) + 1)) : d.name,
@@ -35982,13 +35971,48 @@ function LiveView(props) {
35982
35971
  const startDevice = async () => {
35983
35972
  if (!camA) return;
35984
35973
  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");
35974
+ const kinds = ["microphone", ...cams.find((d) => d.id === camV)?.kind === "camera" ? ["camera"] : []];
35975
+ setAccessIssue(null);
35976
+ try {
35977
+ let st = await (await fetch("/live/api/access")).json();
35978
+ for (const k of kinds) {
35979
+ if (st[k] !== "not-determined") continue;
35980
+ setAsking(true);
35981
+ st = {
35982
+ ...st,
35983
+ [k]: (await post("access", {
35984
+ kind: k,
35985
+ action: "ask"
35986
+ })).state
35987
+ };
35988
+ }
35989
+ setAsking(false);
35990
+ const blocked = kinds.filter((k) => st[k] === "denied" || st[k] === "restricted");
35991
+ if (blocked.length) {
35992
+ setAccessIssue(blocked);
35993
+ return;
35994
+ }
35995
+ } catch {
35996
+ setAsking(false);
35997
+ }
35986
35998
  start(JSON.stringify({
35987
35999
  video: camV === "none" ? void 0 : camV,
35988
36000
  audio: camA,
35989
36001
  title
35990
36002
  }), "device");
35991
36003
  };
36004
+ const openPrivacy = (kind) => void post("access", {
36005
+ kind,
36006
+ action: "settings"
36007
+ }).catch(() => {});
36008
+ const blockedKind = () => {
36009
+ try {
36010
+ const c = JSON.parse(job?.url ?? "{}");
36011
+ return cams.find((d) => d.id === c.video)?.kind === "screen" ? "screen" : "camera";
36012
+ } catch {
36013
+ return "camera";
36014
+ }
36015
+ };
35992
36016
  const emptyBox = () => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
35993
36017
  className: "dshl-drop",
35994
36018
  children: [
@@ -36292,8 +36316,8 @@ function LiveView(props) {
36292
36316
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
36293
36317
  className: "dshl-btn primary",
36294
36318
  onClick: () => void startDevice(),
36295
- disabled: !camA,
36296
- children: t("dev.start")
36319
+ disabled: !camA || asking,
36320
+ children: asking ? t("dev.asking") : t("dev.start")
36297
36321
  })] }),
36298
36322
  srcTab === "push" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
36299
36323
  className: "dshl-field static",
@@ -36390,13 +36414,37 @@ function LiveView(props) {
36390
36414
  className: "dshl-note",
36391
36415
  children: t("note.noModel")
36392
36416
  }),
36393
- (error || job?.status === "error") && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
36417
+ accessIssue && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
36394
36418
  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
- })]
36419
+ children: [
36420
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("dev.denied").replace("{d}", accessIssue.map((k) => t(`dev.k.${k}`)).join(t("dev.and"))) }),
36421
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
36422
+ className: "dshl-link",
36423
+ onClick: () => openPrivacy(accessIssue[0]),
36424
+ children: t("dev.openSettings")
36425
+ }),
36426
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
36427
+ className: "dshl-link",
36428
+ onClick: () => void startDevice(),
36429
+ children: t("st.retry")
36430
+ })
36431
+ ]
36432
+ }),
36433
+ !accessIssue && (error || job?.status === "error") && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
36434
+ className: "dshl-note warn",
36435
+ children: [
36436
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: error || (job?.message === "device-blocked" ? t("dev.blocked") : job?.message) }),
36437
+ job?.message === "device-blocked" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
36438
+ className: "dshl-link",
36439
+ onClick: () => openPrivacy(blockedKind()),
36440
+ children: t("dev.openSettings")
36441
+ }),
36442
+ job && !active && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
36443
+ className: "dshl-link",
36444
+ onClick: () => start(job.url, job.source),
36445
+ children: t("st.retry")
36446
+ })
36447
+ ]
36400
36448
  }),
36401
36449
  !hasMain ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
36402
36450
  className: "dshl-empty",
@@ -37305,7 +37353,14 @@ const en = {
37305
37353
  "src.push.bar": "Waiting for OBS: server and key below",
37306
37354
  "src.push.wait": "Getting the address ready…",
37307
37355
  "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.",
37356
+ "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.",
37357
+ "dev.denied": "DeepSeek Harness needs permission to use the {d}. Open System Settings, switch it on in the list, then retry.",
37358
+ "dev.and": " and ",
37359
+ "dev.k.camera": "camera",
37360
+ "dev.k.microphone": "microphone",
37361
+ "dev.k.screen": "screen",
37362
+ "dev.openSettings": "Open System Settings",
37363
+ "dev.asking": "Allow it in the system dialog…",
37309
37364
  "dev.video": "Picture",
37310
37365
  "dev.audio": "Microphone",
37311
37366
  "dev.cameras": "Cameras",
@@ -37736,7 +37791,14 @@ const zh = {
37736
37791
  "src.push.bar": "等 OBS 推流过来,服务器和密钥在下面",
37737
37792
  "src.push.wait": "正在准备推流地址…",
37738
37793
  "src.push.busy": "关掉当前视频后可以接收推流",
37739
- "dev.blocked": "设备没有传来画面和声音。到 系统设置 → 隐私与安全性 → 摄像头、麦克风(用屏幕时是 屏幕录制)里打开 DeepSeek Harness,再点开始。",
37794
+ "dev.blocked": "设备没有传来画面和声音:DeepSeek Harness 需要系统授权。点「打开系统设置」,在列表里打开 DeepSeek Harness,再点重试。",
37795
+ "dev.denied": "DeepSeek Harness 需要{d}权限。点「打开系统设置」,在列表里打开它,再点重试。",
37796
+ "dev.and": "和",
37797
+ "dev.k.camera": "摄像头",
37798
+ "dev.k.microphone": "麦克风",
37799
+ "dev.k.screen": "屏幕录制",
37800
+ "dev.openSettings": "打开系统设置",
37801
+ "dev.asking": "请在系统弹窗里点允许…",
37740
37802
  "dev.video": "画面",
37741
37803
  "dev.audio": "麦克风",
37742
37804
  "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");
@@ -5778,6 +5811,82 @@ function metered(llm, meter) {
5778
5811
  }
5779
5812
  };
5780
5813
  }
5814
+ // src/access.ts
5815
+ const LOAD = "$.NSBundle.bundleWithPath(\"/System/Library/Frameworks/AVFoundation.framework\").load; const D = $.NSClassFromString(\"AVCaptureDevice\");";
5816
+ const MEDIA = {
5817
+ camera: "vide",
5818
+ microphone: "soun"
5819
+ };
5820
+ const STATES = [
5821
+ "not-determined",
5822
+ "restricted",
5823
+ "denied",
5824
+ "granted"
5825
+ ];
5826
+ function jxa(script, timeout) {
5827
+ return new Promise((resolve) => {
5828
+ execFile("osascript", [
5829
+ "-l",
5830
+ "JavaScript",
5831
+ "-e",
5832
+ script
5833
+ ], { timeout }, (err, out) => resolve(err ? "" : String(out).trim()));
5834
+ });
5835
+ }
5836
+ /** This app's camera and microphone permission (macOS); 'unknown' elsewhere. */
5837
+ async function accessStatus() {
5838
+ if (process.platform !== "darwin") return {
5839
+ camera: "unknown",
5840
+ microphone: "unknown"
5841
+ };
5842
+ const out = await jxa(`${LOAD} JSON.stringify({ camera: D.authorizationStatusForMediaType("vide"), microphone: D.authorizationStatusForMediaType("soun") })`, 1e4);
5843
+ try {
5844
+ const j = JSON.parse(out);
5845
+ return {
5846
+ camera: STATES[Number(j.camera)] ?? "unknown",
5847
+ microphone: STATES[Number(j.microphone)] ?? "unknown"
5848
+ };
5849
+ } catch {
5850
+ return {
5851
+ camera: "unknown",
5852
+ microphone: "unknown"
5853
+ };
5854
+ }
5855
+ }
5856
+ /** Ask once (the system dialog); answers the state afterwards. Already decided: just the state. */
5857
+ async function askAccess(kind) {
5858
+ if (process.platform !== "darwin") return "unknown";
5859
+ const before = (await accessStatus())[kind];
5860
+ if (before !== "not-determined") return before;
5861
+ await jxa(`${LOAD} let done = null; D.requestAccessForMediaTypeCompletionHandler("${MEDIA[kind]}", ok => { done = ok });
5862
+ const end = Date.now() + 120000;
5863
+ while (done === null && Date.now() < end) $.NSRunLoop.currentRunLoop.runUntilDate($.NSDate.dateWithTimeIntervalSinceNow(0.2));
5864
+ String(done)`, 13e4);
5865
+ return (await accessStatus())[kind];
5866
+ }
5867
+ /** Open the system's privacy page for this kind of device, where the app can be switched on. */
5868
+ function openPrivacySettings(kind) {
5869
+ if (process.platform === "darwin") {
5870
+ spawn("open", [`x-apple.systempreferences:com.apple.preference.security?${kind === "camera" ? "Privacy_Camera" : kind === "microphone" ? "Privacy_Microphone" : "Privacy_ScreenCapture"}`], {
5871
+ stdio: "ignore",
5872
+ detached: true
5873
+ }).unref();
5874
+ return true;
5875
+ }
5876
+ if (process.platform === "win32") {
5877
+ spawn("cmd", [
5878
+ "/c",
5879
+ "start",
5880
+ "",
5881
+ `ms-settings:${kind === "camera" ? "privacy-webcam" : kind === "microphone" ? "privacy-microphone" : "privacy"}`
5882
+ ], {
5883
+ stdio: "ignore",
5884
+ detached: true
5885
+ }).unref();
5886
+ return true;
5887
+ }
5888
+ return false;
5889
+ }
5781
5890
  // src/updates.ts
5782
5891
  const DAY = 864e5;
5783
5892
  /** pnpm holds back releases younger than a day: offer only versions it will install. */
@@ -6492,6 +6601,14 @@ function apply(ctx, config) {
6492
6601
  }
6493
6602
  if (route === "version") return send(res, 200, req.method === "POST" ? await updates.check() : updates.state());
6494
6603
  if (route === "update" && req.method === "POST") return send(res, 200, await updates.install());
6604
+ if (route === "access") {
6605
+ if (req.method !== "POST") return send(res, 200, await accessStatus());
6606
+ const b = await readJson(req);
6607
+ const kind = String(b.kind);
6608
+ if (b.action === "settings") return send(res, 200, { opened: openPrivacySettings(kind) });
6609
+ if (kind !== "camera" && kind !== "microphone") return send(res, 400, { error: "camera or microphone" });
6610
+ return send(res, 200, { state: await askAccess(kind) });
6611
+ }
6495
6612
  if (route === "devices") {
6496
6613
  const devices = await listDevices();
6497
6614
  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.3",
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",