feat: subtitles while streaming, and fetched when a download lacks them
YouTube's HLS manifest carries a dozen audio renditions and no subtitles whatsoever — checked against a live master playlist: 16 EXT-X-MEDIA entries, all TYPE=AUDIO, zero SUBTITLES. So the track menu could only ever list audio while streaming, which is what it did. Subtitles are now fetched separately with yt-dlp, tidied through the existing VTT cleanup, and cached per video and language. They reach the player as blob URLs, which share the document's origin — a file:// or 127.0.0.1 track would be cross-origin to the page and need CORS the media pipeline cannot supply. The same path fills in a download saved before subtitles were switched on, without fetching the video again. YouTube serves identical auto-generated captions under both "en" and "en-orig", so byte-identical texts collapse to one entry rather than offering the same track twice, and tracks are labelled "English" rather than "en". The subtitle preference now defaults to English. "None" is a poor default for a setting whose whole purpose is captions: it silently means no subtitles are downloaded, fetched, or offered anywhere, and the Settings text now says so.
This commit is contained in:
@@ -38,6 +38,8 @@ interface Props {
|
||||
subLang: string;
|
||||
/** Persists a choice made here, so the next video matches. */
|
||||
onSubLang: (l: string) => void;
|
||||
/** Captions are being fetched, so the menu is not empty for lack of any. */
|
||||
subsLoading?: boolean;
|
||||
}
|
||||
|
||||
const SKIP_S = 10;
|
||||
@@ -64,7 +66,7 @@ const btn =
|
||||
* bottom, so the native ones are switched off entirely.
|
||||
*/
|
||||
export default function PlayerControls({
|
||||
videoRef, stageRef, onActivity, subLang, onSubLang,
|
||||
videoRef, stageRef, onActivity, subLang, onSubLang, subsLoading,
|
||||
}: Props) {
|
||||
const [playing, setPlaying] = useState(false);
|
||||
const [time, setTime] = useState(0);
|
||||
@@ -331,7 +333,7 @@ export default function PlayerControls({
|
||||
}}
|
||||
/>
|
||||
|
||||
{(audio.length > 1 || subs.length > 0) && (
|
||||
{(audio.length > 1 || subs.length > 0 || subsLoading) && (
|
||||
<div ref={menuRef} className="relative shrink-0">
|
||||
<button
|
||||
onClick={() => { setMenu((m) => !m); onActivity?.(); }}
|
||||
@@ -370,6 +372,15 @@ export default function PlayerControls({
|
||||
</>
|
||||
)}
|
||||
|
||||
{subs.length === 0 && subsLoading && (
|
||||
<>
|
||||
<div className="mt-1 px-2 py-1.5 text-[10px] font-bold uppercase tracking-widest text-slate-400">
|
||||
Subtitles
|
||||
</div>
|
||||
<div className="px-2 py-1.5 text-[12px] text-slate-400">Fetching…</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{subs.length > 0 && (
|
||||
<>
|
||||
<div className="mt-1 px-2 py-1.5 text-[10px] font-bold uppercase tracking-widest text-slate-400">
|
||||
|
||||
Reference in New Issue
Block a user