dsh-vidi 0.1.0 → 0.2.0
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/LICENSE.md +77 -0
- package/README.md +6 -9
- package/README.zh-CN.md +6 -9
- package/icon.svg +12 -0
- package/lib/client.js +542 -36
- package/lib/index.mjs +688 -11
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +28 -6
package/lib/client.js
CHANGED
|
@@ -254,6 +254,13 @@ const Icon = {
|
|
|
254
254
|
...P,
|
|
255
255
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M12 5v14M5 12h14" })
|
|
256
256
|
}),
|
|
257
|
+
pencil: () => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
258
|
+
viewBox: "0 0 24 24",
|
|
259
|
+
width: "16",
|
|
260
|
+
height: "16",
|
|
261
|
+
...P,
|
|
262
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4 20h4L19 9l-4-4L4 16z" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M13.5 6.5l4 4" })]
|
|
263
|
+
}),
|
|
257
264
|
close: () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
258
265
|
viewBox: "0 0 24 24",
|
|
259
266
|
width: "16",
|
|
@@ -464,7 +471,7 @@ function Markdown({ text, fileUrl }) {
|
|
|
464
471
|
}
|
|
465
472
|
|
|
466
473
|
// src/client/ChatPanel.tsx
|
|
467
|
-
const CLOCK$
|
|
474
|
+
const CLOCK$2 = (s) => {
|
|
468
475
|
const h = Math.floor(s / 3600), m = Math.floor(s / 60) % 60, ss = Math.floor(s % 60);
|
|
469
476
|
return h ? `${h}:${String(m).padStart(2, "0")}:${String(ss).padStart(2, "0")}` : `${m}:${String(ss).padStart(2, "0")}`;
|
|
470
477
|
};
|
|
@@ -520,8 +527,8 @@ function ChatPanel(props) {
|
|
|
520
527
|
setDraft(text);
|
|
521
528
|
setTimeout(() => box.current?.focus(), 0);
|
|
522
529
|
};
|
|
523
|
-
const at = CLOCK$
|
|
524
|
-
const around = `${CLOCK$
|
|
530
|
+
const at = CLOCK$2(now);
|
|
531
|
+
const around = `${CLOCK$2(Math.max(0, now - 30))}–${CLOCK$2(now + 30)}`;
|
|
525
532
|
const rows = nodes.flatMap((n, i) => {
|
|
526
533
|
if (n.kind === "user") {
|
|
527
534
|
const text = textOf(n.content);
|
|
@@ -33851,6 +33858,350 @@ const Player = (0, react.forwardRef)(function Player(props, ref) {
|
|
|
33851
33858
|
});
|
|
33852
33859
|
});
|
|
33853
33860
|
|
|
33861
|
+
// src/client/RestreamPanel.tsx
|
|
33862
|
+
/** Platforms, in the order people in China most often push to; YouTube and Twitch have one fixed ingest address. */
|
|
33863
|
+
const PLATFORMS = [
|
|
33864
|
+
"bilibili",
|
|
33865
|
+
"douyin",
|
|
33866
|
+
"kuaishou",
|
|
33867
|
+
"wechat",
|
|
33868
|
+
"douyu",
|
|
33869
|
+
"huya",
|
|
33870
|
+
"yy",
|
|
33871
|
+
"cc",
|
|
33872
|
+
"youtube",
|
|
33873
|
+
"twitch",
|
|
33874
|
+
"custom"
|
|
33875
|
+
];
|
|
33876
|
+
const FIXED = {
|
|
33877
|
+
youtube: "rtmp://a.rtmp.youtube.com/live2",
|
|
33878
|
+
twitch: "rtmp://live.twitch.tv/app"
|
|
33879
|
+
};
|
|
33880
|
+
async function post$2(path, body) {
|
|
33881
|
+
const r = await fetch(`/live/api/${path}`, {
|
|
33882
|
+
method: "POST",
|
|
33883
|
+
headers: { "content-type": "application/json" },
|
|
33884
|
+
body: JSON.stringify(body)
|
|
33885
|
+
});
|
|
33886
|
+
const j = await r.json().catch(() => ({}));
|
|
33887
|
+
if (!r.ok) throw new Error(j.error ?? `HTTP ${r.status}`);
|
|
33888
|
+
return j;
|
|
33889
|
+
}
|
|
33890
|
+
const CLOCK$1 = (s) => {
|
|
33891
|
+
const h = Math.floor(s / 3600), m = Math.floor(s / 60) % 60, ss = Math.floor(s % 60);
|
|
33892
|
+
return `${h ? `${h}:` : ""}${String(m).padStart(h ? 2 : 1, "0")}:${String(ss).padStart(2, "0")}`;
|
|
33893
|
+
};
|
|
33894
|
+
const host = (s) => s.replace(/^[a-z]+:\/\//i, "").split("/")[0] ?? s;
|
|
33895
|
+
/**
|
|
33896
|
+
* The player's restream menu: saved destinations (tick the ones to push to),
|
|
33897
|
+
* adding one (platform, server address, stream key — copied from the
|
|
33898
|
+
* platform's own streaming tool), subtitles drawn in or not, picture size,
|
|
33899
|
+
* then start. While running, each destination shows its own state.
|
|
33900
|
+
*/
|
|
33901
|
+
function RestreamPanel(props) {
|
|
33902
|
+
const { jobId, status, now, style, t } = props;
|
|
33903
|
+
const running = !!status?.running;
|
|
33904
|
+
const [targets, setTargets] = (0, react.useState)([]);
|
|
33905
|
+
const [picked, setPicked] = (0, react.useState)(() => {
|
|
33906
|
+
try {
|
|
33907
|
+
return JSON.parse(localStorage.getItem("dsh-live.rsPicked") ?? "[]");
|
|
33908
|
+
} catch {
|
|
33909
|
+
return [];
|
|
33910
|
+
}
|
|
33911
|
+
});
|
|
33912
|
+
const [editing, setEditing] = (0, react.useState)(null);
|
|
33913
|
+
const [burn, setBurn] = (0, react.useState)(true);
|
|
33914
|
+
const [height, setHeight] = (0, react.useState)(720);
|
|
33915
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
33916
|
+
const [error, setError] = (0, react.useState)("");
|
|
33917
|
+
(0, react.useEffect)(() => {
|
|
33918
|
+
fetch("/live/api/restream/targets").then((r) => r.json()).then((j) => {
|
|
33919
|
+
const list = j.targets ?? [];
|
|
33920
|
+
setTargets(list);
|
|
33921
|
+
if (!list.length) setEditing({
|
|
33922
|
+
platform: "bilibili",
|
|
33923
|
+
server: "",
|
|
33924
|
+
key: ""
|
|
33925
|
+
});
|
|
33926
|
+
}).catch(() => {});
|
|
33927
|
+
}, []);
|
|
33928
|
+
(0, react.useEffect)(() => {
|
|
33929
|
+
try {
|
|
33930
|
+
localStorage.setItem("dsh-live.rsPicked", JSON.stringify(picked));
|
|
33931
|
+
} catch {}
|
|
33932
|
+
}, [picked]);
|
|
33933
|
+
const [, setTick] = (0, react.useState)(0);
|
|
33934
|
+
(0, react.useEffect)(() => {
|
|
33935
|
+
if (!running) return;
|
|
33936
|
+
const id = setInterval(() => setTick((n) => n + 1), 1e3);
|
|
33937
|
+
return () => clearInterval(id);
|
|
33938
|
+
}, [running]);
|
|
33939
|
+
/** Known notes from the host, in the page's language. */
|
|
33940
|
+
const note = (m) => m === "ended" ? t("rs.m.ended") : m === "no-burn" ? t("rs.m.noBurn") : m;
|
|
33941
|
+
const pname = (p) => t(`rs.p.${p}`);
|
|
33942
|
+
const save = async () => {
|
|
33943
|
+
if (!editing) return;
|
|
33944
|
+
setBusy(true);
|
|
33945
|
+
setError("");
|
|
33946
|
+
try {
|
|
33947
|
+
const list = (await post$2("restream/targets", { target: editing })).targets;
|
|
33948
|
+
setTargets(list);
|
|
33949
|
+
const added = editing.id ?? list.at(-1)?.id;
|
|
33950
|
+
if (added && !picked.includes(added)) setPicked((v) => [...v, added]);
|
|
33951
|
+
setEditing(null);
|
|
33952
|
+
} catch (e) {
|
|
33953
|
+
setError(e.message);
|
|
33954
|
+
} finally {
|
|
33955
|
+
setBusy(false);
|
|
33956
|
+
}
|
|
33957
|
+
};
|
|
33958
|
+
const remove = async (id) => {
|
|
33959
|
+
const j = await post$2("restream/targets", { remove: id }).catch(() => null);
|
|
33960
|
+
if (j) {
|
|
33961
|
+
setTargets(j.targets);
|
|
33962
|
+
setPicked((v) => v.filter((x) => x !== id));
|
|
33963
|
+
}
|
|
33964
|
+
};
|
|
33965
|
+
const start = async () => {
|
|
33966
|
+
setBusy(true);
|
|
33967
|
+
setError("");
|
|
33968
|
+
try {
|
|
33969
|
+
await post$2("restream", {
|
|
33970
|
+
jobId,
|
|
33971
|
+
action: "start",
|
|
33972
|
+
targets: picked.filter((id) => targets.some((x) => x.id === id)),
|
|
33973
|
+
from: now,
|
|
33974
|
+
burn,
|
|
33975
|
+
height,
|
|
33976
|
+
style
|
|
33977
|
+
});
|
|
33978
|
+
} catch (e) {
|
|
33979
|
+
setError(e.message);
|
|
33980
|
+
} finally {
|
|
33981
|
+
setBusy(false);
|
|
33982
|
+
}
|
|
33983
|
+
};
|
|
33984
|
+
const stop = async (targetId) => {
|
|
33985
|
+
setBusy(true);
|
|
33986
|
+
try {
|
|
33987
|
+
await post$2("restream", {
|
|
33988
|
+
jobId,
|
|
33989
|
+
action: "stop",
|
|
33990
|
+
targetId
|
|
33991
|
+
});
|
|
33992
|
+
} catch {} finally {
|
|
33993
|
+
setBusy(false);
|
|
33994
|
+
}
|
|
33995
|
+
};
|
|
33996
|
+
const add = async (id) => {
|
|
33997
|
+
await post$2("restream", {
|
|
33998
|
+
jobId,
|
|
33999
|
+
action: "start",
|
|
34000
|
+
targets: [id],
|
|
34001
|
+
from: now,
|
|
34002
|
+
burn,
|
|
34003
|
+
height,
|
|
34004
|
+
style
|
|
34005
|
+
}).catch((e) => setError(e.message));
|
|
34006
|
+
};
|
|
34007
|
+
const stateOf = (id) => running ? status.targets.find((x) => x.id === id) : void 0;
|
|
34008
|
+
const chosen = picked.filter((id) => targets.some((x) => x.id === id));
|
|
34009
|
+
if (editing) {
|
|
34010
|
+
const fixed = FIXED[editing.platform];
|
|
34011
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
34012
|
+
className: "dshl-rs",
|
|
34013
|
+
children: [
|
|
34014
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
34015
|
+
className: "rs-head",
|
|
34016
|
+
children: editing.id ? t("rs.edit") : t("rs.add")
|
|
34017
|
+
}),
|
|
34018
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
34019
|
+
className: "rs-field",
|
|
34020
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("rs.platform") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
34021
|
+
value: editing.platform,
|
|
34022
|
+
onChange: (e) => setEditing({
|
|
34023
|
+
...editing,
|
|
34024
|
+
platform: e.target.value,
|
|
34025
|
+
server: FIXED[e.target.value] ?? (FIXED[editing.platform] === editing.server ? "" : editing.server)
|
|
34026
|
+
}),
|
|
34027
|
+
children: PLATFORMS.map((p) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
34028
|
+
value: p,
|
|
34029
|
+
children: pname(p)
|
|
34030
|
+
}, p))
|
|
34031
|
+
})]
|
|
34032
|
+
}),
|
|
34033
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
34034
|
+
className: "rs-hint",
|
|
34035
|
+
children: t(`rs.h.${editing.platform}`)
|
|
34036
|
+
}),
|
|
34037
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
34038
|
+
className: "rs-field",
|
|
34039
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("rs.server") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
34040
|
+
type: "text",
|
|
34041
|
+
spellCheck: false,
|
|
34042
|
+
placeholder: fixed ?? "rtmp://…",
|
|
34043
|
+
value: editing.server,
|
|
34044
|
+
onChange: (e) => setEditing({
|
|
34045
|
+
...editing,
|
|
34046
|
+
server: e.target.value
|
|
34047
|
+
})
|
|
34048
|
+
})]
|
|
34049
|
+
}),
|
|
34050
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
34051
|
+
className: "rs-field",
|
|
34052
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("rs.key") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
34053
|
+
type: "password",
|
|
34054
|
+
autoComplete: "off",
|
|
34055
|
+
spellCheck: false,
|
|
34056
|
+
placeholder: editing.id ? t("rs.keyKeep") : t("rs.keyPh"),
|
|
34057
|
+
value: editing.key,
|
|
34058
|
+
onChange: (e) => setEditing({
|
|
34059
|
+
...editing,
|
|
34060
|
+
key: e.target.value
|
|
34061
|
+
})
|
|
34062
|
+
})]
|
|
34063
|
+
}),
|
|
34064
|
+
error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
34065
|
+
className: "rs-err",
|
|
34066
|
+
children: error
|
|
34067
|
+
}),
|
|
34068
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
34069
|
+
className: "rs-actions",
|
|
34070
|
+
children: [targets.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
34071
|
+
className: "dshl-btn small",
|
|
34072
|
+
onClick: () => {
|
|
34073
|
+
setEditing(null);
|
|
34074
|
+
setError("");
|
|
34075
|
+
},
|
|
34076
|
+
children: t("player.cancel")
|
|
34077
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
34078
|
+
className: "dshl-btn small primary",
|
|
34079
|
+
disabled: busy || !editing.server.trim(),
|
|
34080
|
+
onClick: () => void save(),
|
|
34081
|
+
children: t("rs.save")
|
|
34082
|
+
})]
|
|
34083
|
+
})
|
|
34084
|
+
]
|
|
34085
|
+
});
|
|
34086
|
+
}
|
|
34087
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
34088
|
+
className: "dshl-rs",
|
|
34089
|
+
children: [
|
|
34090
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
34091
|
+
className: "rs-head",
|
|
34092
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: running ? t("rs.running").replace("{t}", CLOCK$1((Date.now() - status.startedAt) / 1e3)) : t("rs.title") }), !running && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
34093
|
+
className: "dshl-link",
|
|
34094
|
+
onClick: () => setEditing({
|
|
34095
|
+
platform: "bilibili",
|
|
34096
|
+
server: "",
|
|
34097
|
+
key: ""
|
|
34098
|
+
}),
|
|
34099
|
+
children: ["+ ", t("rs.add")]
|
|
34100
|
+
})]
|
|
34101
|
+
}),
|
|
34102
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
34103
|
+
className: "rs-list",
|
|
34104
|
+
children: targets.map((x) => {
|
|
34105
|
+
const st = stateOf(x.id);
|
|
34106
|
+
const on = running ? !!st : picked.includes(x.id);
|
|
34107
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
34108
|
+
className: `rs-row ${on ? "on" : ""}`,
|
|
34109
|
+
children: [
|
|
34110
|
+
running ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: `rs-dot ${st?.state ?? "off"}` }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
34111
|
+
type: "checkbox",
|
|
34112
|
+
checked: on,
|
|
34113
|
+
onChange: (e) => setPicked((v) => e.target.checked ? [...v, x.id] : v.filter((id) => id !== x.id))
|
|
34114
|
+
}),
|
|
34115
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
34116
|
+
className: "rs-name",
|
|
34117
|
+
children: [pname(x.platform), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: host(x.server) })]
|
|
34118
|
+
}),
|
|
34119
|
+
running ? st ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
34120
|
+
className: `rs-state ${st.state}`,
|
|
34121
|
+
title: st.message,
|
|
34122
|
+
children: t(`rs.s.${st.state}`)
|
|
34123
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
34124
|
+
className: "dshl-link",
|
|
34125
|
+
onClick: () => void add(x.id),
|
|
34126
|
+
children: t("rs.join")
|
|
34127
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
34128
|
+
className: "rs-tools",
|
|
34129
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
34130
|
+
className: "ghost",
|
|
34131
|
+
title: t("rs.edit"),
|
|
34132
|
+
onClick: () => setEditing({
|
|
34133
|
+
id: x.id,
|
|
34134
|
+
platform: x.platform,
|
|
34135
|
+
server: x.server,
|
|
34136
|
+
key: ""
|
|
34137
|
+
}),
|
|
34138
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.pencil, {})
|
|
34139
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
34140
|
+
className: "ghost",
|
|
34141
|
+
title: t("rs.remove"),
|
|
34142
|
+
onClick: () => void remove(x.id),
|
|
34143
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.close, {})
|
|
34144
|
+
})]
|
|
34145
|
+
}),
|
|
34146
|
+
running && st && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
34147
|
+
className: "ghost",
|
|
34148
|
+
title: t("rs.stopOne"),
|
|
34149
|
+
onClick: () => void stop(x.id),
|
|
34150
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.close, {})
|
|
34151
|
+
})
|
|
34152
|
+
]
|
|
34153
|
+
}, x.id);
|
|
34154
|
+
})
|
|
34155
|
+
}),
|
|
34156
|
+
running && status.targets.some((x) => x.state === "error" || x.state === "retrying") && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
34157
|
+
className: "rs-err",
|
|
34158
|
+
children: status.targets.filter((x) => x.message && x.state !== "live").map((x) => `${pname(x.platform)}:${x.message}`).join(";")
|
|
34159
|
+
}),
|
|
34160
|
+
!running && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
34161
|
+
className: "rs-opts",
|
|
34162
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
34163
|
+
className: "rs-check",
|
|
34164
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
34165
|
+
type: "checkbox",
|
|
34166
|
+
checked: burn,
|
|
34167
|
+
onChange: (e) => setBurn(e.target.checked)
|
|
34168
|
+
}), t("rs.burn")]
|
|
34169
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
34170
|
+
className: "seg",
|
|
34171
|
+
children: [720, 1080].map((h) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
34172
|
+
className: height === h ? "on" : "",
|
|
34173
|
+
onClick: () => setHeight(h),
|
|
34174
|
+
children: [h, "p"]
|
|
34175
|
+
}, h))
|
|
34176
|
+
})]
|
|
34177
|
+
}),
|
|
34178
|
+
status?.message && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
34179
|
+
className: "rs-hint",
|
|
34180
|
+
children: note(status.message)
|
|
34181
|
+
}),
|
|
34182
|
+
error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
34183
|
+
className: "rs-err",
|
|
34184
|
+
children: error
|
|
34185
|
+
}),
|
|
34186
|
+
running ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
34187
|
+
className: "dshl-btn block rs-stop",
|
|
34188
|
+
disabled: busy,
|
|
34189
|
+
onClick: () => void stop(),
|
|
34190
|
+
children: t("rs.stop")
|
|
34191
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
34192
|
+
className: "dshl-btn block primary",
|
|
34193
|
+
disabled: busy || !chosen.length,
|
|
34194
|
+
onClick: () => void start(),
|
|
34195
|
+
children: chosen.length ? t("rs.start").replace("{n}", String(chosen.length)) : t("rs.pick")
|
|
34196
|
+
}),
|
|
34197
|
+
!running && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
34198
|
+
className: "rs-hint",
|
|
34199
|
+
children: t("rs.note")
|
|
34200
|
+
})
|
|
34201
|
+
]
|
|
34202
|
+
});
|
|
34203
|
+
}
|
|
34204
|
+
|
|
33854
34205
|
// src/client/Settings.tsx
|
|
33855
34206
|
const engineLabel = (t, id) => {
|
|
33856
34207
|
const v = t(`engine.${id}`);
|
|
@@ -33935,7 +34286,7 @@ function Settings(props) {
|
|
|
33935
34286
|
setBusy(false);
|
|
33936
34287
|
}
|
|
33937
34288
|
};
|
|
33938
|
-
const from = key.source === "dsh" ? t("set.key.fromDsh") : key.source === "env" ? t("set.key.fromEnv") : "";
|
|
34289
|
+
const from = key.source === "dsh" ? key.provider ? t("set.key.fromDshP").replace("{p}", key.provider) : t("set.key.fromDsh") : key.source === "env" ? t("set.key.fromEnv") : "";
|
|
33939
34290
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
33940
34291
|
className: "dshl-pick",
|
|
33941
34292
|
ref: box,
|
|
@@ -35026,36 +35377,53 @@ function LiveView(props) {
|
|
|
35026
35377
|
onResume: () => startSubs(),
|
|
35027
35378
|
t
|
|
35028
35379
|
}),
|
|
35029
|
-
menus: [
|
|
35030
|
-
|
|
35031
|
-
|
|
35032
|
-
|
|
35033
|
-
|
|
35034
|
-
|
|
35035
|
-
|
|
35036
|
-
|
|
35037
|
-
|
|
35038
|
-
|
|
35039
|
-
|
|
35040
|
-
|
|
35041
|
-
|
|
35042
|
-
|
|
35043
|
-
|
|
35044
|
-
|
|
35045
|
-
|
|
35046
|
-
|
|
35047
|
-
|
|
35048
|
-
|
|
35049
|
-
|
|
35050
|
-
|
|
35051
|
-
|
|
35052
|
-
|
|
35053
|
-
|
|
35054
|
-
|
|
35055
|
-
|
|
35056
|
-
|
|
35057
|
-
|
|
35058
|
-
|
|
35380
|
+
menus: [
|
|
35381
|
+
{
|
|
35382
|
+
id: "cc",
|
|
35383
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.cc, {}),
|
|
35384
|
+
title: t("menu.cc"),
|
|
35385
|
+
label: `${t("player.cc")} · ${lines.filter((l) => !l.startsWith("site:") || siteChips.some((c) => c.key === l)).map((k) => siteChips.find((c) => c.key === k)?.label ?? langName(k)).join(" · ")}`,
|
|
35386
|
+
content: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SubtitleMenu, {
|
|
35387
|
+
lines,
|
|
35388
|
+
onLines: (l) => void changeLines(l),
|
|
35389
|
+
options: translatable,
|
|
35390
|
+
site: siteChips,
|
|
35391
|
+
name: nameWithSpoken,
|
|
35392
|
+
style: subStyle,
|
|
35393
|
+
onStyle: setSubStyle,
|
|
35394
|
+
t
|
|
35395
|
+
}) })
|
|
35396
|
+
},
|
|
35397
|
+
{
|
|
35398
|
+
id: "restream",
|
|
35399
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.push, {}),
|
|
35400
|
+
title: t("menu.restream"),
|
|
35401
|
+
label: job.restream?.running ? t("rs.live") : void 0,
|
|
35402
|
+
on: !!job.restream?.running,
|
|
35403
|
+
content: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RestreamPanel, {
|
|
35404
|
+
jobId: job.id,
|
|
35405
|
+
status: job.restream,
|
|
35406
|
+
now,
|
|
35407
|
+
style: subStyle,
|
|
35408
|
+
t
|
|
35409
|
+
})
|
|
35410
|
+
},
|
|
35411
|
+
{
|
|
35412
|
+
id: "download",
|
|
35413
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.download, {}),
|
|
35414
|
+
title: t("menu.download"),
|
|
35415
|
+
content: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DownloadPanel, {
|
|
35416
|
+
job,
|
|
35417
|
+
lines: shownLines,
|
|
35418
|
+
tasks: tasks.filter((x) => x.dirId === job.id),
|
|
35419
|
+
sessionId,
|
|
35420
|
+
onTask: refreshTasks,
|
|
35421
|
+
langName,
|
|
35422
|
+
siteChips,
|
|
35423
|
+
t
|
|
35424
|
+
})
|
|
35425
|
+
}
|
|
35426
|
+
]
|
|
35059
35427
|
}),
|
|
35060
35428
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
35061
35429
|
className: "dshl-done",
|
|
@@ -35635,6 +36003,55 @@ const en = {
|
|
|
35635
36003
|
"sub.bg.none": "None",
|
|
35636
36004
|
"sub.bg.light": "Light",
|
|
35637
36005
|
"sub.bg.dark": "Dark",
|
|
36006
|
+
"menu.restream": "Restream",
|
|
36007
|
+
"rs.live": "Live",
|
|
36008
|
+
"rs.title": "Push this video, with subtitles, to live platforms",
|
|
36009
|
+
"rs.running": "Restreaming · {t}",
|
|
36010
|
+
"rs.add": "Add a platform",
|
|
36011
|
+
"rs.edit": "Edit",
|
|
36012
|
+
"rs.remove": "Remove",
|
|
36013
|
+
"rs.platform": "Platform",
|
|
36014
|
+
"rs.server": "Server",
|
|
36015
|
+
"rs.key": "Stream key",
|
|
36016
|
+
"rs.keyPh": "Paste the stream key",
|
|
36017
|
+
"rs.keyKeep": "Leave empty to keep the saved key",
|
|
36018
|
+
"rs.save": "Save",
|
|
36019
|
+
"rs.burn": "Subtitles in the picture",
|
|
36020
|
+
"rs.start": "Start restreaming ({n})",
|
|
36021
|
+
"rs.pick": "Tick where to stream",
|
|
36022
|
+
"rs.stop": "Stop restreaming",
|
|
36023
|
+
"rs.stopOne": "Stop this one",
|
|
36024
|
+
"rs.join": "Add",
|
|
36025
|
+
"rs.m.ended": "The video ended, so restreaming stopped.",
|
|
36026
|
+
"rs.m.noBurn": "Subtitles could not be drawn on this computer; streaming without them.",
|
|
36027
|
+
"rs.note": "Starts from where you are watching, with the subtitles you see. Keys stay on this computer.",
|
|
36028
|
+
"rs.s.connecting": "Connecting",
|
|
36029
|
+
"rs.s.live": "Live",
|
|
36030
|
+
"rs.s.retrying": "Reconnecting",
|
|
36031
|
+
"rs.s.error": "Failed",
|
|
36032
|
+
"rs.s.stopped": "Stopped",
|
|
36033
|
+
"rs.p.bilibili": "Bilibili",
|
|
36034
|
+
"rs.p.douyin": "Douyin",
|
|
36035
|
+
"rs.p.kuaishou": "Kuaishou",
|
|
36036
|
+
"rs.p.wechat": "WeChat Channels",
|
|
36037
|
+
"rs.p.douyu": "Douyu",
|
|
36038
|
+
"rs.p.huya": "Huya",
|
|
36039
|
+
"rs.p.yy": "YY",
|
|
36040
|
+
"rs.p.cc": "NetEase CC",
|
|
36041
|
+
"rs.p.youtube": "YouTube",
|
|
36042
|
+
"rs.p.twitch": "Twitch",
|
|
36043
|
+
"rs.p.custom": "Other (RTMP)",
|
|
36044
|
+
"rs.h.bilibili": "In the Bilibili live tool (直播姬) or the web live page, open the streaming settings and copy the server address and stream key.",
|
|
36045
|
+
"rs.h.douyin": "In Douyin Live Companion (直播伴侣), copy the push address; your account needs streaming rights.",
|
|
36046
|
+
"rs.h.kuaishou": "In Kuaishou Live Companion, open streaming settings and copy the address and key.",
|
|
36047
|
+
"rs.h.wechat": "In WeChat Channels Assistant (视频号助手), create a live session and copy its push address and key.",
|
|
36048
|
+
"rs.h.douyu": "Douyu streamer centre → live settings: copy the push address and code.",
|
|
36049
|
+
"rs.h.huya": "Huya streamer back office → push settings (OBS): copy the address and key.",
|
|
36050
|
+
"rs.h.yy": "Copy the push address and key from the YY live tool.",
|
|
36051
|
+
"rs.h.cc": "Copy the push address and key from the NetEase CC live tool.",
|
|
36052
|
+
"rs.h.youtube": "YouTube Studio → Go live → Stream: copy the stream key (the server is filled in).",
|
|
36053
|
+
"rs.h.twitch": "Twitch dashboard → Settings → Stream: copy the primary stream key (the server is filled in).",
|
|
36054
|
+
"rs.h.custom": "Any RTMP, RTMPS or SRT address. If it already contains the key, leave the key empty.",
|
|
35638
36055
|
"menu.download": "Download",
|
|
35639
36056
|
"usage.tokens": "{n} tokens",
|
|
35640
36057
|
"usage.title": "Usage for this video",
|
|
@@ -35774,7 +36191,7 @@ const en = {
|
|
|
35774
36191
|
"chat.p.summary": "Summarise the key points of this video so far from the subtitles.",
|
|
35775
36192
|
"chat.p.download": "Download the original of this video.",
|
|
35776
36193
|
"page.back": "Back to chats",
|
|
35777
|
-
"page.open": "
|
|
36194
|
+
"page.open": "Vidi video",
|
|
35778
36195
|
"side.subs": "Subtitles {p}%",
|
|
35779
36196
|
"side.subsLive": "Subtitling",
|
|
35780
36197
|
"side.recording": "Recording",
|
|
@@ -35851,6 +36268,7 @@ const en = {
|
|
|
35851
36268
|
"set.login.none": "Off",
|
|
35852
36269
|
"set.key": "{s} key",
|
|
35853
36270
|
"set.key.fromDsh": "from DSH",
|
|
36271
|
+
"set.key.fromDshP": "from DSH · {p}",
|
|
35854
36272
|
"set.key.fromEnv": "from environment",
|
|
35855
36273
|
"set.key.replace": "Paste a new key",
|
|
35856
36274
|
"set.key.paste": "Paste key",
|
|
@@ -35899,6 +36317,55 @@ const zh = {
|
|
|
35899
36317
|
"sub.bg.none": "无",
|
|
35900
36318
|
"sub.bg.light": "浅",
|
|
35901
36319
|
"sub.bg.dark": "深",
|
|
36320
|
+
"menu.restream": "转播",
|
|
36321
|
+
"rs.live": "转播中",
|
|
36322
|
+
"rs.title": "把这个视频连同字幕推到直播平台",
|
|
36323
|
+
"rs.running": "转播中 · {t}",
|
|
36324
|
+
"rs.add": "添加平台",
|
|
36325
|
+
"rs.edit": "修改",
|
|
36326
|
+
"rs.remove": "删除",
|
|
36327
|
+
"rs.platform": "平台",
|
|
36328
|
+
"rs.server": "服务器地址",
|
|
36329
|
+
"rs.key": "推流码",
|
|
36330
|
+
"rs.keyPh": "粘贴推流码",
|
|
36331
|
+
"rs.keyKeep": "留空则沿用已保存的推流码",
|
|
36332
|
+
"rs.save": "保存",
|
|
36333
|
+
"rs.burn": "字幕显示在画面里",
|
|
36334
|
+
"rs.start": "开始转播({n} 个平台)",
|
|
36335
|
+
"rs.pick": "先勾选要推到哪里",
|
|
36336
|
+
"rs.stop": "停止转播",
|
|
36337
|
+
"rs.stopOne": "停止这一路",
|
|
36338
|
+
"rs.join": "加入",
|
|
36339
|
+
"rs.m.ended": "视频播完了,转播已停止。",
|
|
36340
|
+
"rs.m.noBurn": "这台电脑画不了字幕,先不带字幕推流。",
|
|
36341
|
+
"rs.note": "从你正在看的位置开始,带着你看到的字幕。推流码只存在这台电脑上。",
|
|
36342
|
+
"rs.s.connecting": "连接中",
|
|
36343
|
+
"rs.s.live": "推流中",
|
|
36344
|
+
"rs.s.retrying": "重连中",
|
|
36345
|
+
"rs.s.error": "失败",
|
|
36346
|
+
"rs.s.stopped": "已停止",
|
|
36347
|
+
"rs.p.bilibili": "B站",
|
|
36348
|
+
"rs.p.douyin": "抖音",
|
|
36349
|
+
"rs.p.kuaishou": "快手",
|
|
36350
|
+
"rs.p.wechat": "视频号",
|
|
36351
|
+
"rs.p.douyu": "斗鱼",
|
|
36352
|
+
"rs.p.huya": "虎牙",
|
|
36353
|
+
"rs.p.yy": "YY",
|
|
36354
|
+
"rs.p.cc": "网易CC",
|
|
36355
|
+
"rs.p.youtube": "YouTube",
|
|
36356
|
+
"rs.p.twitch": "Twitch",
|
|
36357
|
+
"rs.p.custom": "其他(RTMP)",
|
|
36358
|
+
"rs.h.bilibili": "在「B站直播姬」或网页端「我的直播间 → 开播设置」里,复制服务器地址和串流密钥。",
|
|
36359
|
+
"rs.h.douyin": "在「抖音直播伴侣」里复制推流地址;账号需要先开通推流权限。",
|
|
36360
|
+
"rs.h.kuaishou": "在「快手直播伴侣 → 推流设置」里复制地址和推流码。",
|
|
36361
|
+
"rs.h.wechat": "在「视频号助手」里创建直播,复制推流地址和推流密钥。",
|
|
36362
|
+
"rs.h.douyu": "在「斗鱼主播中心 → 直播设置」里复制推流地址和推流码。",
|
|
36363
|
+
"rs.h.huya": "在「虎牙主播后台 → 推流设置(OBS 推流)」里复制地址和密钥。",
|
|
36364
|
+
"rs.h.yy": "在 YY 开播工具里复制推流地址和推流码。",
|
|
36365
|
+
"rs.h.cc": "在网易CC开播工具里复制推流地址和推流码。",
|
|
36366
|
+
"rs.h.youtube": "在 YouTube Studio「开始直播 → 直播」里复制串流密钥,服务器地址已经填好。",
|
|
36367
|
+
"rs.h.twitch": "在 Twitch 创作者后台「设置 → 直播」里复制主串流密钥,服务器地址已经填好。",
|
|
36368
|
+
"rs.h.custom": "任意 RTMP、RTMPS 或 SRT 地址。地址里已经带了推流码的话,推流码留空。",
|
|
35902
36369
|
"menu.download": "下载",
|
|
35903
36370
|
"usage.tokens": "{n} tokens",
|
|
35904
36371
|
"usage.title": "这个视频的用量",
|
|
@@ -36038,7 +36505,7 @@ const zh = {
|
|
|
36038
36505
|
"chat.p.summary": "根据字幕,总结这个视频到目前为止的要点。",
|
|
36039
36506
|
"chat.p.download": "下载这个视频的原片。",
|
|
36040
36507
|
"page.back": "返回对话",
|
|
36041
|
-
"page.open": "视频",
|
|
36508
|
+
"page.open": "Vidi 视频",
|
|
36042
36509
|
"side.subs": "字幕 {p}%",
|
|
36043
36510
|
"side.subsLive": "生成字幕中",
|
|
36044
36511
|
"side.recording": "录制中",
|
|
@@ -36115,6 +36582,7 @@ const zh = {
|
|
|
36115
36582
|
"set.login.none": "不用",
|
|
36116
36583
|
"set.key": "{s} 密钥",
|
|
36117
36584
|
"set.key.fromDsh": "来自 DSH",
|
|
36585
|
+
"set.key.fromDshP": "来自 DSH · {p}",
|
|
36118
36586
|
"set.key.fromEnv": "来自环境变量",
|
|
36119
36587
|
"set.key.replace": "粘贴新密钥替换",
|
|
36120
36588
|
"set.key.paste": "粘贴密钥",
|
|
@@ -36465,6 +36933,44 @@ const CSS = `
|
|
|
36465
36933
|
.dshl-submenu .seg button.on { background: rgba(255,255,255,.92); color: #111; font-weight: 600; }
|
|
36466
36934
|
|
|
36467
36935
|
/* Download (inside the dark menu) */
|
|
36936
|
+
/* Restream (player menu) */
|
|
36937
|
+
.dshl-rs { display: grid; gap: 10px; padding: 0 6px; }
|
|
36938
|
+
.dshl-rs .rs-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; color: var(--muted); font-size: 12.5px; }
|
|
36939
|
+
.dshl-rs .rs-head .dshl-link { flex: none; font-size: 12.5px; }
|
|
36940
|
+
.dshl-rs .rs-list { display: grid; gap: 2px; margin: 0 -6px; }
|
|
36941
|
+
.dshl-rs .rs-row { display: flex; align-items: center; gap: 10px; min-height: 44px; padding: 4px 6px 4px 10px; border-radius: 8px; }
|
|
36942
|
+
.dshl-rs .rs-row:hover { background: var(--hover); }
|
|
36943
|
+
.dshl-rs .rs-row input[type=checkbox], .dshl-rs .rs-check input { width: 16px; height: 16px; margin: 0; flex: none; accent-color: #3d6df2; cursor: pointer; }
|
|
36944
|
+
.dshl-rs .rs-name { display: grid; flex: 1; min-width: 0; font-size: 13.5px; font-weight: 550; }
|
|
36945
|
+
.dshl-rs .rs-name small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--faint); font-size: 11.5px; font-weight: 400; }
|
|
36946
|
+
.dshl-rs .rs-tools { display: flex; gap: 2px; opacity: .65; }
|
|
36947
|
+
.dshl-rs .rs-row:hover .rs-tools { opacity: 1; }
|
|
36948
|
+
.dshl-rs button.ghost { display: grid; place-items: center; width: 28px; height: 28px; padding: 0; border: 0; border-radius: 6px; background: none; color: var(--muted); cursor: pointer; }
|
|
36949
|
+
.dshl-rs button.ghost:hover { background: var(--l2); color: var(--ink); }
|
|
36950
|
+
.dshl-rs .rs-dot { width: 8px; height: 8px; flex: none; border-radius: 50%; background: var(--faint); }
|
|
36951
|
+
.dshl-rs .rs-dot.live { background: #2fbf71; box-shadow: 0 0 0 3px rgba(47,191,113,.2); }
|
|
36952
|
+
.dshl-rs .rs-dot.connecting, .dshl-rs .rs-dot.retrying { background: #f5a524; animation: dshl-pulse 1.4s ease-in-out infinite; }
|
|
36953
|
+
.dshl-rs .rs-dot.error { background: #ff5a5f; }
|
|
36954
|
+
.dshl-rs .rs-state { flex: none; color: var(--muted); font-size: 12px; }
|
|
36955
|
+
.dshl-rs .rs-state.live { color: #5fd897; }
|
|
36956
|
+
.dshl-rs .rs-state.error { color: #ff7a7e; }
|
|
36957
|
+
.dshl-rs .rs-opts { display: flex; align-items: center; justify-content: space-between; gap: 10px; min-height: 34px; }
|
|
36958
|
+
.dshl-rs .rs-check { display: inline-flex; align-items: center; gap: 8px; font-size: 13px; cursor: pointer; }
|
|
36959
|
+
.dshl-rs .seg { display: inline-flex; gap: 2px; padding: 2px; border-radius: 8px; background: var(--l2); }
|
|
36960
|
+
.dshl-rs .seg button { height: 26px; padding: 0 12px; border: 0; border-radius: 6px; background: none; color: var(--muted); font-size: 12.5px; cursor: pointer; }
|
|
36961
|
+
.dshl-rs .seg button.on { background: rgba(255,255,255,.92); color: #111; font-weight: 600; }
|
|
36962
|
+
.dshl-rs .dshl-btn.block { height: 38px; justify-content: center; }
|
|
36963
|
+
.dshl-rs .dshl-btn.rs-stop { border-color: transparent; background: #e5484d; color: #fff; }
|
|
36964
|
+
.dshl-rs .rs-hint { margin: 0; color: var(--faint); font-size: 12px; line-height: 1.55; }
|
|
36965
|
+
.dshl-rs .rs-err { margin: 0; color: #ff8f92; font-size: 12px; line-height: 1.55; }
|
|
36966
|
+
.dshl-rs .rs-field { display: grid; grid-template-columns: 76px 1fr; align-items: center; gap: 10px; font-size: 13px; color: var(--muted); }
|
|
36967
|
+
.dshl-rs .rs-field input, .dshl-rs .rs-field select { width: 100%; height: 34px; padding: 0 10px; border: 1px solid var(--line); border-radius: 8px; background: var(--l1); color: var(--ink); font: inherit; font-size: 13px; }
|
|
36968
|
+
.dshl-rs .rs-field select { padding-right: 28px; background-color: var(--l1); background-image: url("data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 width=%2710%27 height=%276%27%3E%3Cpath d=%27M1 1l4 4 4-4%27 fill=%27none%27 stroke=%27%23aaa%27 stroke-width=%271.5%27/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 10px center; appearance: none; }
|
|
36969
|
+
.dshl-rs .rs-field input:focus, .dshl-rs .rs-field select:focus { outline: none; border-color: var(--accent); }
|
|
36970
|
+
.dshl-rs .rs-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
36971
|
+
.dshl-menu.sheet .dshl-rs .rs-row { min-height: 50px; }
|
|
36972
|
+
.dshl-menu.sheet .dshl-rs .rs-field input, .dshl-menu.sheet .dshl-rs .rs-field select { height: 42px; font-size: 15px; }
|
|
36973
|
+
.dshl-menu.sheet .dshl-rs .dshl-btn.block { height: 46px; font-size: 15px; }
|
|
36468
36974
|
.dshl-dl { display: grid; gap: 14px; padding: 0 6px; }
|
|
36469
36975
|
.dshl-dl .grp { display: grid; gap: 8px; }
|
|
36470
36976
|
.dshl-dl .cap { color: var(--muted); font-size: 12px; font-weight: 550; }
|