dsh-vidi 0.7.1 → 0.7.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 +164 -11
- package/lib/index.mjs +129 -23
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -255,6 +255,23 @@ const Icon = {
|
|
|
255
255
|
strokeWidth: 2.6,
|
|
256
256
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M5 12.5l4.5 4.5L19 7.5" })
|
|
257
257
|
}),
|
|
258
|
+
sidePanel: () => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
259
|
+
viewBox: "0 0 24 24",
|
|
260
|
+
width: "18",
|
|
261
|
+
height: "18",
|
|
262
|
+
fill: "none",
|
|
263
|
+
stroke: "currentColor",
|
|
264
|
+
strokeWidth: "1.8",
|
|
265
|
+
strokeLinecap: "round",
|
|
266
|
+
strokeLinejoin: "round",
|
|
267
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
268
|
+
x: "3",
|
|
269
|
+
y: "4",
|
|
270
|
+
width: "18",
|
|
271
|
+
height: "16",
|
|
272
|
+
rx: "2.5"
|
|
273
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M15 4v16" })]
|
|
274
|
+
}),
|
|
258
275
|
plus: () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
259
276
|
viewBox: "0 0 24 24",
|
|
260
277
|
width: "14",
|
|
@@ -844,7 +861,9 @@ function Pick(props) {
|
|
|
844
861
|
if (!list.current?.contains(e.target) && !btn.current?.contains(e.target)) close(false);
|
|
845
862
|
};
|
|
846
863
|
const gone = (e) => {
|
|
847
|
-
|
|
864
|
+
const t = e.target;
|
|
865
|
+
if (list.current?.contains(t)) return;
|
|
866
|
+
if (e.type === "resize" || t === document || t === document.documentElement || t instanceof Node && btn.current && t.contains(btn.current)) close(false);
|
|
848
867
|
};
|
|
849
868
|
document.addEventListener("pointerdown", away, true);
|
|
850
869
|
window.addEventListener("resize", gone);
|
|
@@ -35169,6 +35188,80 @@ function upload(file, onProgress) {
|
|
|
35169
35188
|
});
|
|
35170
35189
|
}
|
|
35171
35190
|
/** A value to copy: plain text (not an input look-alike) and a text button. */
|
|
35191
|
+
/** The push stream key: copy it, or change it (typed, or a new random one). */
|
|
35192
|
+
function KeyRow({ value, t, onChanged }) {
|
|
35193
|
+
const [edit, setEdit] = (0, react.useState)(null);
|
|
35194
|
+
const [err, setErr] = (0, react.useState)("");
|
|
35195
|
+
const [done, setDone] = (0, react.useState)(false);
|
|
35196
|
+
const save = async (body) => {
|
|
35197
|
+
if (typeof body.key === "string" && !/^[A-Za-z0-9_-]{3,40}$/.test(body.key.trim())) {
|
|
35198
|
+
setErr(t("push.badKey"));
|
|
35199
|
+
return;
|
|
35200
|
+
}
|
|
35201
|
+
try {
|
|
35202
|
+
await post("pushkey", body);
|
|
35203
|
+
setEdit(null);
|
|
35204
|
+
setErr("");
|
|
35205
|
+
onChanged();
|
|
35206
|
+
} catch (e) {
|
|
35207
|
+
setErr(e.message);
|
|
35208
|
+
}
|
|
35209
|
+
};
|
|
35210
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("push.key") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dd", { children: [edit === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
35211
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
35212
|
+
className: "val",
|
|
35213
|
+
children: value
|
|
35214
|
+
}),
|
|
35215
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
35216
|
+
className: "dshl-link",
|
|
35217
|
+
onClick: () => {
|
|
35218
|
+
navigator.clipboard.writeText(value);
|
|
35219
|
+
setDone(true);
|
|
35220
|
+
setTimeout(() => setDone(false), 1500);
|
|
35221
|
+
},
|
|
35222
|
+
children: done ? t("push.copied") : t("push.copy")
|
|
35223
|
+
}),
|
|
35224
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
35225
|
+
className: "dshl-link",
|
|
35226
|
+
onClick: () => setEdit(value),
|
|
35227
|
+
children: t("push.change")
|
|
35228
|
+
})
|
|
35229
|
+
] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
35230
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
35231
|
+
className: "dshl-keyedit",
|
|
35232
|
+
value: edit,
|
|
35233
|
+
spellCheck: false,
|
|
35234
|
+
autoFocus: true,
|
|
35235
|
+
onFocus: (e) => e.target.select(),
|
|
35236
|
+
onChange: (e) => setEdit(e.target.value),
|
|
35237
|
+
onKeyDown: (e) => {
|
|
35238
|
+
if (e.key === "Enter") save({ key: edit });
|
|
35239
|
+
if (e.key === "Escape") setEdit(null);
|
|
35240
|
+
}
|
|
35241
|
+
}),
|
|
35242
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
35243
|
+
className: "dshl-link",
|
|
35244
|
+
onClick: () => void save({ key: edit }),
|
|
35245
|
+
children: t("push.save")
|
|
35246
|
+
}),
|
|
35247
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
35248
|
+
className: "dshl-link",
|
|
35249
|
+
onClick: () => void save({ random: true }),
|
|
35250
|
+
children: t("push.random")
|
|
35251
|
+
}),
|
|
35252
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
35253
|
+
className: "dshl-link",
|
|
35254
|
+
onClick: () => {
|
|
35255
|
+
setEdit(null);
|
|
35256
|
+
setErr("");
|
|
35257
|
+
},
|
|
35258
|
+
children: t("player.cancel")
|
|
35259
|
+
})
|
|
35260
|
+
] }), err && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
35261
|
+
className: "err",
|
|
35262
|
+
children: err
|
|
35263
|
+
})] })] });
|
|
35264
|
+
}
|
|
35172
35265
|
function CopyRow({ label, value, copy, copied }) {
|
|
35173
35266
|
const [done, setDone] = (0, react.useState)(false);
|
|
35174
35267
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: label }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dd", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -35239,6 +35332,29 @@ function LiveView(props) {
|
|
|
35239
35332
|
const [subStyle, setSubStyle] = (0, react.useState)(() => loadStyle(lines));
|
|
35240
35333
|
const [delay, setDelay] = (0, react.useState)(8);
|
|
35241
35334
|
const [theater, setTheater] = (0, react.useState)(false);
|
|
35335
|
+
const [sideOpen, setSideOpenRaw] = (0, react.useState)(() => store.get("sideOpen", true));
|
|
35336
|
+
const setSideOpen = (v) => {
|
|
35337
|
+
setSideOpenRaw(v);
|
|
35338
|
+
store.set("sideOpen", v);
|
|
35339
|
+
};
|
|
35340
|
+
const [sideW, setSideWRaw] = (0, react.useState)(() => store.get("sideW", 380));
|
|
35341
|
+
const mainRef = (0, react.useRef)(null);
|
|
35342
|
+
const dragSide = (e) => {
|
|
35343
|
+
e.preventDefault();
|
|
35344
|
+
const box = mainRef.current?.getBoundingClientRect();
|
|
35345
|
+
if (!box) return;
|
|
35346
|
+
const move = (ev) => setSideWRaw(Math.round(Math.min(box.width * .6, Math.max(280, box.right - ev.clientX - 8))));
|
|
35347
|
+
const up = () => {
|
|
35348
|
+
window.removeEventListener("pointermove", move);
|
|
35349
|
+
window.removeEventListener("pointerup", up);
|
|
35350
|
+
setSideWRaw((w) => {
|
|
35351
|
+
store.set("sideW", w);
|
|
35352
|
+
return w;
|
|
35353
|
+
});
|
|
35354
|
+
};
|
|
35355
|
+
window.addEventListener("pointermove", move);
|
|
35356
|
+
window.addEventListener("pointerup", up);
|
|
35357
|
+
};
|
|
35242
35358
|
const [tab, setTab] = (0, react.useState)("transcript");
|
|
35243
35359
|
const [query, setQuery] = (0, react.useState)("");
|
|
35244
35360
|
const [trMode, setTrMode] = (0, react.useState)("lines");
|
|
@@ -35739,11 +35855,10 @@ function LiveView(props) {
|
|
|
35739
35855
|
value: armed.rtmp,
|
|
35740
35856
|
copy: t("push.copy"),
|
|
35741
35857
|
copied: t("push.copied")
|
|
35742
|
-
}), armed.key && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
35743
|
-
label: t("push.key"),
|
|
35858
|
+
}), armed.key && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeyRow, {
|
|
35744
35859
|
value: armed.key,
|
|
35745
|
-
|
|
35746
|
-
|
|
35860
|
+
t,
|
|
35861
|
+
onChanged: () => void post("arm", options()).then((a) => setArmed(a.busy ? null : a)).catch(() => {})
|
|
35747
35862
|
})] })]
|
|
35748
35863
|
})
|
|
35749
35864
|
]
|
|
@@ -35951,7 +36066,8 @@ function LiveView(props) {
|
|
|
35951
36066
|
})]
|
|
35952
36067
|
}) : null;
|
|
35953
36068
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
35954
|
-
className: `dshl ${theater && !activeBench ? "theater" : ""} ${pageMode ? "page" : ""} ${activeBench ? "bench" : ""}`,
|
|
36069
|
+
className: `dshl ${theater && !activeBench ? "theater" : ""} ${pageMode ? "page" : ""} ${activeBench ? "bench" : ""} ${pageMode && !sideOpen && !activeBench ? "noside" : ""}`,
|
|
36070
|
+
style: pageMode ? { ["--side-w"]: `${sideW}px` } : void 0,
|
|
35955
36071
|
"data-dsh-own-drop": "dsh-vidi",
|
|
35956
36072
|
onDragEnter: (e) => {
|
|
35957
36073
|
if (e.dataTransfer.types.includes("Files")) e.stopPropagation();
|
|
@@ -36053,6 +36169,13 @@ function LiveView(props) {
|
|
|
36053
36169
|
open: settingsOpen,
|
|
36054
36170
|
onOpen: setSettingsOpen,
|
|
36055
36171
|
t
|
|
36172
|
+
}),
|
|
36173
|
+
pageMode && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
36174
|
+
className: `dshl-ctl icon ${sideOpen ? "on" : ""}`,
|
|
36175
|
+
title: t(sideOpen ? "side.hide" : "side.show"),
|
|
36176
|
+
"aria-pressed": sideOpen,
|
|
36177
|
+
onClick: () => setSideOpen(!sideOpen),
|
|
36178
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.sidePanel, {})
|
|
36056
36179
|
})
|
|
36057
36180
|
]
|
|
36058
36181
|
})
|
|
@@ -36082,6 +36205,7 @@ function LiveView(props) {
|
|
|
36082
36205
|
children: emptyBox
|
|
36083
36206
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("main", {
|
|
36084
36207
|
className: "dshl-main",
|
|
36208
|
+
ref: mainRef,
|
|
36085
36209
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
36086
36210
|
className: "dshl-col",
|
|
36087
36211
|
children: !hasMain || !job ? emptyBox : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
@@ -36233,9 +36357,13 @@ function LiveView(props) {
|
|
|
36233
36357
|
children: transcriptBox
|
|
36234
36358
|
})
|
|
36235
36359
|
] })
|
|
36236
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.
|
|
36360
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
|
|
36237
36361
|
className: "dshl-side",
|
|
36238
|
-
children:
|
|
36362
|
+
children: [pageMode && !activeBench && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
36363
|
+
className: "dshl-resize",
|
|
36364
|
+
onPointerDown: dragSide,
|
|
36365
|
+
title: t("side.resize")
|
|
36366
|
+
}), activeBench && view ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
36239
36367
|
className: "dshl-benchbox",
|
|
36240
36368
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Guard, {
|
|
36241
36369
|
name: `workbench ${activeBench.id}`,
|
|
@@ -36252,7 +36380,7 @@ function LiveView(props) {
|
|
|
36252
36380
|
hasVideo: !!job,
|
|
36253
36381
|
jobsDir,
|
|
36254
36382
|
t
|
|
36255
|
-
}) : transcriptBox
|
|
36383
|
+
}) : transcriptBox]
|
|
36256
36384
|
})]
|
|
36257
36385
|
}),
|
|
36258
36386
|
dragging && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -36823,6 +36951,13 @@ const en = {
|
|
|
36823
36951
|
"rs.overlay": "Subtitles for OBS (Browser Source)",
|
|
36824
36952
|
"rs.overlay.d": "Add a Browser Source in OBS with this address (1920×1080): the newest subtitles on a transparent background, over your camera or any picture. Add ?lines=zh,src&size=4.5 to choose lines and size.",
|
|
36825
36953
|
"rs.copy": "Copy",
|
|
36954
|
+
"push.change": "Change",
|
|
36955
|
+
"push.save": "Save",
|
|
36956
|
+
"push.random": "New random key",
|
|
36957
|
+
"push.badKey": "3–40 letters, digits, - or _",
|
|
36958
|
+
"side.hide": "Hide the chat",
|
|
36959
|
+
"side.show": "Show the chat",
|
|
36960
|
+
"side.resize": "Drag to change the width",
|
|
36826
36961
|
"rs.openPage": "Open",
|
|
36827
36962
|
"rs.screen": "Subtitle screen (16:9)",
|
|
36828
36963
|
"rs.screen.d": "Only the subtitles, large, for a big screen or projector: open it in any browser on this network and go full screen (on this computer: {local}). Add ?theme=light for a light screen; it can also be an OBS source of its own.",
|
|
@@ -37204,6 +37339,13 @@ const zh = {
|
|
|
37204
37339
|
"rs.overlay": "OBS 字幕叠加(浏览器源)",
|
|
37205
37340
|
"rs.overlay.d": "在 OBS 里加一个「浏览器源」,填这个地址(1920×1080):透明背景上只有最新的字幕,叠在摄像头或任何画面上。地址后加 ?lines=zh,src&size=4.5 可以选行和字号。",
|
|
37206
37341
|
"rs.copy": "复制",
|
|
37342
|
+
"push.change": "修改",
|
|
37343
|
+
"push.save": "保存",
|
|
37344
|
+
"push.random": "换一个",
|
|
37345
|
+
"push.badKey": "用 3–40 位字母、数字、- 或 _",
|
|
37346
|
+
"side.hide": "收起对话",
|
|
37347
|
+
"side.show": "展开对话",
|
|
37348
|
+
"side.resize": "拖动调整宽度",
|
|
37207
37349
|
"rs.openPage": "打开",
|
|
37208
37350
|
"rs.screen": "字幕大屏(16:9)",
|
|
37209
37351
|
"rs.screen.d": "只有字幕、字很大,适合大屏和投影:在同一网络里任何浏览器打开、全屏即可(本机用 {local})。地址后加 ?theme=light 换浅色;也可以作为 OBS 的一个独立画面来源。",
|
|
@@ -37700,6 +37842,10 @@ const CSS = `
|
|
|
37700
37842
|
.dshl-pushinfo dl { display: grid; grid-template-columns: auto 1fr; align-items: center; gap: 6px 16px; margin: 0; }
|
|
37701
37843
|
.dshl-pushinfo dt { color: var(--faint); font-size: 12px; }
|
|
37702
37844
|
.dshl-pushinfo dd { display: flex; align-items: center; gap: 14px; min-width: 0; margin: 0; }
|
|
37845
|
+
.dshl-keyedit { width: 140px; height: 26px; padding: 0 8px; border: 1px solid var(--line); border-radius: 6px; background: var(--bg); color: var(--ink); font: 13px ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
37846
|
+
.dshl-keyedit:focus { outline: none; border-color: var(--accent); }
|
|
37847
|
+
.dshl-pushinfo dd:has(.err) { flex-wrap: wrap; row-gap: 4px; }
|
|
37848
|
+
.dshl-pushinfo dd .err { flex-basis: 100%; color: #d1434b; font-size: 12px; }
|
|
37703
37849
|
.dshl-pushinfo .val { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ink); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px; user-select: all; }
|
|
37704
37850
|
.dshl-pushinfo .dshl-link { flex: none; font-size: 12.5px; }
|
|
37705
37851
|
|
|
@@ -37736,12 +37882,18 @@ html[data-platform='darwin'] .dshl-panelwrap .dshl.page .dshl-top { padding-top:
|
|
|
37736
37882
|
html[data-platform='darwin'] .dshl.page .dshl-top { -webkit-app-region: drag; app-region: drag; }
|
|
37737
37883
|
html[data-platform='darwin'] .dshl.page .dshl-top :is(button, input, label, a, .dshl-select, .dshl-pick) { -webkit-app-region: no-drag; app-region: no-drag; }
|
|
37738
37884
|
.dshl.page { display: flex; flex-direction: column; height: 100%; overflow: hidden; }
|
|
37739
|
-
.dshl.page .dshl-main { flex: 1; min-height: 0; grid-template-columns: minmax(0, 1fr) clamp(320px, 30vw, 420px); align-items: stretch; padding-bottom: 16px; }
|
|
37885
|
+
.dshl.page .dshl-main { flex: 1; min-height: 0; grid-template-columns: minmax(0, 1fr) var(--side-w, clamp(320px, 30vw, 420px)); align-items: stretch; padding-bottom: 16px; }
|
|
37740
37886
|
/* Video page: player on top, then the status row, then subtitles/glossary filling what is left; chat on the right. */
|
|
37741
37887
|
.dshl.page .dshl-col { display: flex; flex-direction: column; min-height: 0; container-type: size; }
|
|
37742
37888
|
.dshl.page .dshl-col > .dshl-drop { flex: 1; aspect-ratio: auto; min-height: 0; }
|
|
37743
37889
|
.dshl.page .dshl-stage { flex: none; width: 100%; max-height: max(240px, calc(100cqh - 270px)); }
|
|
37744
|
-
.dshl.page .dshl-side { position:
|
|
37890
|
+
.dshl.page .dshl-side { position: relative; top: auto; height: 100%; max-height: none; }
|
|
37891
|
+
.dshl.page.noside .dshl-main { grid-template-columns: minmax(0, 1fr); }
|
|
37892
|
+
.dshl.page.noside .dshl-side { display: none; }
|
|
37893
|
+
/* Drag the chat panel's left edge to change its width; the player and subtitles take the rest. */
|
|
37894
|
+
.dshl-resize { position: absolute; top: 0; bottom: 0; left: 0; z-index: 3; width: 8px; cursor: col-resize; }
|
|
37895
|
+
.dshl-resize::after { content: ''; position: absolute; top: 50%; left: 2px; width: 3px; height: 36px; border-radius: 2px; background: var(--line); transform: translateY(-50%); opacity: 0; transition: opacity .15s; }
|
|
37896
|
+
.dshl-resize:hover::after { opacity: 1; }
|
|
37745
37897
|
/* Workbench open: the video and its subtitles keep a column on the left; the bench takes the rest. */
|
|
37746
37898
|
.dshl.page.bench .dshl-main { grid-template-columns: clamp(340px, 32vw, 520px) minmax(0, 1fr); }
|
|
37747
37899
|
.dshl-lag { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; margin: -2px 0 8px; padding: 7px 12px; border-radius: 8px; background: var(--l1, rgba(0,0,0,.04)); color: var(--muted); font-size: 12.5px; }
|
|
@@ -37762,6 +37914,7 @@ html[data-platform='darwin'] .dshl.page .dshl-top :is(button, input, label, a, .
|
|
|
37762
37914
|
.dshl.page.theater .dshl-stage { max-height: 100cqh; }
|
|
37763
37915
|
@media (max-width: 1000px) {
|
|
37764
37916
|
.dshl.page { display: block; overflow: auto; }
|
|
37917
|
+
.dshl-resize { display: none; }
|
|
37765
37918
|
.dshl.page.bench .dshl-main { grid-template-columns: minmax(0, 1fr); }
|
|
37766
37919
|
.dshl.page.bench .dshl-side { height: auto; min-height: 0; }
|
|
37767
37920
|
.dshl-benchbox { overflow: visible; }
|
package/lib/index.mjs
CHANGED
|
@@ -2289,11 +2289,28 @@ var LiveJob = class LiveJob extends EventEmitter {
|
|
|
2289
2289
|
}
|
|
2290
2290
|
}, 400);
|
|
2291
2291
|
}
|
|
2292
|
-
|
|
2292
|
+
let rejected = false;
|
|
2293
|
+
ff.stderr.on("data", (d) => {
|
|
2294
|
+
appendFileSync(join(this.deps.dir, "ffmpeg.log"), d);
|
|
2295
|
+
if (this.opts.source === "push" && !rejected && /Unexpected stream/.test(d.toString())) {
|
|
2296
|
+
rejected = true;
|
|
2297
|
+
ff.kill("SIGKILL");
|
|
2298
|
+
}
|
|
2299
|
+
});
|
|
2293
2300
|
ff.on("exit", (code) => {
|
|
2294
2301
|
if (this.ffmpeg !== ff) return;
|
|
2295
2302
|
this.ffmpeg = void 0;
|
|
2296
2303
|
if (this.status !== "running") return;
|
|
2304
|
+
if (this.opts.source === "push" && this.message && (rejected || !this.segments().length)) {
|
|
2305
|
+
if (rejected) rmSync(hls, {
|
|
2306
|
+
recursive: true,
|
|
2307
|
+
force: true
|
|
2308
|
+
});
|
|
2309
|
+
setTimeout(() => {
|
|
2310
|
+
if (this.status === "running" && !this.ffmpeg) this.spawnFfmpeg(input);
|
|
2311
|
+
}, 500);
|
|
2312
|
+
return;
|
|
2313
|
+
}
|
|
2297
2314
|
this.finishCapture(code === 0 ? "ended" : this.ffmpegError(code), code === 0 || this.segments().length > 0);
|
|
2298
2315
|
});
|
|
2299
2316
|
}
|
|
@@ -4725,6 +4742,8 @@ var Stream = class {
|
|
|
4725
4742
|
cjk;
|
|
4726
4743
|
open = [];
|
|
4727
4744
|
done = [];
|
|
4745
|
+
/** When the last piece arrived (wall clock, ms). */
|
|
4746
|
+
lastWall = 0;
|
|
4728
4747
|
constructor(cjk) {
|
|
4729
4748
|
this.cjk = cjk;
|
|
4730
4749
|
}
|
|
@@ -4754,18 +4773,51 @@ var Stream = class {
|
|
|
4754
4773
|
this.push(p);
|
|
4755
4774
|
}
|
|
4756
4775
|
push(p) {
|
|
4776
|
+
this.lastWall = Date.now();
|
|
4757
4777
|
this.open.push(p);
|
|
4758
4778
|
const text = this.text.trim();
|
|
4759
4779
|
const len = this.cjk ? Array.from(text).length : text.length;
|
|
4760
4780
|
const soft = this.cjk ? SOFT_CJK : SOFT_LATIN, hard = this.cjk ? HARD_CJK : HARD_LATIN;
|
|
4761
4781
|
if (END.test(text) || len >= soft && /[,,、;;::]\s*$/.test(text) || len >= hard) this.close();
|
|
4762
4782
|
}
|
|
4783
|
+
/** End the sentence at its last comma or full stop (the rest starts the next one); false when it has none. */
|
|
4784
|
+
closeAtPause() {
|
|
4785
|
+
const text = this.text;
|
|
4786
|
+
let cut = -1;
|
|
4787
|
+
for (let i = text.length - 2; i > 0; i--) if (/[,。、;:!?,;:!?]/.test(text[i])) {
|
|
4788
|
+
cut = i + 1;
|
|
4789
|
+
break;
|
|
4790
|
+
}
|
|
4791
|
+
if (cut <= 0) return false;
|
|
4792
|
+
const head = [], tail = [];
|
|
4793
|
+
let n = 0;
|
|
4794
|
+
for (const p of this.open) {
|
|
4795
|
+
if (n + p.text.length <= cut) head.push(p);
|
|
4796
|
+
else if (n >= cut) tail.push(p);
|
|
4797
|
+
else {
|
|
4798
|
+
head.push({
|
|
4799
|
+
t: p.t,
|
|
4800
|
+
text: p.text.slice(0, cut - n)
|
|
4801
|
+
});
|
|
4802
|
+
tail.push({
|
|
4803
|
+
t: p.t,
|
|
4804
|
+
text: p.text.slice(cut - n)
|
|
4805
|
+
});
|
|
4806
|
+
}
|
|
4807
|
+
n += p.text.length;
|
|
4808
|
+
}
|
|
4809
|
+
this.open = head;
|
|
4810
|
+
this.close();
|
|
4811
|
+
this.open = tail;
|
|
4812
|
+
return true;
|
|
4813
|
+
}
|
|
4763
4814
|
close() {
|
|
4764
4815
|
const text = this.text.trim();
|
|
4765
4816
|
if (text) this.done.push({
|
|
4766
4817
|
text,
|
|
4767
4818
|
start: this.open[0].t,
|
|
4768
|
-
end: this.open.at(-1).t
|
|
4819
|
+
end: this.open.at(-1).t,
|
|
4820
|
+
wall: Date.now()
|
|
4769
4821
|
});
|
|
4770
4822
|
this.open = [];
|
|
4771
4823
|
}
|
|
@@ -4791,6 +4843,11 @@ var OpenAITranslateProvider = class {
|
|
|
4791
4843
|
reconnects = 0;
|
|
4792
4844
|
/** Audio sent so far (s); a session's positions count from `base`. */
|
|
4793
4845
|
sent = 0;
|
|
4846
|
+
/** Audio waiting to be sent: the service listens in real time, so bursts (a recording read from its
|
|
4847
|
+
* start) go up at most a little faster than that. */
|
|
4848
|
+
queue = [];
|
|
4849
|
+
queued = 0;
|
|
4850
|
+
drain;
|
|
4794
4851
|
base = 0;
|
|
4795
4852
|
src = new Stream(false);
|
|
4796
4853
|
tr;
|
|
@@ -4801,6 +4858,8 @@ var OpenAITranslateProvider = class {
|
|
|
4801
4858
|
seq = 0;
|
|
4802
4859
|
/** End of the last finished line (s): everything before it is on screen. */
|
|
4803
4860
|
readyAt = 0;
|
|
4861
|
+
/** Where the last line ends on screen (lines never overlap). */
|
|
4862
|
+
lastEnd = 0;
|
|
4804
4863
|
/** Arrival delay of text after its audio position, for the page (s). */
|
|
4805
4864
|
lag = 0;
|
|
4806
4865
|
connectedAt = 0;
|
|
@@ -4821,6 +4880,7 @@ var OpenAITranslateProvider = class {
|
|
|
4821
4880
|
this.onError = onError;
|
|
4822
4881
|
await this.connect();
|
|
4823
4882
|
this.quiet = setInterval(() => this.settle(), 400);
|
|
4883
|
+
this.drain = setInterval(() => this.sendDue(), 100);
|
|
4824
4884
|
}
|
|
4825
4885
|
connect() {
|
|
4826
4886
|
return new Promise((resolve, reject) => {
|
|
@@ -4874,35 +4934,52 @@ var OpenAITranslateProvider = class {
|
|
|
4874
4934
|
}
|
|
4875
4935
|
push(pcm) {
|
|
4876
4936
|
if (this.stopped) return;
|
|
4877
|
-
this.
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4937
|
+
this.queue.push(Buffer.from(pcm));
|
|
4938
|
+
this.queued += pcm.length / BYTES_PER_SEC;
|
|
4939
|
+
this.sendDue();
|
|
4940
|
+
}
|
|
4941
|
+
/** Send queued audio, everything including silence, no faster than 1.5× real time since the session began. */
|
|
4942
|
+
sendDue() {
|
|
4943
|
+
if (this.ws?.readyState !== WebSocket.OPEN) return;
|
|
4944
|
+
const allowed = this.base + (Date.now() - this.connectedAt) / 1e3 * 1.5 + 2;
|
|
4945
|
+
while (this.queue.length && this.sent < allowed) {
|
|
4946
|
+
const pcm = this.queue.shift();
|
|
4947
|
+
this.queued -= pcm.length / BYTES_PER_SEC;
|
|
4948
|
+
this.send({
|
|
4949
|
+
type: "session.input_audio_buffer.append",
|
|
4950
|
+
audio: this.up.push(pcm).toString("base64")
|
|
4951
|
+
});
|
|
4952
|
+
this.sent += pcm.length / BYTES_PER_SEC;
|
|
4953
|
+
}
|
|
4882
4954
|
}
|
|
4883
|
-
/** Audio
|
|
4955
|
+
/** Audio received but not yet in a finished line (queued, or sent and still being worked on). */
|
|
4884
4956
|
backlog() {
|
|
4885
|
-
return Math.max(0, this.sent - this.readyAt);
|
|
4957
|
+
return this.queued + Math.max(0, this.sent - this.readyAt);
|
|
4886
4958
|
}
|
|
4887
4959
|
onEvent(ev) {
|
|
4888
4960
|
if (ev.type === "session.input_transcript.delta" || ev.type === "session.output_transcript.delta") {
|
|
4889
4961
|
const t = this.base + (ev.elapsed_ms ?? 0) / 1e3;
|
|
4890
|
-
const
|
|
4891
|
-
this.lag = this.lag ? this.lag * .9 +
|
|
4962
|
+
const behind = Math.max(0, this.sent - t);
|
|
4963
|
+
this.lag = this.lag ? this.lag * .9 + behind * .1 : behind;
|
|
4892
4964
|
const piece = {
|
|
4893
4965
|
t,
|
|
4894
4966
|
text: String(ev.delta ?? "")
|
|
4895
4967
|
};
|
|
4896
4968
|
if (ev.type === "session.input_transcript.delta") this.src.add(piece);
|
|
4897
|
-
else
|
|
4969
|
+
else {
|
|
4970
|
+
const next = this.src.done[this.srcUsed];
|
|
4971
|
+
if (next && this.tr.text.trim() && t - next.end >= 1.2) {
|
|
4972
|
+
if (!this.tr.closeAtPause() && t - next.end >= 3) this.tr.close();
|
|
4973
|
+
}
|
|
4974
|
+
this.tr.add(piece);
|
|
4975
|
+
}
|
|
4898
4976
|
this.pair(false);
|
|
4899
4977
|
this.draft();
|
|
4900
4978
|
} else if (ev.type === "error") this.onError?.(/* @__PURE__ */ new Error(`OpenAI: ${ev.error?.message ?? "error"}`));
|
|
4901
4979
|
}
|
|
4902
4980
|
/** Close streams that went quiet (a pause without punctuation), then pair what is complete. */
|
|
4903
4981
|
settle() {
|
|
4904
|
-
const
|
|
4905
|
-
for (const s of [this.src, this.tr]) if (s.last !== void 0 && now - s.last > QUIET_SEC + this.lag) s.close();
|
|
4982
|
+
for (const s of [this.src, this.tr]) if (s.last !== void 0 && Date.now() - s.lastWall > QUIET_SEC * 1e3) s.close();
|
|
4906
4983
|
this.pair(false);
|
|
4907
4984
|
this.draft();
|
|
4908
4985
|
}
|
|
@@ -4913,7 +4990,10 @@ var OpenAITranslateProvider = class {
|
|
|
4913
4990
|
*/
|
|
4914
4991
|
pair(all) {
|
|
4915
4992
|
while (this.tr.done.length) {
|
|
4916
|
-
const t = this.tr.done
|
|
4993
|
+
const t = this.tr.done[0];
|
|
4994
|
+
const srcReady = this.srcUsed < this.src.done.length && this.src.done[this.srcUsed].end <= t.end + .5;
|
|
4995
|
+
if (!all && !srcReady && this.src.text.trim() && Date.now() - t.wall < 1500) break;
|
|
4996
|
+
this.tr.done.shift();
|
|
4917
4997
|
const want = Array.from(t.text).length * this.ratio;
|
|
4918
4998
|
const src = [];
|
|
4919
4999
|
let len = 0;
|
|
@@ -4936,28 +5016,37 @@ var OpenAITranslateProvider = class {
|
|
|
4936
5016
|
}
|
|
4937
5017
|
line(t, src) {
|
|
4938
5018
|
const srcText = src.map((s) => s.text).join(" ").trim();
|
|
4939
|
-
const
|
|
4940
|
-
|
|
5019
|
+
const spoken = src.length ? {
|
|
5020
|
+
start: src[0].start - .6,
|
|
5021
|
+
end: src.at(-1).end - .2
|
|
5022
|
+
} : {
|
|
5023
|
+
start: (t?.start ?? 0) - 1.4,
|
|
5024
|
+
end: (t?.end ?? 0) - .8
|
|
5025
|
+
};
|
|
5026
|
+
const start = Math.max(0, this.lastEnd, spoken.start);
|
|
5027
|
+
const end = Math.max(start + 1, spoken.end);
|
|
5028
|
+
this.lastEnd = end;
|
|
4941
5029
|
this.emit?.({
|
|
4942
5030
|
key: `ot:${this.seq++}`,
|
|
4943
5031
|
start,
|
|
4944
|
-
end
|
|
5032
|
+
end,
|
|
4945
5033
|
src: srcText,
|
|
4946
5034
|
tr: t ? { [this.opts.target]: t.text } : void 0,
|
|
4947
5035
|
final: true
|
|
4948
5036
|
});
|
|
4949
|
-
this.readyAt = Math.max(this.readyAt, end);
|
|
5037
|
+
this.readyAt = Math.max(this.readyAt, t?.end ?? end, end);
|
|
4950
5038
|
}
|
|
4951
5039
|
/** The sentence being translated, as a draft line. */
|
|
4952
5040
|
draft() {
|
|
4953
5041
|
const tr = this.tr.text.trim();
|
|
4954
5042
|
if (!tr) return;
|
|
4955
5043
|
const srcText = [...this.src.done.slice(this.srcUsed).map((s) => s.text), this.src.text.trim()].filter(Boolean).join(" ");
|
|
4956
|
-
const
|
|
5044
|
+
const from = this.src.done[this.srcUsed]?.start ?? this.src.start;
|
|
5045
|
+
const start = Math.max(0, this.lastEnd, from !== void 0 ? from - .6 : (this.tr.start ?? 0) - 1.4);
|
|
4957
5046
|
this.emit?.({
|
|
4958
5047
|
key: `ot:${this.seq}`,
|
|
4959
5048
|
start,
|
|
4960
|
-
end: Math.max(this.tr.last ?? start
|
|
5049
|
+
end: Math.max(start + 1, (this.src.last ?? this.tr.last ?? start) - .2),
|
|
4961
5050
|
src: srcText,
|
|
4962
5051
|
tr: { [this.opts.target]: tr },
|
|
4963
5052
|
final: false
|
|
@@ -4970,6 +5059,7 @@ var OpenAITranslateProvider = class {
|
|
|
4970
5059
|
async stop() {
|
|
4971
5060
|
await new Promise((r) => setTimeout(r, 2500));
|
|
4972
5061
|
if (this.quiet) clearInterval(this.quiet);
|
|
5062
|
+
if (this.drain) clearInterval(this.drain);
|
|
4973
5063
|
this.src.close();
|
|
4974
5064
|
this.tr.close();
|
|
4975
5065
|
this.pair(true);
|
|
@@ -5807,7 +5897,14 @@ function apply(ctx, config) {
|
|
|
5807
5897
|
* page owns it.
|
|
5808
5898
|
*/
|
|
5809
5899
|
let armed;
|
|
5810
|
-
const
|
|
5900
|
+
const hadSettings = existsSync(settingsFile);
|
|
5901
|
+
const pushKey = () => {
|
|
5902
|
+
const saved = prefs().pushKey;
|
|
5903
|
+
if (saved) return saved;
|
|
5904
|
+
const key = hadSettings ? "dsh" : Math.random().toString(36).slice(2, 10);
|
|
5905
|
+
savePrefs({ pushKey: key });
|
|
5906
|
+
return key;
|
|
5907
|
+
};
|
|
5811
5908
|
async function disarm() {
|
|
5812
5909
|
const a = armed;
|
|
5813
5910
|
armed = void 0;
|
|
@@ -5879,7 +5976,7 @@ function apply(ctx, config) {
|
|
|
5879
5976
|
host: lanAddress(),
|
|
5880
5977
|
rtmpPort: config.rtmpPort,
|
|
5881
5978
|
srtPort: config.srtPort,
|
|
5882
|
-
key: waiting ?
|
|
5979
|
+
key: waiting ? pushKey() : void 0
|
|
5883
5980
|
},
|
|
5884
5981
|
cookies: () => prefs().cookies
|
|
5885
5982
|
};
|
|
@@ -6109,6 +6206,15 @@ function apply(ctx, config) {
|
|
|
6109
6206
|
}
|
|
6110
6207
|
return send(res, 200, job.summary());
|
|
6111
6208
|
}
|
|
6209
|
+
if (route === "pushkey" && req.method === "POST") {
|
|
6210
|
+
if ([...jobs.values()].some((j) => j.opts.source === "push" && j.status === "running" && !j.message)) return send(res, 409, { error: "a stream is coming in; change the key after it ends" });
|
|
6211
|
+
const b = await readJson(req);
|
|
6212
|
+
const key = b.random ? Math.random().toString(36).slice(2, 10) : String(b.key ?? "").trim();
|
|
6213
|
+
if (!/^[A-Za-z0-9_-]{3,40}$/.test(key)) return send(res, 400, { error: "use 3–40 letters, digits, - or _" });
|
|
6214
|
+
savePrefs({ pushKey: key });
|
|
6215
|
+
await disarm();
|
|
6216
|
+
return send(res, 200, { key });
|
|
6217
|
+
}
|
|
6112
6218
|
if (route === "arm" && req.method === "POST") {
|
|
6113
6219
|
const b = await readJson(req);
|
|
6114
6220
|
return send(res, 200, await arm({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vidi",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.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",
|