fix(stream): enable captions only for real Korean tracks, skip auto-generated
The broadcast subtitle rule treated any ko* track as Korean, so YouTube's auto-generated (자동 생성) Korean track would switch captions on. Match only human-authored tracks (kind !== 'asr', vss_id not 'a.*') and pass the full track object to setOption so YouTube selects the manual track rather than the same-languageCode ASR one. Captions stay off when only an auto track exists. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -29,7 +29,8 @@ real browsing session captured from the X display.
|
|||||||
ad" instantly, closes overlay ads, fast-forwards unskippable ads (seek-to-end
|
ad" instantly, closes overlay ads, fast-forwards unskippable ads (seek-to-end
|
||||||
+ 16x + mute) and RESTORES the pre-ad muted/playbackRate when the ad ends; and
|
+ 16x + mute) and RESTORES the pre-ad muted/playbackRate when the ad ends; and
|
||||||
(2) applies the subtitle rule per video: captions OFF by default, Korean ON
|
(2) applies the subtitle rule per video: captions OFF by default, Korean ON
|
||||||
when the video offers a Korean track. Run it alongside the broadcast; it
|
only when the video offers a real (human-authored) Korean track; YouTube's
|
||||||
|
auto-generated Korean track is ignored. Run it alongside the broadcast; it
|
||||||
reconnects across Chrome restarts.
|
reconnects across Chrome restarts.
|
||||||
|
|
||||||
## Run
|
## Run
|
||||||
|
|||||||
@@ -5,7 +5,9 @@
|
|||||||
// so they clear in ~1s. The pre-ad muted/playbackRate are SAVED and
|
// so they clear in ~1s. The pre-ad muted/playbackRate are SAVED and
|
||||||
// RESTORED when the ad ends, so the main video is never left muted/fast.
|
// RESTORED when the ad ends, so the main video is never left muted/fast.
|
||||||
// 2. Applies the subtitle rule per video: captions OFF by default, but a
|
// 2. Applies the subtitle rule per video: captions OFF by default, but a
|
||||||
// Korean track is turned ON when the video offers one. Runs once per video.
|
// real (human-authored) Korean track is turned ON when the video offers
|
||||||
|
// one. YouTube's auto-generated (자동 생성) Korean track does NOT count and
|
||||||
|
// is left off. Runs once per video.
|
||||||
// Self-contained: no extension, no network/hosts changes. Reconnects across
|
// Self-contained: no extension, no network/hosts changes. Reconnects across
|
||||||
// Chrome restarts.
|
// Chrome restarts.
|
||||||
//
|
//
|
||||||
@@ -44,29 +46,41 @@ const WATCH = `(() => {
|
|||||||
return adShowing;
|
return adShowing;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// A "real" Korean caption track is a human-authored/uploaded Korean track,
|
||||||
|
// NOT YouTube's auto-speech-recognition (자동 생성) track. ASR tracks report
|
||||||
|
// kind === 'asr' and a vss_id like 'a.ko'; manual tracks have an empty kind
|
||||||
|
// and a vss_id like '.ko'. We only ever turn captions on for a real track.
|
||||||
|
const isRealKo = (t) => !!t && /^ko/i.test(t.languageCode || '')
|
||||||
|
&& t.kind !== 'asr' && !/^a\./.test(t.vss_id || '');
|
||||||
|
|
||||||
const capTick = (adShowing) => {
|
const capTick = (adShowing) => {
|
||||||
if (adShowing) return; // don't touch captions while an ad plays
|
if (adShowing) return; // don't touch captions while an ad plays
|
||||||
const p = document.getElementById('movie_player');
|
const p = document.getElementById('movie_player');
|
||||||
if (!p || !p.getOption || !p.getVideoData) return;
|
if (!p || !p.getOption || !p.getVideoData) return;
|
||||||
const vid = p.getVideoData().video_id;
|
const vid = p.getVideoData().video_id;
|
||||||
if (!vid) return;
|
if (!vid) return;
|
||||||
// Decide the desired state once per video (off, or Korean if offered).
|
// Decide the desired state once per video: ON only if a real (non-ASR)
|
||||||
|
// Korean track is offered, otherwise OFF.
|
||||||
if (capWant[vid] === undefined) {
|
if (capWant[vid] === undefined) {
|
||||||
capTries[vid] = (capTries[vid] || 0) + 1;
|
capTries[vid] = (capTries[vid] || 0) + 1;
|
||||||
let tracks = [];
|
let tracks = [];
|
||||||
try { p.loadModule('captions'); tracks = p.getOption('captions', 'tracklist') || []; } catch {}
|
try { p.loadModule('captions'); tracks = p.getOption('captions', 'tracklist') || []; } catch {}
|
||||||
if (tracks.length) capWant[vid] = tracks.find((t) => /^ko/i.test(t.languageCode || '')) ? 'ko' : 'off';
|
if (tracks.length) capWant[vid] = tracks.some(isRealKo) ? 'ko' : 'off';
|
||||||
else if (capTries[vid] > 16) capWant[vid] = 'off'; // no tracks: keep it off
|
else if (capTries[vid] > 16) capWant[vid] = 'off'; // no tracks: keep it off
|
||||||
else return; // tracklist not ready yet
|
else return; // tracklist not ready yet
|
||||||
}
|
}
|
||||||
// Enforce it every tick so YouTube cannot silently re-enable captions.
|
// Enforce it every tick so YouTube cannot silently re-enable captions or
|
||||||
let curLc = '';
|
// swap our manual Korean track for the auto-generated one.
|
||||||
try { const c = p.getOption('captions', 'track'); curLc = (c && c.languageCode) || ''; } catch {}
|
let curTrack = null;
|
||||||
|
try { curTrack = p.getOption('captions', 'track'); } catch {}
|
||||||
|
const curLc = (curTrack && curTrack.languageCode) || '';
|
||||||
if (capWant[vid] === 'ko') {
|
if (capWant[vid] === 'ko') {
|
||||||
if (!/^ko/i.test(curLc)) {
|
if (!isRealKo(curTrack)) { // not on the real Korean track (off, or ASR) -> set it
|
||||||
let tracks = []; try { tracks = p.getOption('captions', 'tracklist') || []; } catch {}
|
let tracks = []; try { tracks = p.getOption('captions', 'tracklist') || []; } catch {}
|
||||||
const ko = tracks.find((t) => /^ko/i.test(t.languageCode || ''));
|
const ko = tracks.find(isRealKo);
|
||||||
if (ko) { try { p.setOption('captions', 'track', { languageCode: ko.languageCode }); } catch {} }
|
// Pass the full track object so YouTube selects this exact manual track
|
||||||
|
// and not the same-languageCode auto-generated one.
|
||||||
|
if (ko) { try { p.setOption('captions', 'track', ko); } catch {} }
|
||||||
}
|
}
|
||||||
} else if (curLc) { // want off but a track is on -> turn it off
|
} else if (curLc) { // want off but a track is on -> turn it off
|
||||||
try { p.setOption('captions', 'track', {}); } catch {}
|
try { p.setOption('captions', 'track', {}); } catch {}
|
||||||
|
|||||||
Reference in New Issue
Block a user