dsh-vidi 0.4.2 → 0.4.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
@@ -35632,6 +35632,10 @@ function SubsButton(props) {
35632
35632
  className: "now",
35633
35633
  children: pct !== void 0 ? t("subs.progress").replace("{p}", String(pct)) : t("subs.progressLive").replace("{t}", CLOCK(at))
35634
35634
  }),
35635
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
35636
+ className: "short",
35637
+ children: pct !== void 0 ? `${pct}%` : CLOCK(at)
35638
+ }),
35635
35639
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
35636
35640
  className: "act",
35637
35641
  children: t("subs.pause")
@@ -37050,7 +37054,12 @@ html[data-platform='darwin'] .dshl.page .dshl-top :is(button, input, label, a, s
37050
37054
  .dshl-ctrlrow .pill svg { flex: none; width: 18px; height: 18px; }
37051
37055
  .dshl-ctrlrow .pill .txt { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
37052
37056
  .dshl-ctrlrow .pill .caret { flex: none; width: 5px; height: 5px; border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor; opacity: .8; transform: translateY(1px) rotate(-135deg); }
37053
- @container (max-width: 640px) { .dshl-ctrlrow .pill .txt, .dshl-ctrlrow .pill .caret { display: none; } .dshl-ctrlrow .pill { padding: 0 6px; } }
37057
+ @container (max-width: 640px) {
37058
+ .dshl-ctrlrow .pill .txt, .dshl-ctrlrow .pill .caret { display: none; } .dshl-ctrlrow .pill { padding: 0 6px; }
37059
+ /* Narrow player (a workbench beside it, a phone): fewer words, no volume slider (the mute button stays). */
37060
+ .dshl-ctrlrow .vol, .dshl-ctrlrow .theater { display: none; }
37061
+ .dshl-ctrlrow .ic { width: 32px; height: 32px; }
37062
+ }
37054
37063
  @container (max-width: 520px) {
37055
37064
  .dshl-ctrlrow { gap: 2px; }
37056
37065
  .dshl-ctrlrow .vol, .dshl-ctrlrow .mute, .dshl-ctrlrow .theater { display: none; }
@@ -37066,7 +37075,8 @@ html[data-platform='darwin'] .dshl.page .dshl-top :is(button, input, label, a, s
37066
37075
  .dshl-subsbtn:hover { filter: brightness(1.08); }
37067
37076
  .dshl-subsbtn.running { background: linear-gradient(to right, rgba(127,176,255,.45) var(--p, 0%), rgba(255,255,255,.14) var(--p, 0%)); font-weight: 500; }
37068
37077
  .dshl-subsbtn.running .dot { width: 7px; height: 7px; border-radius: 50%; background: #ff5a5f; animation: dshl-pulse 1.6s ease-in-out infinite; }
37069
- .dshl-subsbtn.running .act { display: none; }
37078
+ .dshl-subsbtn.running .act, .dshl-subsbtn .short { display: none; }
37079
+ @container (max-width: 640px) { .dshl-subsbtn .now { display: none; } .dshl-subsbtn.running:not(:hover) .short { display: inline; } }
37070
37080
  .dshl-subsbtn.running:hover .now { display: none; }
37071
37081
  .dshl-subsbtn.running:hover .act { display: inline; }
37072
37082
  .dshl-subsbtn.running:hover .dot { width: 8px; height: 8px; border-radius: 1px; background: #fff; animation: none; }
package/lib/index.mjs CHANGED
@@ -1693,6 +1693,8 @@ const AHEAD_MAX_LINES = 10;
1693
1693
  *
1694
1694
  * Events: 'state' (JobSummary), 'cue' (Cue — new or revised).
1695
1695
  */
1696
+ /** Drop the first `skip` seconds after decoding (an output -ss; empty when there is nothing to drop). */
1697
+ const seekAfter = (skip) => skip > .01 ? ["-ss", skip.toFixed(3)] : [];
1696
1698
  var LiveJob = class LiveJob extends EventEmitter {
1697
1699
  opts;
1698
1700
  deps;
@@ -2594,7 +2596,14 @@ var LiveJob = class LiveJob extends EventEmitter {
2594
2596
  exportText(langs, format) {
2595
2597
  return exportCues(this.cues, langs, format, this.title);
2596
2598
  }
2597
- /** Best available picture source for time range [from, to]: original download, finished recording, or recorded segments. */
2599
+ /**
2600
+ * Best available picture source for time range [from, to]: original
2601
+ * download, finished recording, or recorded segments. `args` are the input
2602
+ * arguments; `skip` is how much to drop after decoding (output -ss). Files
2603
+ * seek exactly on input; live segments do not (a seek lands between
2604
+ * keyframes — YouTube live has one every 5 s — and decodes nothing), so they
2605
+ * are read from the start of the first segment and trimmed after decoding.
2606
+ */
2598
2607
  mediaFor(from, to) {
2599
2608
  const original = readdirSync(this.deps.dir).find((n) => /^original\.(mp4|mkv|webm)$/.test(n));
2600
2609
  if (original) return {
@@ -2604,7 +2613,7 @@ var LiveJob = class LiveJob extends EventEmitter {
2604
2613
  "-i",
2605
2614
  join(this.deps.dir, original)
2606
2615
  ],
2607
- offset: 0
2616
+ skip: 0
2608
2617
  };
2609
2618
  if (this.captureDone && existsSync(join(this.deps.dir, "recording.mp4"))) return {
2610
2619
  args: [
@@ -2613,7 +2622,7 @@ var LiveJob = class LiveJob extends EventEmitter {
2613
2622
  "-i",
2614
2623
  join(this.deps.dir, "recording.mp4")
2615
2624
  ],
2616
- offset: 0
2625
+ skip: 0
2617
2626
  };
2618
2627
  const segs = this.segments();
2619
2628
  const files = readFileSync(this.m3u8, "utf8").split("\n").filter((l) => l && !l.startsWith("#"));
@@ -2621,29 +2630,26 @@ var LiveJob = class LiveJob extends EventEmitter {
2621
2630
  let last = segs.findIndex((sg) => sg.start + sg.dur >= to);
2622
2631
  if (last < 0) last = segs.length - 1;
2623
2632
  if (!segs.length || first > last) throw new Error("that part has not been recorded yet");
2624
- const list = files.slice(first, last + 1).map((f) => join(this.deps.dir, this.hlsName, f)).join("|");
2625
2633
  return {
2626
- args: [
2627
- "-ss",
2628
- String(from - segs[first].start),
2629
- "-i",
2630
- `concat:${list}`
2631
- ],
2632
- offset: 0
2634
+ args: ["-i", `concat:${files.slice(first, last + 1).map((f) => join(this.deps.dir, this.hlsName, f)).join("|")}`],
2635
+ skip: Math.max(0, from - segs[first].start)
2633
2636
  };
2634
2637
  }
2635
2638
  /** One small JPEG of the picture at media time `at`, for the model to look at (undefined for audio-only or on failure). */
2636
2639
  async still(at) {
2637
2640
  try {
2638
- const { args } = this.mediaFor(at, at + 1);
2641
+ const { args, skip } = this.mediaFor(at, at + 1);
2639
2642
  const { stdout } = await run$1("ffmpeg", [
2640
2643
  "-v",
2641
2644
  "error",
2642
2645
  ...args,
2646
+ ...seekAfter(skip),
2643
2647
  "-frames:v",
2644
2648
  "1",
2645
2649
  "-vf",
2646
2650
  "scale=768:-2",
2651
+ "-pix_fmt",
2652
+ "yuvj420p",
2647
2653
  "-q:v",
2648
2654
  "5",
2649
2655
  "-f",
@@ -2666,18 +2672,22 @@ var LiveJob = class LiveJob extends EventEmitter {
2666
2672
  const dir = join(this.deps.dir, "frames");
2667
2673
  mkdirSync(dir, { recursive: true });
2668
2674
  const out = join(dir, `frame_${at.toFixed(1).replace(".", "_")}.jpg`);
2669
- const { args } = this.mediaFor(at, at + 1);
2675
+ const { args, skip } = this.mediaFor(at, at + 1);
2670
2676
  await run$1("ffmpeg", [
2671
2677
  "-v",
2672
2678
  "error",
2673
2679
  "-y",
2674
2680
  ...args,
2681
+ ...seekAfter(skip),
2675
2682
  "-frames:v",
2676
2683
  "1",
2684
+ "-pix_fmt",
2685
+ "yuvj420p",
2677
2686
  "-q:v",
2678
2687
  "2",
2679
2688
  out
2680
2689
  ], { timeout: 6e4 });
2690
+ if (!existsSync(out)) throw new Error("no picture at that moment");
2681
2691
  return out;
2682
2692
  }
2683
2693
  /**
@@ -2690,14 +2700,14 @@ var LiveJob = class LiveJob extends EventEmitter {
2690
2700
  mkdirSync(dir, { recursive: true });
2691
2701
  const name = `clip_${from.toFixed(1)}-${to.toFixed(1)}`.replace(/\./g, "_");
2692
2702
  const out = join(dir, `${name}.mp4`);
2693
- const { args } = this.mediaFor(from, to);
2703
+ const { args, skip } = this.mediaFor(from, to);
2694
2704
  const subArgs = [];
2695
2705
  const maps = [];
2696
2706
  if (langs?.length) {
2697
2707
  const inRange = this.cues.filter((c) => c.final && c.end > from && c.start < to).map((c) => ({
2698
2708
  ...c,
2699
- start: Math.max(0, c.start - from),
2700
- end: Math.min(to, c.end) - from
2709
+ start: Math.max(0, c.start - from) + skip,
2710
+ end: Math.min(to, c.end) - from + skip
2701
2711
  }));
2702
2712
  (langs.length > 1 ? [langs, ...langs.map((l) => [l])] : [langs]).forEach((ls, i) => {
2703
2713
  const f = join(dir, `.${name}.${i}.srt`);
@@ -2712,6 +2722,7 @@ var LiveJob = class LiveJob extends EventEmitter {
2712
2722
  "-y",
2713
2723
  ...args,
2714
2724
  ...subArgs,
2725
+ ...seekAfter(skip),
2715
2726
  "-t",
2716
2727
  String(to - from),
2717
2728
  "-map",
@@ -2764,8 +2775,16 @@ var LiveJob = class LiveJob extends EventEmitter {
2764
2775
  rows
2765
2776
  });
2766
2777
  }
2778
+ const { args, skip } = this.mediaFor(from, to);
2767
2779
  const pictures = /* @__PURE__ */ new Map();
2768
2780
  const lines = ["ffconcat version 1.0"];
2781
+ if (skip > 0) {
2782
+ const blank = await drawSubtitles([], style, w, h);
2783
+ if (!blank) throw new Error("subtitles could not be drawn on this computer");
2784
+ const file = join(tmp, "blank.png");
2785
+ writeFileSync(file, blank);
2786
+ lines.push(`file '${file.replace(/'/g, "'\\''")}'`, `duration ${skip.toFixed(3)}`);
2787
+ }
2769
2788
  for (const s of spans) {
2770
2789
  let file = pictures.get(s.key);
2771
2790
  if (!file) {
@@ -2780,7 +2799,6 @@ var LiveJob = class LiveJob extends EventEmitter {
2780
2799
  lines.push(`file '${pictures.get(spans.at(-1).key).replace(/'/g, "'\\''")}'`);
2781
2800
  const list = join(tmp, "subs.ffconcat");
2782
2801
  writeFileSync(list, lines.join("\n") + "\n");
2783
- const { args } = this.mediaFor(from, to);
2784
2802
  try {
2785
2803
  await run$1("ffmpeg", [
2786
2804
  "-v",
@@ -2793,6 +2811,7 @@ var LiveJob = class LiveJob extends EventEmitter {
2793
2811
  "0",
2794
2812
  "-i",
2795
2813
  list,
2814
+ ...seekAfter(skip),
2796
2815
  "-t",
2797
2816
  String(to - from),
2798
2817
  "-filter_complex",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vidi",
3
- "version": "0.4.2",
3
+ "version": "0.4.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",