Skip to content

Commit 3b3da0f

Browse files
committed
[#39] Video quality setting
1 parent c96700d commit 3b3da0f

File tree

9 files changed

+250
-5
lines changed

9 files changed

+250
-5
lines changed

src/_locales/de/messages.json

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,42 @@
8383
"message": "Wenn gesetzt, dann wird das Video beim Laden auf Fenstergröße erweitert. Es gibt die Option auch manuell im Spieler.",
8484
"description": "Hilfstext für Expand der Optionen Seite."
8585
},
86+
"optionsQualityTitle": {
87+
"message": "Automatische Videoqualität",
88+
"description": "Beschriftung für Quality der Optionen Seite."
89+
},
90+
"optionsQualityHint": {
91+
"message": "Erlaubt das automatische Überschreiben der Videoqualität je auch höchste/niedrigste Verfügbare, oder mit Präferenz und wechsel auf nächst-höhere/nierere Option wenn nicht verfügbar.",
92+
"description": "Hilfstext für Quality der Optionen Seite."
93+
},
94+
"optionsQualityTypeLabel": {
95+
"message": "Qualität Modus",
96+
"description": "Beschriftung für Quality der Optionen Seite."
97+
},
98+
"optionsQualityPreferredLabel": {
99+
"message": "Bevorzugte Qualität",
100+
"description": "Beschriftung für Preferred Quality der Optionen Seite."
101+
},
102+
"optionsQualityOff": {
103+
"message": "Aus (keine Anpassung)",
104+
"description": "Beschriftung für Quality Off der Optionen Seite."
105+
},
106+
"optionsQualityHighest": {
107+
"message": "Höchste Verfügbare",
108+
"description": "Beschriftung für Quality Highest der Optionen Seite."
109+
},
110+
"optionsQualityLowest": {
111+
"message": "Niedrigste Verfügbare",
112+
"description": "Beschriftung für Quality Lowest der Optionen Seite."
113+
},
114+
"optionsQualityPreferredChooseHigher": {
115+
"message": "Bevorzugt, Rückfallend auf Höhere",
116+
"description": "Beschriftung für Quality PreferredChooseHigher der Optionen Seite."
117+
},
118+
"optionsQualityPreferredChooseLower": {
119+
"message": "Bevorzugt, Rückfallend auf Niederere",
120+
"description": "Beschriftung für Quality PreferredChooseLower der Optionen Seite."
121+
},
86122
"optionsPlayerConfigureTitle": {
87123
"message": "Videospieler konfigurieren",
88124
"description": "Beschriftung für PlayerConfigure der Optionen Seite."

src/_locales/en/messages.json

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,42 @@
8383
"message": "If set the video will be expanded to the size of the window on load. This can also be done manually in the player.",
8484
"description": "Hint for Expand of options page."
8585
},
86+
"optionsQualityTitle": {
87+
"message": "Auto Video Quality",
88+
"description": "Label for Quality of options page."
89+
},
90+
"optionsQualityHint": {
91+
"message": "Override the video quality of the player to highest/lowest available, or to a preferred quality with switch to next higher/lower if not available.",
92+
"description": "Hint for Quality of options page."
93+
},
94+
"optionsQualityTypeLabel": {
95+
"message": "Quality Mode",
96+
"description": "Label for Quality of options page."
97+
},
98+
"optionsQualityPreferredLabel": {
99+
"message": "Preferred Quality",
100+
"description": "Label for Preferred Quality of options page."
101+
},
102+
"optionsQualityOff": {
103+
"message": "Off (default)",
104+
"description": "Label for Quality Off of options page."
105+
},
106+
"optionsQualityHighest": {
107+
"message": "Highest available",
108+
"description": "Label for Quality Highest of options page."
109+
},
110+
"optionsQualityLowest": {
111+
"message": "Lowest available",
112+
"description": "Label for Quality Lowest of options page."
113+
},
114+
"optionsQualityPreferredChooseHigher": {
115+
"message": "Preferred, fallback to next higher",
116+
"description": "Label for Quality PreferredChooseHigher of options page."
117+
},
118+
"optionsQualityPreferredChooseLower": {
119+
"message": "Preferred, fallback to next lower",
120+
"description": "Label for Quality PreferredChooseLower of options page."
121+
},
86122
"optionsPlayerConfigureTitle": {
87123
"message": "Configure player",
88124
"description": "Label for PlayerConfigure of options page."

src/options.html

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,22 @@ <h3 class="enhancer-field-title i18n">__MSG_optionsExpandTitle__</h3>
7676
<p class="hint i18n">__MSG_optionsExpandHint__</p>
7777
</div>
7878

79+
<div class="enhancer-field">
80+
<h3 class="enhancer-field-title i18n">__MSG_optionsQualityTitle__</h3>
81+
82+
<div class="enhancer-control">
83+
<select name="qualityType" id="qualityType" class="enhancer-text-input" data-default="Off"></select>
84+
<label class="title i18n" for="preferredQuality">__MSG_optionsQualityTypeLabel__</label>
85+
</div>
86+
87+
<div class="enhancer-control">
88+
<input type="number" class="enhancer-text-input" name="preferredQuality" id="preferredQuality" data-default="1080" size="5" placeholder="1080p" />
89+
<label class="title i18n" for="preferredQuality">__MSG_optionsQualityPreferredLabel__</label>
90+
</div>
91+
92+
<p class="hint i18n">__MSG_optionsQualityHint__</p>
93+
</div>
94+
7995
<div class="enhancer-field">
8096
<h3 class="enhancer-field-title i18n">__MSG_optionsPlayerConfigureTitle__</h3>
8197

src/scripts/helpers/shared/constants.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,11 @@ export const enum BrowserMessage {
3737
GET_YTID = 'getYoutubeId',
3838
GET_VID = 'getNebulaVid',
3939
}
40+
41+
export const QualityType = {
42+
Off: 'Off',
43+
Highest: 'Highest',
44+
Lowest: 'Lowest',
45+
PreferredChooseHigher: 'PreferredChooseHigher',
46+
PreferredChooseLower: 'PreferredChooseLower',
47+
} as const;

src/scripts/options.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { loadCreators } from './background';
33
import { purgeCache } from './background/ext';
44
import { getChannels } from './helpers/api';
55
import { buildModal } from './helpers/modal';
6-
import { BrowserMessage, getBrowserInstance, getFromStorage, notification, setToStorage } from './helpers/sharedExt';
6+
import { BrowserMessage, QualityType, getBrowserInstance, getFromStorage, notification, setToStorage } from './helpers/sharedExt';
77
import { load, saveDirect } from './options/form';
88
import { showLogs } from './options/logs';
99
import { showManageCreators } from './options/managecreators';
@@ -46,6 +46,11 @@ const vChange = () => {
4646
els.volumeChange.disabled = !c;
4747
};
4848
els.volumeEnabled.addEventListener('change', vChange);
49+
const qChange = () => {
50+
const type = els.qualityType.value as keyof typeof QualityType;
51+
els.preferredQuality.parentElement.style.display = type === QualityType.PreferredChooseHigher || type === QualityType.PreferredChooseLower ? '' : 'none';
52+
};
53+
els.qualityType.addEventListener('change', qChange);
4954
const nChange = () => {
5055
els.ytOpenTab.disabled = !els.watchnebula.checked;
5156
};
@@ -82,6 +87,7 @@ document.querySelector('#configurePlayer').addEventListener('click', showConfigu
8287
load(true)
8388
.then(aChange)
8489
.then(vChange)
90+
.then(qChange)
8591
.then(nChange)
8692
.then(nTabChange)
8793
.then(hChange)

src/scripts/options/settings.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { purgeCacheIfNecessary } from '../background/ext';
2-
import { arrFromLengthy, getBrowserInstance, getFromStorage, parseTimeString, toTimeString } from '../helpers/sharedExt';
2+
import { QualityType, arrFromLengthy, getBrowserInstance, getFromStorage, parseTimeString, toTimeString } from '../helpers/sharedExt';
33

44
export class Settings {
55
private static instance: Settings = null;
@@ -12,6 +12,8 @@ export class Settings {
1212
volumeShow: HTMLInputElement = undefined;
1313
volumeChange: HTMLInputElement = undefined;
1414
autoExpand: HTMLInputElement = undefined;
15+
qualityType: HTMLSelectElement = undefined;
16+
preferredQuality: HTMLInputElement = undefined;
1517
youtube: HTMLInputElement = undefined;
1618
ytOpenTab: HTMLInputElement = undefined;
1719
ytMuteOnly: HTMLInputElement = undefined;
@@ -43,6 +45,13 @@ export class Settings {
4345
}
4446
}
4547
});
48+
49+
for (const opt of Object.keys(QualityType)) {
50+
const option = document.createElement('option');
51+
option.value = QualityType[opt];
52+
option.text = getBrowserInstance().i18n.getMessage('optionsQuality' + QualityType[opt]);
53+
this.qualityType.appendChild(option);
54+
}
4655
}
4756

4857
static get() {
@@ -71,6 +80,9 @@ export const toData = async (useDefaults = false) => {
7180
data[key] = val;
7281
});
7382

83+
if (!((data.qualityType as string) in QualityType))
84+
data.qualityType = QualityType.Off;
85+
7486
data.visitedColor = (data.visitedColor as string).split(';')[0].trim();
7587
els.visitedColor.value = data.visitedColor;
7688

src/scripts/page/player.ts

Lines changed: 131 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import createQueueButton, { toggleQueueButton } from './components/queue';
66
import createSpeedDial from './components/speeddial';
77
import attachVolumeText, { toggleVolumeShow } from './components/volume';
88
import { init as initDispatch, loadPrefix, navigatePrefix } from './dispatcher';
9-
import { Message, getFromStorage, notification, onStorageChange, parseTypeObject, replyMessage, sendMessage } from './sharedpage';
9+
import { Message, QualityType, getFromStorage, notification, onStorageChange, parseTypeObject, replyMessage, sendMessage } from './sharedpage';
1010

1111
export type Player = HTMLVideoElement & { _enhancerInit: boolean; };
1212

@@ -20,6 +20,8 @@ const optionsDefaults = {
2020
volumeLog: false,
2121
autoExpand: false,
2222
playerSettings: {} as Partial<Settings>,
23+
qualityType: QualityType.Off as keyof typeof QualityType,
24+
preferredQuality: 1080,
2325
};
2426
let options = { ...optionsDefaults };
2527
let lastPositon: number | undefined = undefined;
@@ -79,6 +81,7 @@ export const init = async () => {
7981
player.autoplay = options.autoplay;
8082
toggleVolumeShow(player, options.volumeShow);
8183
addPlayerControls(player);
84+
setPlayerQuality();
8285
});
8386
};
8487

@@ -90,6 +93,9 @@ export const initPlayer = async () => {
9093
await waitForButtonsAndSetIds();
9194

9295
player.addEventListener('ended', () => sendMessage(Message.QUEUE_NEXT, null, false));
96+
const handlers = getPlayerController();
97+
handlers.store.subscribe(e => e.qualityLevels, setPlayerQuality);
98+
setPlayerQuality();
9399

94100
const { autoplay, autoplayQueue } = options;
95101
console.debug('autoplay?', autoplay, 'autoplayQueue?', autoplayQueue);
@@ -376,4 +382,128 @@ export const setupHistory = () => {
376382
console.error(e);
377383
}
378384
return false;
385+
};
386+
387+
interface Store<T> {
388+
getInitialState: () => T,
389+
getState: () => T & { set: (n: Partial<T>) => void; },
390+
setState: (n: T | ((old: T) => T), options?: any) => void,
391+
subscribe: <U>(selector: (state: T) => U, notify: (old_value: U, new_value: U) => void, options?: Partial<{ equalityFn: (o: U, n: U) => boolean, fireImmediately: boolean; }>) => (() => void),
392+
}
393+
394+
interface VideoStoreData {
395+
airPlayAvailable: boolean;
396+
airPlayConnected: undefined;
397+
aspectRatio: number,
398+
autoplayAvailable: boolean;
399+
autoplayEnabled: boolean;
400+
buffered: number[];
401+
chromecastAvailable: boolean;
402+
chromecastConnected: boolean;
403+
chromecastDeviceName: string;
404+
chromecastQueue: undefined;
405+
currentAutoLevel: any;
406+
currentLanguage: string;
407+
currentTime: number;
408+
deviceSupportsVideoCodecs: boolean;
409+
duration: number,
410+
fullScreenAvailable: boolean;
411+
hasEnded: boolean;
412+
isBuffering: boolean;
413+
isEndScreenShowing: boolean;
414+
isFullScreen: boolean;
415+
isInitialized: boolean;
416+
isMuted: boolean;
417+
isPictureInPicture: boolean;
418+
isPlaying: boolean;
419+
isScrubbing: boolean;
420+
isSeeking: boolean;
421+
mediaId: string;
422+
mediaSessionMetadata: any;
423+
mediaTitle: 'The Hunt For the World\'s Top-4 Most Wanted Leaders';
424+
mediaType: 'video/mp4';
425+
muteChangeAvailable: true;
426+
muxData: any;
427+
pictureInPictureAvailable: undefined;
428+
playbackSpeed: number;
429+
quality: number;
430+
qualityLevels: ({ height: number; } & Record<string, any>)[];
431+
resetTech: () => void;
432+
src: string;
433+
subtitles: { label: string, language: string; }[];
434+
volume: number;
435+
volumeChangeAvailable: boolean;
436+
wasEverPlayed: boolean;
437+
}
438+
439+
interface PlayerHandlers {
440+
store: Store<VideoStoreData>,
441+
hls: any,
442+
options: Record<string, any>,
443+
relativeSeek: any,
444+
videoElement: HTMLVideoElement,
445+
changeLanguage: (name: string) => void,
446+
nextFrame: () => void,
447+
previousFrame: () => void,
448+
setCurrentTime: (time: number) => void,
449+
setHls: (hls: any) => void,
450+
setQualityLevel: (level: number) => void,
451+
}
452+
453+
const getPlayerController = () => {
454+
try {
455+
const root = document.getElementById('video-player');
456+
if (!root) {
457+
console.dev.error('Player not found');
458+
return undefined;
459+
}
460+
for (const key of Object.keys(root)) {
461+
if (key.startsWith('__reactProps')) {
462+
for (const child of (root[key] as any).children) {
463+
if (child.props && child.props.playerHandlers)
464+
return child.props.playerHandlers as PlayerHandlers;
465+
}
466+
break;
467+
}
468+
}
469+
} catch (e) {
470+
console.error(e);
471+
}
472+
console.dev.error('Player handlers not found');
473+
return undefined;
474+
};
475+
476+
const setPlayerQuality = () => {
477+
const handlers = getPlayerController();
478+
if (!handlers) return;
479+
const state = handlers.store.getState();
480+
const levels = state.qualityLevels.map(e => e.height);
481+
if (levels.length === 0) {
482+
console.dev.debug('No quality levels defined, postponing setting of quality');
483+
return;
484+
}
485+
let targetQuality: number;
486+
switch (options.qualityType) {
487+
case QualityType.Highest:
488+
targetQuality = Math.max(...levels);
489+
break;
490+
case QualityType.Lowest:
491+
targetQuality = Math.min(...levels);
492+
break;
493+
case QualityType.PreferredChooseHigher:
494+
// sort ascending, choose first which is higher than preferred
495+
targetQuality = levels.filter(a => a >= options.preferredQuality).toSorted((a, b) => a - b)[0];
496+
break;
497+
case QualityType.PreferredChooseLower:
498+
targetQuality = levels.filter(a => a <= options.preferredQuality).toSorted((a, b) => b - a)[0];
499+
break;
500+
default:
501+
return;
502+
}
503+
if (state.quality === targetQuality) {
504+
console.dev.debug('Target quality', targetQuality, 'already matches current state');
505+
return;
506+
}
507+
console.log('Setting quality to', targetQuality, 'setting was', options.qualityType);
508+
handlers.setQualityLevel(targetQuality);
379509
};

src/styles/helpers/shared.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
white-space: nowrap;
4040
}
4141

42-
input {
42+
input, select {
4343
&.enhancer-text-input {
4444
display: block;
4545
appearance: none;

src/styles/options.scss

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@ form {
8181
}
8282

8383
label + .enhancer-control,
84-
.hint + .enhancer-control {
84+
.hint + .enhancer-control,
85+
.enhancer-control + .enhancer-control {
8586
margin-top: 8px;
8687
}
8788

0 commit comments

Comments
 (0)