dsh-vidi 0.8.7 → 0.8.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/lib/client.js CHANGED
@@ -34384,14 +34384,9 @@ const CLOCK$1 = (s) => {
34384
34384
  return `${h ? `${h}:` : ""}${String(m).padStart(h ? 2 : 1, "0")}:${String(ss).padStart(2, "0")}`;
34385
34385
  };
34386
34386
  const host = (s) => s.replace(/^[a-z]+:\/\//i, "").split("/")[0] ?? s;
34387
- /**
34388
- * The player's restream menu: saved destinations (tick the ones to push to),
34389
- * adding one (platform, server address, stream key — copied from the
34390
- * platform's own streaming tool), subtitles drawn in or not, picture size,
34391
- * then start. While running, each destination shows its own state.
34392
- */
34393
34387
  function RestreamPanel(props) {
34394
- const { jobId, status, now, style, t } = props;
34388
+ const { jobId, status, now, style, timing, t } = props;
34389
+ const fromNow = () => timing ? timing.fromFor(timing.mode) : now;
34395
34390
  const running = !!status?.running;
34396
34391
  const [targets, setTargets] = (0, react.useState)([]);
34397
34392
  const [picked, setPicked] = (0, react.useState)(() => {
@@ -34463,7 +34458,7 @@ function RestreamPanel(props) {
34463
34458
  jobId,
34464
34459
  action: "start",
34465
34460
  targets: picked.filter((id) => targets.some((x) => x.id === id)),
34466
- from: now,
34461
+ from: fromNow(),
34467
34462
  burn,
34468
34463
  height,
34469
34464
  style
@@ -34491,12 +34486,39 @@ function RestreamPanel(props) {
34491
34486
  jobId,
34492
34487
  action: "start",
34493
34488
  targets: [id],
34494
- from: now,
34489
+ from: fromNow(),
34495
34490
  burn,
34496
34491
  height,
34497
34492
  style
34498
34493
  }).catch((e) => setError(e.message));
34499
34494
  };
34495
+ const switchMode = async (m) => {
34496
+ if (!timing || m === timing.mode) return;
34497
+ timing.onMode(m);
34498
+ if (!running) return;
34499
+ const ids = status.targets.map((x) => x.id);
34500
+ setBusy(true);
34501
+ setError("");
34502
+ try {
34503
+ await post$2("restream", {
34504
+ jobId,
34505
+ action: "stop"
34506
+ });
34507
+ await post$2("restream", {
34508
+ jobId,
34509
+ action: "start",
34510
+ targets: ids,
34511
+ from: timing.fromFor(m),
34512
+ burn,
34513
+ height,
34514
+ style
34515
+ });
34516
+ } catch (e) {
34517
+ setError(e.message);
34518
+ } finally {
34519
+ setBusy(false);
34520
+ }
34521
+ };
34500
34522
  const stateOf = (id) => running ? status.targets.find((x) => x.id === id) : void 0;
34501
34523
  const chosen = picked.filter((id) => targets.some((x) => x.id === id));
34502
34524
  if (editing) {
@@ -34678,6 +34700,32 @@ function RestreamPanel(props) {
34678
34700
  className: "rs-err",
34679
34701
  children: status.targets.filter((x) => x.message && x.state !== "live").map((x) => `${pname(x.platform)}:${x.message}`).join(";")
34680
34702
  }),
34703
+ timing && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
34704
+ className: "rs-mode",
34705
+ children: [
34706
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
34707
+ className: "rs-sub",
34708
+ children: t("rs.mode")
34709
+ }),
34710
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
34711
+ className: "seg",
34712
+ children: [
34713
+ "live",
34714
+ "realtime",
34715
+ "polished"
34716
+ ].map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
34717
+ className: timing.mode === m ? "on" : "",
34718
+ disabled: busy,
34719
+ onClick: () => void switchMode(m),
34720
+ children: t(`mode.${m}`)
34721
+ }, m))
34722
+ }),
34723
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
34724
+ className: "rs-hint",
34725
+ children: t(`rs.mode.${timing.mode}`).replace("{s}", String(Math.max(1, Math.round(timing.behindFor(timing.mode)))))
34726
+ })
34727
+ ]
34728
+ }),
34681
34729
  !running && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
34682
34730
  className: "rs-opts",
34683
34731
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -36014,6 +36062,18 @@ function LiveView(props) {
36014
36062
  const missing = deps.filter((d) => !d.ok && (d.name === "ffmpeg" || d.name === "yt-dlp" || (engine || "local") === "local" && d.name.startsWith("whisper")));
36015
36063
  const needsModel = !hasTranslator && shownLines.some((l) => l !== "src");
36016
36064
  const hasMain = !!job && !(job.status === "error" && !job.files.some((f) => /\.(mp4|m4a|srt|vtt)$/.test(f)));
36065
+ const liveEdge = job?.recorded ?? now;
36066
+ const pushFrom = (m) => {
36067
+ const firstPass = subs.ready ?? subs.at;
36068
+ const at = m === "live" ? liveEdge - 2 : m === "realtime" ? firstPass : Math.max(lastPolished, firstPass - 150);
36069
+ return Math.max(0, Math.min(at, liveEdge));
36070
+ };
36071
+ const pushTiming = job && !job.vod && job.status === "running" ? {
36072
+ mode,
36073
+ onMode: changeMode,
36074
+ fromFor: pushFrom,
36075
+ behindFor: (m) => liveEdge - pushFrom(m)
36076
+ } : void 0;
36017
36077
  (0, react.useEffect)(() => {
36018
36078
  if (job && !hasMain) setSrcTabRaw(job.source === "device" ? "camera" : job.source === "push" ? "push" : job.source === "file" ? "file" : "link");
36019
36079
  }, [job?.id, hasMain]);
@@ -36594,6 +36654,7 @@ function LiveView(props) {
36594
36654
  status: job.restream,
36595
36655
  now,
36596
36656
  style: subStyle,
36657
+ timing: pushTiming,
36597
36658
  t
36598
36659
  })
36599
36660
  },
@@ -37418,6 +37479,10 @@ const en = {
37418
37479
  "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.",
37419
37480
  "dev.denied": "DeepSeek Harness needs permission to use the {d}. Open System Settings, switch it on in the list, then retry.",
37420
37481
  "dev.and": " and ",
37482
+ "rs.mode": "Subtitle mode of the push-out",
37483
+ "rs.mode.live": "Pushed out about {s} s behind; subtitles come a little after the words.",
37484
+ "rs.mode.realtime": "Pushed out about {s} s behind, with first-pass subtitles in step.",
37485
+ "rs.mode.polished": "Pushed out about {s} s behind, with polished subtitles.",
37421
37486
  "own.live": "Live",
37422
37487
  "own.delayed": "As pushed out",
37423
37488
  "own.toLive": "Show the live picture large",
@@ -37862,6 +37927,10 @@ const zh = {
37862
37927
  "dev.blocked": "设备没有传来画面和声音:DeepSeek Harness 需要系统授权。点「打开系统设置」,在列表里打开 DeepSeek Harness,再点重试。",
37863
37928
  "dev.denied": "DeepSeek Harness 需要{d}权限。点「打开系统设置」,在列表里打开它,再点重试。",
37864
37929
  "dev.and": "和",
37930
+ "rs.mode": "推流用的字幕模式",
37931
+ "rs.mode.live": "推出去比实时晚约 {s} 秒,字幕会比说话稍晚出现。",
37932
+ "rs.mode.realtime": "推出去比实时晚约 {s} 秒,带对齐好的初版字幕。",
37933
+ "rs.mode.polished": "推出去比实时晚约 {s} 秒,带精修字幕。",
37865
37934
  "own.live": "实时画面",
37866
37935
  "own.delayed": "推流画面",
37867
37936
  "own.toLive": "放大实时画面",
@@ -38402,6 +38471,9 @@ html[data-platform='darwin'] .dshl.page .dshl-top :is(button, input, label, a, .
38402
38471
  outline: 1px solid rgba(255,255,255,.28); box-shadow: 0 6px 24px rgba(0,0,0,.5); cursor: pointer; transition: bottom .25s; }
38403
38472
  .dshl-stage.idle .dshl-vbox.pip, .dshl-stage.idle .dshl-livecam.pip { bottom: 14px; }
38404
38473
  .dshl-stage .pip .dshl-sub { transform: translateX(-50%) !important; }
38474
+ .dshl-rs .rs-mode { display: grid; gap: 6px; margin: 10px 0 4px; }
38475
+ .dshl-rs .rs-mode .seg { display: flex; }
38476
+ .dshl-rs .rs-mode .seg button { flex: 1; }
38405
38477
  .dshl-piplabel { position: absolute; top: 6px; left: 6px; z-index: 3; padding: 2px 7px; border-radius: 5px; background: rgba(0,0,0,.6); color: #fff; font-size: 11px; pointer-events: none; }
38406
38478
  .dshl-stage.idle { cursor: none; }
38407
38479
  .dshl-holdnote { position: absolute; top: 14px; left: 50%; z-index: 4; display: flex; align-items: center; gap: 8px; padding: 6px 12px; border-radius: 999px; transform: translateX(-50%);
@@ -38426,11 +38498,11 @@ html[data-platform='darwin'] .dshl.page .dshl-top :is(button, input, label, a, .
38426
38498
  .dshl-stage:not(.idle) .dshl-sub[data-pos=bottom] { transform: translate(-50%, -64px); }
38427
38499
  .dshl-stage:not(.idle) .dshl-sub[data-pos=top] { transform: translate(-50%, 34px); }
38428
38500
 
38429
- .dshl-livebadge { position: absolute; top: 12px; left: 12px; z-index: 3; display: inline-flex; align-items: center; gap: 6px; height: 26px; padding: 0 10px; border: 0; border-radius: 6px;
38430
- background: rgba(0,0,0,.55); color: #fff; font-size: 12px; font-weight: 600; letter-spacing: .04em; cursor: pointer; transition: opacity .25s; }
38431
- .dshl-livebadge svg { color: #9a9a9a; }
38432
- .dshl-livebadge.on { background: var(--live); }
38433
- .dshl-livebadge.on svg { color: #fff; animation: dshl-pulse 1.6s ease-in-out infinite; }
38501
+ .dshl-livebadge { position: absolute; top: 12px; left: 12px; z-index: 3; display: inline-flex; align-items: center; gap: 6px; height: 24px; padding: 0 10px 0 8px; border: 0; border-radius: 999px;
38502
+ background: rgba(0,0,0,.42); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); color: #fff !important; font-size: 11.5px; font-weight: 600; letter-spacing: .06em; line-height: 1; cursor: pointer; transition: opacity .25s, background .15s; }
38503
+ .dshl-livebadge:hover { background: rgba(0,0,0,.6); }
38504
+ .dshl-livebadge svg { width: 10px; height: 10px; flex: none; color: rgba(255,255,255,.5); }
38505
+ .dshl-livebadge.on svg { color: #ff4d4f; filter: drop-shadow(0 0 3px rgba(255,77,79,.8)); animation: dshl-pulse 1.6s ease-in-out infinite; }
38434
38506
 
38435
38507
  .dshl-toast { position: absolute; top: 12px; left: 50%; z-index: 6; display: inline-flex; align-items: center; gap: 10px; max-width: calc(100% - 24px); padding: 8px 14px;
38436
38508
  border-radius: 10px; background: rgba(20,20,22,.86); color: #fff; font-size: 13px; box-shadow: 0 6px 20px rgba(0,0,0,.3); transform: translateX(-50%); }
@@ -38893,8 +38965,14 @@ async function openPage(sessions, opts = {}) {
38893
38965
  }
38894
38966
  async function showPage(sessions, opts) {
38895
38967
  const current = sessions.current();
38896
- const sid = opts.sessionId ?? await pageSession(sessions, opts.workspaces, opts.title ?? "Video");
38897
- if (sid !== current) sessions.open(sid);
38968
+ const fresh = () => pageSession(sessions, opts.workspaces, opts.title ?? "Video");
38969
+ let sid = opts.sessionId && sessions.has(opts.sessionId) ? opts.sessionId : await fresh();
38970
+ if (sid !== current) try {
38971
+ sessions.open(sid);
38972
+ } catch {
38973
+ sid = await fresh();
38974
+ sessions.open(sid);
38975
+ }
38898
38976
  if (panelHost) {
38899
38977
  pageStore.set({
38900
38978
  open: true,
@@ -39037,7 +39115,11 @@ function SidebarButton(props) {
39037
39115
  const pct = item?.subs === "running" && item.duration ? Math.min(99, Math.round(item.at / item.duration * 100)) : void 0;
39038
39116
  const meta = !busy ? "" : item?.subs === "running" ? pct !== void 0 ? t("side.subs").replace("{p}", String(pct)) : t("side.subsLive") : item?.recording ? t("side.recording") : item?.loading ? t("side.loading") : t("side.downloading");
39039
39117
  const thumb = busy && frame && (!item || frame.jobId === item.jobId) ? frame.dataUrl : void 0;
39040
- const open = () => void openPage(sessions, item ? { sessionId: item.conversationId } : {
39118
+ const open = () => void openPage(sessions, item ? {
39119
+ sessionId: item.conversationId,
39120
+ workspaces,
39121
+ title: t("view.live")
39122
+ } : {
39041
39123
  workspaces,
39042
39124
  title: t("view.live")
39043
39125
  });
package/lib/index.mjs CHANGED
@@ -1912,8 +1912,6 @@ function deviceInput(c, q) {
1912
1912
  if (process.platform === "darwin") return {
1913
1913
  args: [
1914
1914
  ...q,
1915
- "-use_wallclock_as_timestamps",
1916
- "1",
1917
1915
  "-f",
1918
1916
  "avfoundation",
1919
1917
  "-framerate",
@@ -1998,6 +1996,8 @@ var LiveJob = class LiveJob extends EventEmitter {
1998
1996
  /** Real-time picture (own sources): the newest JPEG frame, and whether frames are coming. */
1999
1997
  lastFrame;
2000
1998
  previewOn = false;
1999
+ /** Closed by the user: no longer offered anywhere (its files stay). */
2000
+ closed = false;
2001
2001
  id;
2002
2002
  status = "resolving";
2003
2003
  message;
@@ -2451,6 +2451,7 @@ var LiveJob = class LiveJob extends EventEmitter {
2451
2451
  ...picture,
2452
2452
  "-map",
2453
2453
  input.audio,
2454
+ ...input.fps ? ["-af", "aresample=async=1:min_hard_comp=0.1:first_pts=0"] : [],
2454
2455
  "-c:a",
2455
2456
  "aac",
2456
2457
  "-b:a",
@@ -6558,6 +6559,7 @@ function apply(ctx, config) {
6558
6559
  const job = jobs.get(String(b.jobId));
6559
6560
  if (!job) return send(res, 404, { error: "no such job" });
6560
6561
  await job.close("closed");
6562
+ job.closed = true;
6561
6563
  const conv = job.summary().conversationId;
6562
6564
  if (byConversation.get(conv) === job.id) byConversation.set(conv, "");
6563
6565
  try {
@@ -6601,7 +6603,7 @@ function apply(ctx, config) {
6601
6603
  return send(res, 200, job.summary());
6602
6604
  }
6603
6605
  if (route === "activity") return send(res, 200, {
6604
- items: [...jobs.values()].map((j) => j.summary()).filter((s) => s.status === "running" && !s.message || s.status === "resolving" || s.subs?.state === "running").map((s) => ({
6606
+ items: [...jobs.values()].filter((j) => !j.closed).map((j) => j.summary()).filter((s) => s.status === "running" && !s.message || s.status === "resolving" || s.subs?.state === "running").map((s) => ({
6605
6607
  jobId: s.id,
6606
6608
  conversationId: s.conversationId,
6607
6609
  title: s.title,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vidi",
3
- "version": "0.8.7",
3
+ "version": "0.8.9",
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",