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.
- package/lib/client.js +117 -15
- package/lib/index.mjs +89 -16
- 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
|
-
|
|
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
|
|
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.
|
|
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",
|