dsh-vidi 0.5.1 → 0.5.2

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.
Files changed (3) hide show
  1. package/lib/client.js +117 -15
  2. package/lib/index.mjs +89 -16
  3. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -1603,7 +1603,7 @@ function fragmentsAreEqual(frag, otherFrag) {
1603
1603
  /**
1604
1604
  * Object representing parsed data from an HLS Segment. Found in {@link hls.js#LevelDetails.fragments}.
1605
1605
  */
1606
- var Fragment = class extends BaseSegment {
1606
+ var Fragment$1 = class extends BaseSegment {
1607
1607
  constructor(type, base) {
1608
1608
  super(base);
1609
1609
  this._decryptdata = null;
@@ -9753,7 +9753,7 @@ var M3U8Parser = class M3U8Parser {
9753
9753
  let discontinuityCounter = 0;
9754
9754
  let currentBitrate = 0;
9755
9755
  let prevFrag = null;
9756
- let frag = new Fragment(type, base);
9756
+ let frag = new Fragment$1(type, base);
9757
9757
  let result;
9758
9758
  let i;
9759
9759
  let levelkeys;
@@ -9771,7 +9771,7 @@ var M3U8Parser = class M3U8Parser {
9771
9771
  while ((result = LEVEL_PLAYLIST_REGEX_FAST.exec(string)) !== null) {
9772
9772
  if (createNextFrag) {
9773
9773
  createNextFrag = false;
9774
- frag = new Fragment(type, base);
9774
+ frag = new Fragment$1(type, base);
9775
9775
  frag.playlistOffset = totalduration;
9776
9776
  frag.setStart(totalduration);
9777
9777
  frag.sn = currentSN;
@@ -9819,7 +9819,7 @@ var M3U8Parser = class M3U8Parser {
9819
9819
  const byteRange = frag.byteRange;
9820
9820
  if (byteRange.length === 2) frag.bitrate = (byteRange[1] - byteRange[0]) * 8 / frag.duration | 0;
9821
9821
  if (level.iframesOnly && byteRange[0] && ((_currentInitSegment = currentInitSegment) == null ? void 0 : _currentInitSegment.cc) !== discontinuityCounter) {
9822
- const init = new Fragment(type, base);
9822
+ const init = new Fragment$1(type, base);
9823
9823
  init.relurl = frag.relurl;
9824
9824
  init.setByteRange(`${Math.min(byteRange[0], 1316)}@0`);
9825
9825
  init.level = id;
@@ -9953,7 +9953,7 @@ var M3U8Parser = class M3U8Parser {
9953
9953
  case "MAP": {
9954
9954
  const mapAttrs = new AttrList(value1, level);
9955
9955
  if (frag.duration) {
9956
- const init = new Fragment(type, base);
9956
+ const init = new Fragment$1(type, base);
9957
9957
  setInitSegment(init, mapAttrs, id, levelkeys);
9958
9958
  currentInitSegment = init;
9959
9959
  frag.initSegment = currentInitSegment;
@@ -33343,6 +33343,95 @@ var Hls = class Hls {
33343
33343
  };
33344
33344
  Hls.defaultConfig = void 0;
33345
33345
 
33346
+ // src/wrap.ts
33347
+ /**
33348
+ * Line breaking for subtitles drawn on a canvas (burned into clips and
33349
+ * restreams, screenshots): break where the text allows, never start a line
33350
+ * with closing punctuation, and spread the text evenly over the lines it
33351
+ * needs — so a long line wraps as two halves instead of a full line plus one
33352
+ * orphaned character.
33353
+ */
33354
+ /** Characters that may not begin a line (they stay with the text before them). */
33355
+ const NO_START = new Set(Array.from(",。、;:!?)》」』】〕〉”’…—,.;:!?)]}%"));
33356
+ /** Where a sentence pauses: breaking right after these reads best. */
33357
+ const PAUSE = ",。、;:!?,;:!? ";
33358
+ /** Greedy breaking at `width`: Latin text at spaces, CJK between any two characters. */
33359
+ function greedy(chars, width, measure) {
33360
+ const lines = [];
33361
+ let cur = "";
33362
+ for (const ch of chars) {
33363
+ const next = cur + ch;
33364
+ if (cur && measure(next) > width && !NO_START.has(ch)) {
33365
+ const sp = cur.lastIndexOf(" ");
33366
+ const pause = Math.max(...Array.from(PAUSE).map((p) => cur.lastIndexOf(p)));
33367
+ if (/[A-Za-z0-9]/.test(ch) && /[A-Za-z0-9]$/.test(cur) && sp > cur.length * .3) {
33368
+ lines.push(cur.slice(0, sp));
33369
+ cur = cur.slice(sp + 1) + ch;
33370
+ } else if (pause > cur.length * .6 && pause < cur.length - 1) {
33371
+ lines.push(cur.slice(0, pause + 1).trimEnd());
33372
+ cur = cur.slice(pause + 1).trimStart() + ch;
33373
+ } else {
33374
+ lines.push(cur.trimEnd());
33375
+ cur = ch.trimStart();
33376
+ }
33377
+ } else cur = next;
33378
+ }
33379
+ if (cur.trim()) lines.push(cur.trimEnd());
33380
+ return lines;
33381
+ }
33382
+ /** Wrap `text` to at most `maxWidth`, using as few lines as possible, each about the same length. */
33383
+ function wrapBalanced(text, maxWidth, measure) {
33384
+ const chars = Array.from(text.trim());
33385
+ const first = greedy(chars, maxWidth, measure);
33386
+ if (first.length < 2) return first;
33387
+ let lo = measure(text) / first.length * .9, hi = maxWidth, best = first;
33388
+ for (let i = 0; i < 14 && hi - lo > 1; i++) {
33389
+ const mid = (lo + hi) / 2;
33390
+ const lines = greedy(chars, mid, measure);
33391
+ if (lines.length <= first.length) {
33392
+ best = lines;
33393
+ hi = mid;
33394
+ } else lo = mid;
33395
+ }
33396
+ return atPauses(best, maxWidth, measure);
33397
+ }
33398
+ /** Move each break to a nearby pause (within 4 characters) when both lines still fit. */
33399
+ function atPauses(lines, maxWidth, measure) {
33400
+ const out = lines.map((l) => Array.from(l));
33401
+ for (let k = 0; k + 1 < out.length; k++) {
33402
+ const a = out[k], b = out[k + 1];
33403
+ if (PAUSE.includes(a.at(-1) ?? "")) continue;
33404
+ const sep = /[\x21-\x7e]$/.test(a.join("")) && /^[\x21-\x7e]/.test(b.join("")) ? [" "] : [];
33405
+ let pick;
33406
+ for (let d = 1; d <= 4 && pick === void 0; d++) for (const shift of [d, -d]) {
33407
+ const cut = a.length + shift;
33408
+ if (cut <= 0) continue;
33409
+ const joined = [
33410
+ ...a,
33411
+ ...sep,
33412
+ ...b
33413
+ ];
33414
+ if (cut >= joined.length) continue;
33415
+ if (!PAUSE.includes(joined[cut - 1])) continue;
33416
+ const na = joined.slice(0, cut).join("").trimEnd(), nb = joined.slice(cut).join("").trimStart();
33417
+ if (nb && measure(na) <= maxWidth && measure(nb) <= maxWidth) {
33418
+ pick = shift;
33419
+ break;
33420
+ }
33421
+ }
33422
+ if (pick !== void 0) {
33423
+ const joined = [
33424
+ ...a,
33425
+ ...sep,
33426
+ ...b
33427
+ ], cut = a.length + pick;
33428
+ out[k] = Array.from(joined.slice(0, cut).join("").trimEnd());
33429
+ out[k + 1] = Array.from(joined.slice(cut).join("").trimStart());
33430
+ }
33431
+ }
33432
+ return out.map((l) => l.join(""));
33433
+ }
33434
+
33346
33435
  // src/client/subStyle.tsx
33347
33436
  const STORE = "dsh-live.subStyle";
33348
33437
  /** First line is the main one (largest); the original, when not first, is the smallest. */
@@ -33400,11 +33489,29 @@ function visibleRows(texts, style) {
33400
33489
  return true;
33401
33490
  });
33402
33491
  }
33492
+ let measurer;
33403
33493
  function SubtitleOverlay({ texts, style, draft }) {
33404
33494
  const rows = visibleRows(texts, style);
33405
- if (!rows.length) return null;
33495
+ const box = (0, react.useRef)(null);
33496
+ const [width, setWidth] = (0, react.useState)(0);
33497
+ (0, react.useLayoutEffect)(() => {
33498
+ const el = box.current;
33499
+ if (!el) return;
33500
+ const ro = new ResizeObserver(() => setWidth(el.clientWidth));
33501
+ ro.observe(el);
33502
+ setWidth(el.clientWidth);
33503
+ return () => ro.disconnect();
33504
+ }, []);
33505
+ const lines = (text, size, weight) => {
33506
+ if (measurer === void 0) measurer = typeof document === "undefined" ? null : document.createElement("canvas").getContext("2d");
33507
+ if (!width || !measurer || !box.current) return [text];
33508
+ const px = size / 100 * (width / .92);
33509
+ measurer.font = `${weight} ${px}px ${getComputedStyle(box.current).fontFamily}`;
33510
+ return wrapBalanced(text, (width - px * .9) * .97, (t) => measurer.measureText(t).width);
33511
+ };
33406
33512
  const edge = style.position === "top" ? { top: `${style.offset}%` } : { bottom: `${style.offset}%` };
33407
33513
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
33514
+ ref: box,
33408
33515
  className: "dshl-sub",
33409
33516
  "data-pos": style.position,
33410
33517
  style: {
@@ -33414,7 +33521,7 @@ function SubtitleOverlay({ texts, style, draft }) {
33414
33521
  children: rows.map((l, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
33415
33522
  className: `${i === 0 ? "tgt" : "src"} ${l.key === "src" && draft ? "draft" : ""} ${l.key.startsWith("site:") ? "site" : ""}`,
33416
33523
  style: { fontSize: `${l.size}cqw` },
33417
- children: texts[l.key]
33524
+ children: lines(texts[l.key].trim(), l.size, i === 0 ? 600 : 400).map((ln, k) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [k > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("br", {}), ln] }, k))
33418
33525
  }) }, l.key))
33419
33526
  });
33420
33527
  }
@@ -33429,14 +33536,7 @@ function drawSubtitles(g, W, H, texts, style) {
33429
33536
  const blocks = rows.map((l, i) => {
33430
33537
  const px = l.size / 100 * W;
33431
33538
  g.font = `${i === 0 ? 600 : 400} ${px}px ${family}`;
33432
- const words = [...String(texts[l.key])];
33433
- const lines = [];
33434
- let cur = "";
33435
- for (const ch of words) if (g.measureText(cur + ch).width > W * .88 && cur) {
33436
- lines.push(cur);
33437
- cur = ch.trimStart();
33438
- } else cur += ch;
33439
- if (cur) lines.push(cur);
33539
+ const lines = wrapBalanced(String(texts[l.key]), W * .88, (t) => g.measureText(t).width);
33440
33540
  return {
33441
33541
  px,
33442
33542
  weight: i === 0 ? 600 : 400,
@@ -37389,6 +37489,8 @@ html[data-platform='darwin'] .dshl.page .dshl-top :is(button, input, label, a, .
37389
37489
  .dshl-sub .src { color: #e8e8e8; }
37390
37490
  .dshl-sub .draft { color: #b8b8b8; font-style: italic; }
37391
37491
  .dshl-sub div + div { margin-top: .3em; }
37492
+ /* A line that wraps is split into even halves, not a full line and one orphaned character. */
37493
+ .dshl-sub div { line-break: strict; }
37392
37494
  .dshl-stage:not(.idle) .dshl-sub[data-pos=bottom] { transform: translate(-50%, -64px); }
37393
37495
  .dshl-stage:not(.idle) .dshl-sub[data-pos=top] { transform: translate(-50%, 34px); }
37394
37496
 
package/lib/index.mjs CHANGED
@@ -1157,6 +1157,94 @@ function findOriginal(dir) {
1157
1157
  const f = readdirSync(dir).find((n) => /^original\.(mp4|mkv|webm|m4a|mp3)$/.test(n));
1158
1158
  return f ? join(dir, f) : void 0;
1159
1159
  }
1160
+ // src/wrap.ts
1161
+ /**
1162
+ * Line breaking for subtitles drawn on a canvas (burned into clips and
1163
+ * restreams, screenshots): break where the text allows, never start a line
1164
+ * with closing punctuation, and spread the text evenly over the lines it
1165
+ * needs — so a long line wraps as two halves instead of a full line plus one
1166
+ * orphaned character.
1167
+ */
1168
+ /** Characters that may not begin a line (they stay with the text before them). */
1169
+ const NO_START = new Set(Array.from(",。、;:!?)》」』】〕〉”’…—,.;:!?)]}%"));
1170
+ /** Where a sentence pauses: breaking right after these reads best. */
1171
+ const PAUSE = ",。、;:!?,;:!? ";
1172
+ /** Greedy breaking at `width`: Latin text at spaces, CJK between any two characters. */
1173
+ function greedy(chars, width, measure) {
1174
+ const lines = [];
1175
+ let cur = "";
1176
+ for (const ch of chars) {
1177
+ const next = cur + ch;
1178
+ if (cur && measure(next) > width && !NO_START.has(ch)) {
1179
+ const sp = cur.lastIndexOf(" ");
1180
+ const pause = Math.max(...Array.from(PAUSE).map((p) => cur.lastIndexOf(p)));
1181
+ if (/[A-Za-z0-9]/.test(ch) && /[A-Za-z0-9]$/.test(cur) && sp > cur.length * .3) {
1182
+ lines.push(cur.slice(0, sp));
1183
+ cur = cur.slice(sp + 1) + ch;
1184
+ } else if (pause > cur.length * .6 && pause < cur.length - 1) {
1185
+ lines.push(cur.slice(0, pause + 1).trimEnd());
1186
+ cur = cur.slice(pause + 1).trimStart() + ch;
1187
+ } else {
1188
+ lines.push(cur.trimEnd());
1189
+ cur = ch.trimStart();
1190
+ }
1191
+ } else cur = next;
1192
+ }
1193
+ if (cur.trim()) lines.push(cur.trimEnd());
1194
+ return lines;
1195
+ }
1196
+ /** Wrap `text` to at most `maxWidth`, using as few lines as possible, each about the same length. */
1197
+ function wrapBalanced(text, maxWidth, measure) {
1198
+ const chars = Array.from(text.trim());
1199
+ const first = greedy(chars, maxWidth, measure);
1200
+ if (first.length < 2) return first;
1201
+ let lo = measure(text) / first.length * .9, hi = maxWidth, best = first;
1202
+ for (let i = 0; i < 14 && hi - lo > 1; i++) {
1203
+ const mid = (lo + hi) / 2;
1204
+ const lines = greedy(chars, mid, measure);
1205
+ if (lines.length <= first.length) {
1206
+ best = lines;
1207
+ hi = mid;
1208
+ } else lo = mid;
1209
+ }
1210
+ return atPauses(best, maxWidth, measure);
1211
+ }
1212
+ /** Move each break to a nearby pause (within 4 characters) when both lines still fit. */
1213
+ function atPauses(lines, maxWidth, measure) {
1214
+ const out = lines.map((l) => Array.from(l));
1215
+ for (let k = 0; k + 1 < out.length; k++) {
1216
+ const a = out[k], b = out[k + 1];
1217
+ if (PAUSE.includes(a.at(-1) ?? "")) continue;
1218
+ const sep = /[\x21-\x7e]$/.test(a.join("")) && /^[\x21-\x7e]/.test(b.join("")) ? [" "] : [];
1219
+ let pick;
1220
+ for (let d = 1; d <= 4 && pick === void 0; d++) for (const shift of [d, -d]) {
1221
+ const cut = a.length + shift;
1222
+ if (cut <= 0) continue;
1223
+ const joined = [
1224
+ ...a,
1225
+ ...sep,
1226
+ ...b
1227
+ ];
1228
+ if (cut >= joined.length) continue;
1229
+ if (!PAUSE.includes(joined[cut - 1])) continue;
1230
+ const na = joined.slice(0, cut).join("").trimEnd(), nb = joined.slice(cut).join("").trimStart();
1231
+ if (nb && measure(na) <= maxWidth && measure(nb) <= maxWidth) {
1232
+ pick = shift;
1233
+ break;
1234
+ }
1235
+ }
1236
+ if (pick !== void 0) {
1237
+ const joined = [
1238
+ ...a,
1239
+ ...sep,
1240
+ ...b
1241
+ ], cut = a.length + pick;
1242
+ out[k] = Array.from(joined.slice(0, cut).join("").trimEnd());
1243
+ out[k + 1] = Array.from(joined.slice(cut).join("").trimStart());
1244
+ }
1245
+ }
1246
+ return out.map((l) => l.join(""));
1247
+ }
1160
1248
  // src/restream.ts
1161
1249
  /** Platforms people push to; only a label on our side — the address and key come from the platform's own streaming tool. */
1162
1250
  const PLATFORMS = [
@@ -1247,22 +1335,7 @@ async function drawSubtitles(rows, style, w, h) {
1247
1335
  const blocks = rows.map((r) => {
1248
1336
  const px = Math.round(r.size / 100 * w);
1249
1337
  g.font = `${r.main ? 600 : 500} ${px}px ${FONT}`;
1250
- const lines = [];
1251
- let cur = "";
1252
- for (const ch of Array.from(r.text)) {
1253
- const next = cur + ch;
1254
- if (cur && g.measureText(next).width > maxW - px * .9) {
1255
- const sp = cur.lastIndexOf(" ");
1256
- if (sp > cur.length * .5 && /[A-Za-z]/.test(ch)) {
1257
- lines.push(cur.slice(0, sp));
1258
- cur = cur.slice(sp + 1) + ch;
1259
- } else {
1260
- lines.push(cur);
1261
- cur = ch.trimStart();
1262
- }
1263
- } else cur = next;
1264
- }
1265
- if (cur.trim()) lines.push(cur);
1338
+ const lines = wrapBalanced(r.text, maxW - px * .9, (t) => g.measureText(t).width);
1266
1339
  return {
1267
1340
  px,
1268
1341
  main: r.main,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vidi",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
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",