dsh-vidi 0.8.6 → 0.8.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 CHANGED
@@ -33608,13 +33608,22 @@ const fmt = (s) => {
33608
33608
  * stage, so subtitles (sized in container units) and menus come along.
33609
33609
  */
33610
33610
  const Player = (0, react.forwardRef)(function Player(props, ref) {
33611
- const { trail, src, live, vod, duration, subsAt, waitingText, cues, siteCues, spoken, style, delay, onDelay, showDelay, theater, onTheater, onTime, action, menus = [], onSnapshot, onClip, onThumb, quality, t } = props;
33611
+ const { preview, trail, src, live, vod, duration, subsAt, waitingText, cues, siteCues, spoken, style, delay, onDelay, showDelay, theater, onTheater, onTime, action, menus = [], onSnapshot, onClip, onThumb, quality, t } = props;
33612
33612
  const stage = (0, react.useRef)(null);
33613
33613
  const video = (0, react.useRef)(null);
33614
33614
  const hls = (0, react.useRef)(null);
33615
33615
  const [ready, setReady] = (0, react.useState)(false);
33616
33616
  const [playing, setPlaying] = (0, react.useState)(false);
33617
33617
  const [muted, setMuted] = (0, react.useState)(false);
33618
+ const [bigLive, setBigLive] = (0, react.useState)(true);
33619
+ const liveBig = !!preview && bigLive;
33620
+ (0, react.useEffect)(() => {
33621
+ const el = video.current;
33622
+ if (el && liveBig) {
33623
+ el.muted = true;
33624
+ setMuted(true);
33625
+ }
33626
+ }, [liveBig]);
33618
33627
  const [volume, setVolume] = (0, react.useState)(1);
33619
33628
  const [now, setNow] = (0, react.useState)(0);
33620
33629
  const [edge, setEdge] = (0, react.useState)(0);
@@ -33806,6 +33815,7 @@ const Player = (0, react.forwardRef)(function Player(props, ref) {
33806
33815
  const pos = h?.liveSyncPosition ?? edge - delay;
33807
33816
  if (pos != null && isFinite(pos)) el.currentTime = pos;
33808
33817
  el.play();
33818
+ setBigLive(true);
33809
33819
  }, [edge, delay]);
33810
33820
  const applyDelay = (0, react.useCallback)((v) => {
33811
33821
  onDelay(v);
@@ -33878,13 +33888,20 @@ const Player = (0, react.forwardRef)(function Player(props, ref) {
33878
33888
  const behind = Math.max(0, edge - now);
33879
33889
  const atLive = behind < delay + 10;
33880
33890
  const current = cues.filter((c) => c.start - .2 <= now && now <= c.end + 1.2).at(-1) ?? (trail ? cues.filter((c) => c.start <= now && c.end > now - 8).at(-1) : void 0);
33891
+ const textsOf = (cue) => {
33892
+ const out = {};
33893
+ if (!cue) return out;
33894
+ out.src = noStop(cue.src);
33895
+ for (const [k, v] of Object.entries(cue.tr ?? {})) if (v && v.trim() !== cue.src.trim()) out[k] = noStop(v);
33896
+ if (spoken) out[spoken] = out.src;
33897
+ return out;
33898
+ };
33899
+ const newest = cues.at(-1);
33900
+ const liveCue = newest && (!newest.final || (subsAt ?? 0) - newest.end < 6) ? newest : void 0;
33901
+ const liveTexts = ccOn ? textsOf(liveCue) : {};
33881
33902
  const texts = {};
33882
33903
  if (ccOn) {
33883
- if (current) {
33884
- texts.src = noStop(current.src);
33885
- for (const [k, v] of Object.entries(current.tr ?? {})) if (v && v.trim() !== current.src.trim()) texts[k] = noStop(v);
33886
- if (spoken) texts[spoken] = texts.src;
33887
- }
33904
+ if (current) Object.assign(texts, textsOf(current));
33888
33905
  for (const [k, list] of Object.entries(siteCues ?? {})) texts[k] = list.find((c) => c.start - .2 <= now && now <= c.end + .3)?.text;
33889
33906
  }
33890
33907
  const snapshot = async () => {
@@ -34027,16 +34044,52 @@ const Player = (0, react.forwardRef)(function Player(props, ref) {
34027
34044
  if (!e.target.closest(".dshl-controls, .dshl-menu")) toggleFull();
34028
34045
  },
34029
34046
  children: [
34030
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("video", {
34031
- ref: video,
34032
- playsInline: true,
34033
- onClick: togglePlay
34047
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
34048
+ className: `dshl-vbox ${liveBig ? "pip" : ""}`,
34049
+ onClick: liveBig ? () => setBigLive(false) : void 0,
34050
+ title: liveBig ? t("own.toDelayed") : void 0,
34051
+ children: [
34052
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("video", {
34053
+ ref: video,
34054
+ playsInline: true,
34055
+ onClick: liveBig ? void 0 : togglePlay
34056
+ }),
34057
+ liveBig && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SubtitleOverlay, {
34058
+ texts,
34059
+ style,
34060
+ draft: current ? !current.final : false
34061
+ }),
34062
+ liveBig && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
34063
+ className: "dshl-piplabel",
34064
+ children: t("own.delayed")
34065
+ })
34066
+ ]
34034
34067
  }),
34035
- !ready && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
34068
+ preview && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
34069
+ className: `dshl-livecam ${liveBig ? "" : "pip"}`,
34070
+ onClick: liveBig ? void 0 : () => setBigLive(true),
34071
+ title: liveBig ? void 0 : t("own.toLive"),
34072
+ children: [
34073
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
34074
+ src: preview,
34075
+ alt: ""
34076
+ }),
34077
+ !liveBig && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SubtitleOverlay, {
34078
+ texts: liveTexts,
34079
+ style,
34080
+ draft: liveCue ? !liveCue.final : false
34081
+ }),
34082
+ !liveBig && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
34083
+ className: "dshl-piplabel",
34084
+ children: t("own.live")
34085
+ })
34086
+ ]
34087
+ }),
34088
+ !ready && !liveBig && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
34036
34089
  className: "dshl-overlay",
34037
34090
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "dshl-spinner" }), waitingText]
34038
34091
  }),
34039
- ready && held && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
34092
+ ready && held && !liveBig && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
34040
34093
  className: "dshl-holdnote",
34041
34094
  children: [
34042
34095
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", { className: "dshl-spinner sm" }),
@@ -34048,7 +34101,11 @@ const Player = (0, react.forwardRef)(function Player(props, ref) {
34048
34101
  })
34049
34102
  ]
34050
34103
  }),
34051
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SubtitleOverlay, {
34104
+ liveBig ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SubtitleOverlay, {
34105
+ texts: liveTexts,
34106
+ style,
34107
+ draft: liveCue ? !liveCue.final : false
34108
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SubtitleOverlay, {
34052
34109
  texts,
34053
34110
  style,
34054
34111
  draft: current ? !current.final : false
@@ -34075,13 +34132,13 @@ const Player = (0, react.forwardRef)(function Player(props, ref) {
34075
34132
  ]
34076
34133
  }),
34077
34134
  ready && live && !vod && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
34078
- className: `dshl-livebadge ${atLive ? "on" : ""}`,
34135
+ className: `dshl-livebadge ${atLive || liveBig ? "on" : ""}`,
34079
34136
  onClick: goLive,
34080
34137
  title: t("player.goLive"),
34081
34138
  children: [
34082
34139
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.live, {}),
34083
34140
  " ",
34084
- atLive ? t("player.live") : t("player.behind").replace("{s}", fmt(behind))
34141
+ atLive || liveBig ? t("player.live") : t("player.behind").replace("{s}", fmt(behind))
34085
34142
  ]
34086
34143
  }),
34087
34144
  openMenu && (() => {
@@ -34162,6 +34219,7 @@ const Player = (0, react.forwardRef)(function Player(props, ref) {
34162
34219
  value: Math.min(now, total),
34163
34220
  onChange: (e) => {
34164
34221
  if (video.current) video.current.currentTime = Math.min(Number(e.target.value), edge);
34222
+ setBigLive(false);
34165
34223
  },
34166
34224
  "aria-label": "seek"
34167
34225
  })
@@ -36474,6 +36532,7 @@ function LiveView(props) {
36474
36532
  duration: job.duration,
36475
36533
  subsAt: subs.state === "off" ? void 0 : subs.at,
36476
36534
  waitingText: job.status === "resolving" ? statusText : t("player.waiting"),
36535
+ preview: job.preview && job.status === "running" ? `/live/api/preview?jobId=${encodeURIComponent(job.id)}` : void 0,
36477
36536
  cues,
36478
36537
  trail: mode === "live" && !job.vod,
36479
36538
  siteCues: shownSite,
@@ -37359,6 +37418,10 @@ const en = {
37359
37418
  "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
37419
  "dev.denied": "DeepSeek Harness needs permission to use the {d}. Open System Settings, switch it on in the list, then retry.",
37361
37420
  "dev.and": " and ",
37421
+ "own.live": "Live",
37422
+ "own.delayed": "As pushed out",
37423
+ "own.toLive": "Show the live picture large",
37424
+ "own.toDelayed": "Show what is pushed out (with the subtitle delay) large",
37362
37425
  "dev.stalled": "The camera sent pictures but they could not be recorded. Retry; if it happens again, pick another camera.",
37363
37426
  "st.failedEarlier": "This one did not finish last time.",
37364
37427
  "dev.k.camera": "camera",
@@ -37799,6 +37862,10 @@ const zh = {
37799
37862
  "dev.blocked": "设备没有传来画面和声音:DeepSeek Harness 需要系统授权。点「打开系统设置」,在列表里打开 DeepSeek Harness,再点重试。",
37800
37863
  "dev.denied": "DeepSeek Harness 需要{d}权限。点「打开系统设置」,在列表里打开它,再点重试。",
37801
37864
  "dev.and": "和",
37865
+ "own.live": "实时画面",
37866
+ "own.delayed": "推流画面",
37867
+ "own.toLive": "放大实时画面",
37868
+ "own.toDelayed": "放大推流画面(按字幕模式延迟)",
37802
37869
  "dev.stalled": "摄像头有画面传来,但没能录下来。点重试;还不行就换一个摄像头试试。",
37803
37870
  "st.failedEarlier": "上次这个没有成功。",
37804
37871
  "dev.k.camera": "摄像头",
@@ -38327,6 +38394,15 @@ html[data-platform='darwin'] .dshl.page .dshl-top :is(button, input, label, a, .
38327
38394
  .dshl-stage.full { width: 100vw; height: 100vh; aspect-ratio: auto; border-radius: 0; }
38328
38395
  .dshl-stage.pagefull { position: fixed; inset: 0; z-index: 2147483000; width: 100vw; height: 100vh; aspect-ratio: auto; border-radius: 0; }
38329
38396
  .dshl-stage video { display: block; width: 100%; height: 100%; object-fit: contain; background: #000; }
38397
+ /* Own camera: the recording sits in a box that becomes a small window while the real-time picture is large. */
38398
+ .dshl-vbox { position: absolute; inset: 0; container-type: inline-size; }
38399
+ .dshl-livecam { position: absolute; inset: 0; z-index: 1; container-type: inline-size; background: #000; }
38400
+ .dshl-livecam img { display: block; width: 100%; height: 100%; object-fit: contain; }
38401
+ .dshl-vbox.pip, .dshl-livecam.pip { inset: auto 14px 74px auto; z-index: 3; width: 28%; min-width: 180px; height: auto; aspect-ratio: 16 / 9; overflow: hidden; border-radius: 8px;
38402
+ outline: 1px solid rgba(255,255,255,.28); box-shadow: 0 6px 24px rgba(0,0,0,.5); cursor: pointer; transition: bottom .25s; }
38403
+ .dshl-stage.idle .dshl-vbox.pip, .dshl-stage.idle .dshl-livecam.pip { bottom: 14px; }
38404
+ .dshl-stage .pip .dshl-sub { transform: translateX(-50%) !important; }
38405
+ .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; }
38330
38406
  .dshl-stage.idle { cursor: none; }
38331
38407
  .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%);
38332
38408
  background: rgba(20,20,22,.78); color: #f2f2f3; font-size: 12.5px; white-space: nowrap; backdrop-filter: blur(8px); }
@@ -38350,11 +38426,11 @@ html[data-platform='darwin'] .dshl.page .dshl-top :is(button, input, label, a, .
38350
38426
  .dshl-stage:not(.idle) .dshl-sub[data-pos=bottom] { transform: translate(-50%, -64px); }
38351
38427
  .dshl-stage:not(.idle) .dshl-sub[data-pos=top] { transform: translate(-50%, 34px); }
38352
38428
 
38353
- .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;
38354
- background: rgba(0,0,0,.55); color: #fff; font-size: 12px; font-weight: 600; letter-spacing: .04em; cursor: pointer; transition: opacity .25s; }
38355
- .dshl-livebadge svg { color: #9a9a9a; }
38356
- .dshl-livebadge.on { background: var(--live); }
38357
- .dshl-livebadge.on svg { color: #fff; animation: dshl-pulse 1.6s ease-in-out infinite; }
38429
+ .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;
38430
+ 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; }
38431
+ .dshl-livebadge:hover { background: rgba(0,0,0,.6); }
38432
+ .dshl-livebadge svg { width: 10px; height: 10px; flex: none; color: rgba(255,255,255,.5); }
38433
+ .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; }
38358
38434
 
38359
38435
  .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;
38360
38436
  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%); }
package/lib/index.mjs CHANGED
@@ -1990,9 +1990,14 @@ const AHEAD_MAX_LINES = 10;
1990
1990
  */
1991
1991
  /** Drop the first `skip` seconds after decoding (an output -ss; empty when there is nothing to drop). */
1992
1992
  const seekAfter = (skip) => skip > .01 ? ["-ss", skip.toFixed(3)] : [];
1993
+ const SOI = Buffer.from([255, 216]);
1994
+ const EOI = Buffer.from([255, 217]);
1993
1995
  var LiveJob = class LiveJob extends EventEmitter {
1994
1996
  opts;
1995
1997
  deps;
1998
+ /** Real-time picture (own sources): the newest JPEG frame, and whether frames are coming. */
1999
+ lastFrame;
2000
+ previewOn = false;
1996
2001
  id;
1997
2002
  status = "resolving";
1998
2003
  message;
@@ -2076,6 +2081,7 @@ var LiveJob = class LiveJob extends EventEmitter {
2076
2081
  vod: this.vod,
2077
2082
  duration: this.duration,
2078
2083
  recorded: this.vod ? void 0 : this.recordedSec(),
2084
+ preview: this.previewOn,
2079
2085
  loginBrowser: this.loginBrowser,
2080
2086
  needsLogin: this.needsLogin,
2081
2087
  hls: this.hlsName,
@@ -2463,11 +2469,43 @@ var LiveJob = class LiveJob extends EventEmitter {
2463
2469
  join(hls, "seg_%05d.ts"),
2464
2470
  join(hls, "index.m3u8")
2465
2471
  ];
2472
+ const preview = (this.opts.source === "device" || this.opts.source === "push") && !!input.video;
2473
+ if (preview) args.push("-map", input.video, "-an", "-vf", "scale=-2:540,fps=15", "-c:v", "mjpeg", "-q:v", "6", "-f", "image2pipe", "pipe:3");
2466
2474
  const ff = spawn("ffmpeg", args, { stdio: [
2467
2475
  "ignore",
2468
2476
  "ignore",
2469
- "pipe"
2477
+ "pipe",
2478
+ preview ? "pipe" : "ignore"
2470
2479
  ] });
2480
+ this.previewOn = false;
2481
+ if (preview) {
2482
+ let tail = Buffer.alloc(0);
2483
+ ff.stdio[3].on("data", (chunk) => {
2484
+ if (this.ffmpeg !== ff) return;
2485
+ let buf = tail.length ? Buffer.concat([tail, chunk]) : chunk;
2486
+ for (;;) {
2487
+ const start = buf.indexOf(SOI);
2488
+ if (start < 0) {
2489
+ buf = Buffer.alloc(0);
2490
+ break;
2491
+ }
2492
+ const end = buf.indexOf(EOI, start + 2);
2493
+ if (end < 0) {
2494
+ buf = buf.subarray(start);
2495
+ break;
2496
+ }
2497
+ const frame = Buffer.from(buf.subarray(start, end + 2));
2498
+ buf = buf.subarray(end + 2);
2499
+ this.lastFrame = frame;
2500
+ if (!this.previewOn) {
2501
+ this.previewOn = true;
2502
+ this.emit("state", this.summary());
2503
+ }
2504
+ this.emit("frame", frame);
2505
+ }
2506
+ tail = buf.length > 4e6 ? Buffer.alloc(0) : Buffer.from(buf);
2507
+ });
2508
+ }
2471
2509
  this.ffmpeg = ff;
2472
2510
  if (this.opts.source === "device") setTimeout(() => {
2473
2511
  if (this.ffmpeg !== ff || this.segments().length) return;
@@ -2498,6 +2536,10 @@ var LiveJob = class LiveJob extends EventEmitter {
2498
2536
  }
2499
2537
  });
2500
2538
  ff.on("exit", (code) => {
2539
+ if (this.previewOn && this.ffmpeg === ff) {
2540
+ this.previewOn = false;
2541
+ this.lastFrame = void 0;
2542
+ }
2501
2543
  if (this.ffmpeg !== ff) return;
2502
2544
  this.ffmpeg = void 0;
2503
2545
  if (this.status !== "running") return;
@@ -6846,6 +6888,25 @@ function apply(ctx, config) {
6846
6888
  saveAs: typeof b.saveAs === "string" ? b.saveAs.replace(/[\\/:*?"<>|]+/g, " ").slice(0, 150) : "audio"
6847
6889
  }));
6848
6890
  }
6891
+ if (route === "preview") {
6892
+ const job = jobs.get(url.searchParams.get("jobId") ?? "");
6893
+ if (!job) return send(res, 404, { error: "no such job" });
6894
+ res.writeHead(200, {
6895
+ "content-type": "multipart/x-mixed-replace; boundary=vidiframe",
6896
+ "cache-control": "no-store",
6897
+ connection: "keep-alive"
6898
+ });
6899
+ const write = (f) => {
6900
+ if (res.writableLength > 2e6) return;
6901
+ res.write(`--vidiframe\r\nContent-Type: image/jpeg\r\nContent-Length: ${f.length}\r\n\r\n`);
6902
+ res.write(f);
6903
+ res.write("\r\n");
6904
+ };
6905
+ if (job.lastFrame) write(job.lastFrame);
6906
+ job.on("frame", write);
6907
+ req.on("close", () => job.off("frame", write));
6908
+ return;
6909
+ }
6849
6910
  if (route === "job") {
6850
6911
  const jid = url.searchParams.get("jobId");
6851
6912
  const job = jid ? jobs.get(jid) : jobForConversation(url.searchParams.get("conversationId") ?? "");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vidi",
3
- "version": "0.8.6",
3
+ "version": "0.8.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",