Skip to content

Commit 3085be7

Browse files
authored
🔀 Merge pull request #304 from richardfrost/crunchyroll_text
Crunchyroll Caption/Subtitle Text
2 parents 2f5ca23 + acaf21e commit 3085be7

File tree

3 files changed

+79
-62
lines changed

3 files changed

+79
-62
lines changed

src/script/lib/globals.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
interface AudioRule {
22
_displayElement?: HTMLElement; // Element to display/hide
33
_dynamic?: boolean; // [Dynamic] Set to true on a dynamic rule
4+
apfCaptions?: boolean; // [Cue] Display an HTML version of the caption/subtitle text: Requires videoCueHideCues = true
5+
apfCaptionsSelector?: string; // [Cue] Selector for container that will hold the custom HTML captions
46
checkInterval?: number; // [Watcher] Set a custom watch interval (in ms, Default: 20)
57
className?: string; // [Element] node.className.includes()
68
containsSelector?: string; // [Element] node.querySelector() [Not commonly used]
@@ -17,6 +19,7 @@ interface AudioRule {
1719
externalSub?: boolean; // [Cue] [BETA]: Convert external captions/subtitles obtained from remote source to VTTCues
1820
externalSubFormatKey?: string; // [Cue] [BETA]: Key name for caption/subtitle format (Default: 'format')
1921
externalSubTrackLabel?: string; // [Cue] [BETA]: Label used for processed TextTrack
22+
externalSubTrackMode?: TextTrackMode; // [Cue] [BETA]: TextTrack mode for new TextTrack
2023
externalSubURLKey?: string; // [Cue] [BETA]: Key name for caption/subtitle URL (Default: 'url')
2124
externalSubVar?: string; // [Cue] [BETA]: Global variable to find available caption/subtitle data
2225
filterSubtitles?: boolean; // [All] Filter subtitle text (Default: true)

src/script/webAudio.ts

Lines changed: 73 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,33 @@ export default class WebAudio {
8787
}
8888
}
8989

90+
apfCaptionLine(rule: AudioRule, text: string): HTMLSpanElement {
91+
const line = document.createElement('span');
92+
line.classList.add('APF-subtitle-line');
93+
line.style.background = 'black';
94+
line.style.color = 'white';
95+
line.style.fontSize = '3.5vw';
96+
line.style.paddingLeft = '4px';
97+
line.style.paddingRight = '4px';
98+
line.style.height = '18px';
99+
line.textContent = text;
100+
return line;
101+
}
102+
103+
apfCaptionLines(rule: AudioRule, lines: HTMLSpanElement[]): HTMLDivElement {
104+
const apfLines = document.createElement('div');
105+
apfLines.classList.add('APF-subtitles');
106+
apfLines.style.bottom = '10px';
107+
apfLines.style.position = 'absolute';
108+
apfLines.style.textAlign = 'center';
109+
apfLines.style.width = '100%';
110+
lines.forEach((line) => {
111+
apfLines.appendChild(line);
112+
apfLines.appendChild(document.createElement('br'));
113+
});
114+
return apfLines;
115+
}
116+
90117
clean(subtitleContainer, ruleIndex = 0): void {
91118
const rule = this.rules[ruleIndex];
92119
if (rule.mode === 'watcher') { return; } // If this is for a watcher rule, leave the text alone
@@ -128,11 +155,8 @@ export default class WebAudio {
128155
}
129156
});
130157

131-
switch (rule.showSubtitles) {
132-
case Constants.ShowSubtitles.Filtered: if (filtered) { this.showSubtitles(rule, subtitles); } else { this.hideSubtitles(rule, subtitles); } break;
133-
case Constants.ShowSubtitles.Unfiltered: if (filtered) { this.hideSubtitles(rule, subtitles); } else { this.showSubtitles(rule, subtitles); } break;
134-
case Constants.ShowSubtitles.None: this.hideSubtitles(rule, subtitles); break;
135-
}
158+
const shouldBeShown = this.subtitlesShouldBeShown(rule, filtered);
159+
shouldBeShown ? this.showSubtitles(rule) : this.hideSubtitles(rule);
136160
}
137161

138162
cleanYouTubeAutoSubs(node): void {
@@ -236,15 +260,9 @@ export default class WebAudio {
236260
// Some sites ignore textTrack.mode = 'hidden' and will still show captions
237261
// This is a fallback (not preferred) method that can be used for hiding the cues
238262
hideCue(rule: AudioRule, cue: FilteredVTTCue) {
239-
if (
240-
(rule.showSubtitles === Constants.ShowSubtitles.Filtered && !cue.filtered)
241-
|| (rule.showSubtitles === Constants.ShowSubtitles.Unfiltered && cue.filtered)
242-
|| rule.showSubtitles === Constants.ShowSubtitles.None
243-
) {
244-
cue.text = '';
245-
cue.position = 100;
246-
cue.size = 0;
247-
}
263+
cue.text = '';
264+
cue.position = 100;
265+
cue.size = 0;
248266
}
249267

250268
hideSubtitles(rule: AudioRule, subtitles?) {
@@ -280,9 +298,11 @@ export default class WebAudio {
280298
}
281299

282300
initCueRule(rule: AudioRule) {
301+
if (rule.apfCaptions === true) { rule.videoCueHideCues = true; }
283302
if (rule.videoSelector === undefined) { rule.videoSelector = WebAudio.DefaultVideoSelector; }
284303
if (rule.videoCueRequireShowing === undefined) { rule.videoCueRequireShowing = this.filter.cfg.muteCueRequireShowing; }
285304
if (rule.externalSub) {
305+
if (rule.externalSubTrackMode === undefined) { rule.externalSubTrackMode = 'showing'; }
286306
if (rule.externalSubURLKey === undefined) { rule.externalSubURLKey = 'url'; }
287307
if (rule.externalSubFormatKey === undefined) { rule.externalSubFormatKey = 'format'; }
288308
if (rule.externalSubTrackLabel === undefined) { rule.externalSubTrackLabel = 'APF'; }
@@ -446,7 +466,7 @@ export default class WebAudio {
446466
newTextTrack(rule: AudioRule, video: HTMLVideoElement, cues: VTTCue[]): TextTrack {
447467
if (video.textTracks) {
448468
const track = video.addTextTrack('captions', rule.externalSubTrackLabel, rule.videoCueLanguage) as TextTrack;
449-
track.mode = 'showing';
469+
track.mode = rule.externalSubTrackMode;
450470
for (let i = 0; i < cues.length; i++) {
451471
track.addCue(cues[i]);
452472
}
@@ -603,15 +623,13 @@ export default class WebAudio {
603623
}
604624

605625
const result = this.replaceTextResult(cue.text);
626+
cue.originalText = cue.text;
606627
if (result.modified) {
607628
cue.filtered = true;
608-
cue.originalText = cue.text;
609629
cue.text = result.filtered;
610630
} else {
611631
cue.filtered = false;
612632
}
613-
614-
if (rule.videoCueHideCues) { this.hideCue(rule, cue); }
615633
}
616634
}
617635

@@ -748,6 +766,15 @@ export default class WebAudio {
748766
this.fillerAudio.currentTime = 0;
749767
}
750768

769+
subtitlesShouldBeShown(rule, filtered: boolean = false): boolean {
770+
switch(rule.showSubtitles) {
771+
case Constants.ShowSubtitles.All: return true;
772+
case Constants.ShowSubtitles.Filtered: return filtered;
773+
case Constants.ShowSubtitles.Unfiltered: return !filtered;
774+
case Constants.ShowSubtitles.None: return false;
775+
}
776+
}
777+
751778
// Checks if a node is a supported audio node.
752779
// Returns rule id upon first match, otherwise returns false
753780
supportedNode(node) {
@@ -883,12 +910,8 @@ export default class WebAudio {
883910
}
884911

885912
if (data.skipped) { return false; }
886-
// Hide/show captions/subtitles
887-
switch (rule.showSubtitles) {
888-
case Constants.ShowSubtitles.Filtered: if (data.filtered) { instance.showSubtitles(rule); } else { instance.hideSubtitles(rule); } break;
889-
case Constants.ShowSubtitles.Unfiltered: if (data.filtered) { instance.hideSubtitles(rule); } else { instance.showSubtitles(rule); } break;
890-
case Constants.ShowSubtitles.None: instance.hideSubtitles(rule); break;
891-
}
913+
const shouldBeShown = instance.subtitlesShouldBeShown(rule, data.filtered);
914+
shouldBeShown ? instance.showSubtitles(rule) : instance.hideSubtitles(rule);
892915
if (data.filtered) { instance.filter.updateCounterBadge(); }
893916
} else {
894917
if (rule.ignoreMutations) { instance.filter.startObserving(); } // Start observing when video is not playing
@@ -908,49 +931,38 @@ export default class WebAudio {
908931

909932
textTrack.oncuechange = () => {
910933
if (textTrack.activeCues && textTrack.activeCues.length > 0) {
911-
let filtered = false;
912-
913-
for (let i = 0; i < textTrack.activeCues.length; i++) {
914-
const activeCue = textTrack.activeCues[i] as FilteredVTTCue;
915-
if (!activeCue.hasOwnProperty('filtered')) {
916-
const cues = textTrack.cues as any as FilteredVTTCue[];
917-
instance.processCues(cues, rule);
918-
}
919-
920-
if (activeCue.filtered) {
921-
filtered = true;
922-
instance.mute(rule, video);
934+
const activeCues = Array.from(textTrack.activeCues as any as FilteredVTTCue[]);
935+
const apfLines = [];
936+
937+
const processed = activeCues.some((activeCue) => activeCue.hasOwnProperty('filtered'));
938+
if (!processed) { instance.processCues(activeCues, rule); }
939+
const filtered = activeCues.some((activeCue) => activeCue.filtered);
940+
filtered ? instance.mute(rule, video) : instance.unmute(rule, video);
941+
const shouldBeShown = instance.subtitlesShouldBeShown(rule, filtered);
942+
943+
for (let i = 0; i < activeCues.length; i++) {
944+
const activeCue = activeCues[i];
945+
if (!shouldBeShown && rule.videoCueHideCues) { instance.hideCue(rule, activeCue); }
946+
if (rule.apfCaptions) {
947+
const text = filtered ? activeCue.text : activeCue.originalText;
948+
const line = instance.apfCaptionLine(rule, text);
949+
apfLines.unshift(line); // Cues seem to show up in reverse order
923950
}
924951
}
925952

926-
if (!filtered) { instance.unmute(rule, video); }
927-
928-
if (!rule.videoCueHideCues) {
929-
if (filtered) {
930-
switch (rule.showSubtitles) {
931-
case Constants.ShowSubtitles.Filtered: textTrack.mode = 'showing'; break;
932-
case Constants.ShowSubtitles.Unfiltered: textTrack.mode = 'hidden'; break;
933-
}
934-
} else {
935-
switch (rule.showSubtitles) {
936-
case Constants.ShowSubtitles.Filtered: textTrack.mode = 'hidden'; break;
937-
case Constants.ShowSubtitles.Unfiltered: textTrack.mode = 'showing'; break;
938-
}
953+
if (apfLines.length) {
954+
const container = document.getElementById(rule.apfCaptionsSelector);
955+
const oldLines = container.querySelector('div.APF-subtitles');
956+
if (oldLines) { oldLines.remove(); }
957+
if (shouldBeShown) {
958+
const apfCaptions = instance.apfCaptionLines(rule, apfLines);
959+
container.appendChild(apfCaptions);
939960
}
940961
}
941962

942-
if (rule.displaySelector) {
943-
if (filtered) {
944-
switch (rule.showSubtitles) {
945-
case Constants.ShowSubtitles.Filtered: instance.showSubtitles(rule); break;
946-
case Constants.ShowSubtitles.Unfiltered: instance.hideSubtitles(rule); break;
947-
}
948-
} else {
949-
switch (rule.showSubtitles) {
950-
case Constants.ShowSubtitles.Filtered: instance.hideSubtitles(rule); break;
951-
case Constants.ShowSubtitles.Unfiltered: instance.showSubtitles(rule); break;
952-
}
953-
}
963+
if (!rule.videoCueHideCues) { textTrack.mode = shouldBeShown ? 'showing' : 'hidden'; }
964+
if (rule.displaySelector) { // Hide original subtitles if using apfCaptions
965+
apfLines.length || !shouldBeShown ? instance.hideSubtitles(rule) : instance.showSubtitles(rule);
954966
}
955967
} else { // No active cues
956968
instance.unmute(rule, video);

src/script/webAudioSites.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,14 @@ export default class WebAudioSites {
5454
'www.criterionchannel.com': [{ iframe: true, mode: 'cue', videoCueHideCues: true, videoCueRequireShowing: false }],
5555
'www.crunchyroll.com': [
5656
{
57+
apfCaptions: true,
58+
apfCaptionsSelector: 'vilosVttJs',
5759
displaySelector: 'canvas#velocity-canvas',
5860
externalSub: true,
61+
externalSubTrackMode: 'hidden',
5962
externalSubVar: 'window.v1config.media.subtitles',
6063
iframe: true,
6164
mode: 'cue',
62-
showSubtitles: 0,
6365
videoCueLanguage: 'enUS',
6466
videoCueRequireShowing: false,
6567
}

0 commit comments

Comments
 (0)