@oxymoron.13/tizentube-s95b 0.2.3 → 0.3.1

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.
@@ -1,6 +1,7 @@
1
1
  // Picture in Picture Mode for TizenTube
2
2
 
3
3
  import resolveCommand from "../resolveCommand.js";
4
+ import { s95bMiniLayout, s95bMiniEntered } from './s95bMiniPlayer.js';
4
5
 
5
6
  window.isPipPlaying = false;
6
7
  let PlayerService = null;
@@ -51,14 +52,10 @@ function enablePip() {
51
52
 
52
53
  function onPipEnter() {
53
54
  videoElement.style.removeProperty('inset');
54
- const pipWidth = window.innerWidth / 3.5;
55
- const pipHeight = window.innerHeight / 3.5;
56
- videoElement.style.width = `${pipWidth}px`;
57
- videoElement.style.height = `${pipHeight}px`;
58
- videoElement.style.top = '68vh';
59
- videoElement.style.left = '68vw';
55
+ s95bMiniLayout(videoElement);
60
56
 
61
57
  window.isPipPlaying = true;
58
+ s95bMiniEntered(videoElement);
62
59
  videoElement.removeEventListener('play', onPipEnter);
63
60
  }
64
61
 
@@ -1,26 +1,42 @@
1
1
  /* S95B Performance Edition – GPL-3.0-only */
2
2
  export const STRINGS = Object.freeze({
3
3
  de: Object.freeze({
4
- title: 'S95B · Performance', language: 'Sprache', codec: 'Videocodec', quality: 'Bevorzugte Qualität',
4
+ title: 'S95B · Performance', language: 'Sprache', codec: 'Videocodec', quality: 'Standardqualität (alle Videos)',
5
5
  automatic: 'Automatisch', light: 'Ressourcenschonender Modus',
6
6
  lightHint: 'Schaltet Vorschauen und DeArrow ab; vorherige Werte werden beim Ausschalten wiederhergestellt.',
7
7
  codecHint: 'H.264 liefert bei YouTube in der Regel höchstens 1080p; 4K gibt es nur als VP9 oder AV1. Ein Codec wird nur erzwungen, wenn dadurch keine Qualität verloren geht. Gilt ab dem nächsten Video.',
8
- qualityHint: 'Eine feste Qualität gilt sofort. „Automatisch“ wirkt eventuell erst nach einem Neustart von YouTube.',
8
+ qualityHint: 'Gilt für alle Videos (YouTubes eigene Zeile „Qualität“ ändert nur das laufende Video). Eine feste Qualität gilt sofort. „Automatisch“ wirkt eventuell erst nach einem Neustart von YouTube.',
9
9
  languageHint: 'Gilt für dieses Menü, beim nächsten Öffnen.',
10
10
  statVideo: 'Video', statFrames: 'Verlorene Bilder', statCodec: 'Codec-Wunsch',
11
11
  statUnavailable: 'nicht verfügbar', statWait: 'wartet auf Video',
12
12
  languageDE: 'Deutsch', languageEN: 'English',
13
+ mini: 'Mini-Player',
14
+ miniHint: 'TizenTubes Mini-Player (Eintrag „MiniPlayer“ im Video-Menü). Änderungen gelten beim nächsten Start des Mini-Players. Ein neu gestartetes Video öffnet im Vollbild und ersetzt den Mini-Player.',
15
+ miniOn: 'Mini-Player anbieten',
16
+ miniOnHint: 'Eintrag im Video-Menü und Schaltfläche in der Player-Leiste (die Leiste ändert sich eventuell erst nach einem Neustart von YouTube).',
17
+ miniPos: 'Position', posBR: 'Unten rechts', posBL: 'Unten links', posTR: 'Oben rechts', posTL: 'Oben links',
18
+ miniSize: 'Größe', sizeSmall: 'Klein', sizeMedium: 'Mittel (wie bisher)', sizeLarge: 'Groß',
19
+ miniBrowse: 'Beim Stöbern', browsePlay: 'Weiterlaufen', browsePause: 'Pausieren',
20
+ miniBrowseHint: 'Pausieren: Das Video hält im kleinen Fenster an (der Anfang ist kurz hörbar) und läuft im Vollbild an derselben Stelle weiter.',
13
21
  }),
14
22
  en: Object.freeze({
15
- title: 'S95B · Performance', language: 'Language', codec: 'Video codec', quality: 'Preferred quality',
23
+ title: 'S95B · Performance', language: 'Language', codec: 'Video codec', quality: 'Default quality (all videos)',
16
24
  automatic: 'Automatic', light: 'Low-overhead mode',
17
25
  lightHint: 'Disables previews and DeArrow; the previous values are restored when switched off.',
18
26
  codecHint: 'On YouTube, H.264 usually tops out at 1080p; 4K is only available as VP9 or AV1. A codec is only forced if no quality is lost. Applies from the next video.',
19
- qualityHint: 'A fixed quality applies immediately. “Automatic” may only take effect after restarting YouTube.',
27
+ qualityHint: 'Applies to all videos (YouTube’s own “Quality” row only changes the current video). A fixed quality applies immediately. “Automatic” may only take effect after restarting YouTube.',
20
28
  languageHint: 'Applies to this menu, the next time it is opened.',
21
29
  statVideo: 'Video', statFrames: 'Dropped frames', statCodec: 'Codec preference',
22
30
  statUnavailable: 'not available', statWait: 'waiting for video',
23
31
  languageDE: 'Deutsch', languageEN: 'English',
32
+ mini: 'Mini player',
33
+ miniHint: 'TizenTube’s mini player (“MiniPlayer” row in the video menu). Changes apply the next time the mini player starts. A newly started video opens full screen and replaces the mini player.',
34
+ miniOn: 'Offer mini player',
35
+ miniOnHint: 'Row in the video menu and button in the player bar (the bar may only change after restarting YouTube).',
36
+ miniPos: 'Position', posBR: 'Bottom right', posBL: 'Bottom left', posTR: 'Top right', posTL: 'Top left',
37
+ miniSize: 'Size', sizeSmall: 'Small', sizeMedium: 'Medium (as before)', sizeLarge: 'Large',
38
+ miniBrowse: 'While browsing', browsePlay: 'Keep playing', browsePause: 'Pause',
39
+ miniBrowseHint: 'Pause: the video stops in the small window (its start is briefly audible) and continues full screen from the same position.',
24
40
  })
25
41
  });
26
42
  export function translate(lang, key) {
@@ -6,11 +6,11 @@
6
6
  import { configRead, configWrite, configChangeEmitter } from '../config.js';
7
7
  import { optionShow } from '../ui/settings.js';
8
8
  import { translate } from './s95bI18n.js';
9
+ import { codecMenu, qualityMenu, codecLabel, currentCodec } from './s95bOptions.js';
10
+ import { MINI_POSITIONS, MINI_SIZES, MINI_BROWSE } from './s95bMiniPlayer.js';
9
11
 
10
12
  const STATE_KEY = 's95b-perf-edition-v1';
11
13
  const EDITED = ['enablePreviews', 'enableDeArrow', 'enableDeArrowThumbnails'];
12
- const CODECS = ['any', 'vp9', 'av01', 'avc1'];
13
- const QUALITIES = ['auto', '2160p', '1440p', '1080p', '720p'];
14
14
  const defaultState = { lang: 'de', light: false, previous: null };
15
15
  let state = { ...defaultState };
16
16
 
@@ -29,16 +29,6 @@ function save() {
29
29
  }
30
30
  function t(key) { return translate(state.lang, key); }
31
31
 
32
- function codecLabel(value) {
33
- return value === 'any' ? t('automatic') : value === 'vp9' ? 'VP9' : value === 'av01' ? 'AV1' : 'H.264';
34
- }
35
- function qualityLabel(value) {
36
- return value === 'auto' ? t('automatic') : value === '2160p' ? '2160p (4K)' : value;
37
- }
38
- function currentCodec() {
39
- const value = configRead('preferredVideoCodec');
40
- return CODECS.indexOf(value) >= 0 ? value : 'any';
41
- }
42
32
 
43
33
  // Low-overhead mode: switches off a few TizenTube features and remembers the previous values.
44
34
  function turnLight(enable) {
@@ -61,6 +51,16 @@ function turnLight(enable) {
61
51
  save();
62
52
  }
63
53
 
54
+ // Source frame rate as YouTube's own stats report it ("3840x2160@24 / ..."), same source as upstream autoFrameRate.js.
55
+ function frameRate() {
56
+ try {
57
+ const player = document.querySelector('.html5-video-player');
58
+ if (!player || typeof player.getStatsForNerds !== 'function') return '';
59
+ const match = String(player.getStatsForNerds().resolution || '').match(/@([\d.]+)/);
60
+ return match ? ' @' + match[1] : '';
61
+ } catch (_) { return ''; }
62
+ }
63
+
64
64
  // One snapshot of what the web player reports, taken each time the menu opens.
65
65
  function statsLine() {
66
66
  const video = document.querySelector('video');
@@ -69,7 +69,7 @@ function statsLine() {
69
69
  if (video) {
70
70
  const width = video.videoWidth || 0;
71
71
  const height = video.videoHeight || 0;
72
- size = width > 0 && height > 0 ? width + ' × ' + height : t('statUnavailable');
72
+ size = width > 0 && height > 0 ? width + ' × ' + height + frameRate() : t('statUnavailable');
73
73
  if (typeof video.getVideoPlaybackQuality === 'function') {
74
74
  try {
75
75
  const q = video.getVideoPlaybackQuality();
@@ -78,7 +78,29 @@ function statsLine() {
78
78
  }
79
79
  }
80
80
  return t('statVideo') + ': ' + size + ' | ' + t('statFrames') + ': ' + dropped +
81
- ' | ' + t('statCodec') + ': ' + codecLabel(currentCodec());
81
+ ' | ' + t('statCodec') + ': ' + codecLabel(t, currentCodec());
82
+ }
83
+
84
+ const MINI_DEFAULTS = { s95bMiniPos: 'br', s95bMiniSize: 'medium', s95bMiniBrowse: 'play' };
85
+ const MINI_VALUES = { s95bMiniPos: MINI_POSITIONS, s95bMiniSize: MINI_SIZES, s95bMiniBrowse: MINI_BROWSE };
86
+ const MINI_LABELS = { br: 'posBR', bl: 'posBL', tr: 'posTR', tl: 'posTL', small: 'sizeSmall', medium: 'sizeMedium',
87
+ large: 'sizeLarge', play: 'browsePlay', pause: 'browsePause' };
88
+
89
+ function miniChoice(title, key, hint) {
90
+ return { name: t(title), value: null, menuId: 's95b-' + key,
91
+ menuHeader: { title: t(title), subtitle: hint ? t(hint) : t('miniHint') },
92
+ options: MINI_VALUES[key].map(value => ({ name: t(MINI_LABELS[value]), key, value })) };
93
+ }
94
+ function miniMenu() {
95
+ return { name: t('mini'), icon: 'CLEAR_COOKIES', value: null, menuId: 's95b-mini',
96
+ menuHeader: { title: t('mini'), subtitle: t('miniHint') },
97
+ options: [
98
+ // TizenTube's own switch; the fork extends it to the "MiniPlayer" row in the video menu.
99
+ { name: t('miniOn'), subtitle: t('miniOnHint'), value: 'enableMPButton' },
100
+ miniChoice('miniPos', 's95bMiniPos'),
101
+ miniChoice('miniSize', 's95bMiniSize'),
102
+ miniChoice('miniBrowse', 's95bMiniBrowse', 'miniBrowseHint')
103
+ ] };
82
104
  }
83
105
 
84
106
  function buildOptions() {
@@ -89,12 +111,9 @@ function buildOptions() {
89
111
  { name: t('languageDE'), key: 's95bLanguage', value: 'de' },
90
112
  { name: t('languageEN'), key: 's95bLanguage', value: 'en' }
91
113
  ] },
92
- { name: t('codec'), icon: 'VIDEO_QUALITY', value: null, menuId: 's95b-codec',
93
- menuHeader: { title: t('codec'), subtitle: t('codecHint') },
94
- options: CODECS.map(codec => ({ name: codecLabel(codec), key: 'preferredVideoCodec', value: codec })) },
95
- { name: t('quality'), icon: 'VIDEO_QUALITY', value: null, menuId: 's95b-quality',
96
- menuHeader: { title: t('quality'), subtitle: t('qualityHint') },
97
- options: QUALITIES.map(quality => ({ name: qualityLabel(quality), key: 'preferredVideoQuality', value: quality })) },
114
+ codecMenu(t),
115
+ qualityMenu(t),
116
+ miniMenu(),
98
117
  { name: t('light'), subtitle: t('lightHint'), value: 's95bLowOverhead' }
99
118
  ];
100
119
  }
@@ -131,6 +150,9 @@ function init() {
131
150
  // Menu entries read their current value from the TizenTube config, so mirror our stored state into it first.
132
151
  if (configRead('s95bLowOverhead') !== state.light) configWrite('s95bLowOverhead', state.light);
133
152
  if (configRead('s95bLanguage') !== state.lang) configWrite('s95bLanguage', state.lang);
153
+ Object.keys(MINI_DEFAULTS).forEach(key => {
154
+ if (MINI_VALUES[key].indexOf(configRead(key)) < 0) configWrite(key, MINI_DEFAULTS[key]);
155
+ });
134
156
  configChangeEmitter.addEventListener('configChange', onConfigChange);
135
157
  // Capture phase on window: only the yellow key is consumed.
136
158
  window.addEventListener('keydown', onKeys, true);
@@ -0,0 +1,36 @@
1
+ /* S95B Performance Edition – GPL-3.0-only
2
+ * Settings for TizenTube's own mini player (features/pictureInPicture.js): position, size and
3
+ * whether the video keeps playing while browsing. Called from the two patched lines in pictureInPicture.js.
4
+ * Defaults reproduce upstream exactly: 1/3.5 of the screen at 68vh / 68vw (bottom right), keeps playing.
5
+ */
6
+ import { configRead } from '../config.js';
7
+
8
+ export const MINI_POSITIONS = ['br', 'bl', 'tr', 'tl'];
9
+ export const MINI_SIZES = ['small', 'medium', 'large'];
10
+ export const MINI_BROWSE = ['play', 'pause'];
11
+ const DIVISOR = { small: 4.5, medium: 3.5, large: 2.5 };
12
+ // Upstream's gap between the mini player and the right/bottom screen edge, in vw/vh (100 - 68 - 100/3.5).
13
+ const GAP = 100 - 68 - 100 / 3.5;
14
+ const round = value => Math.round(value * 100) / 100;
15
+
16
+ export function s95bMiniLayout(videoElement) {
17
+ const position = MINI_POSITIONS.indexOf(configRead('s95bMiniPos')) >= 0 ? configRead('s95bMiniPos') : 'br';
18
+ const divisor = DIVISOR[configRead('s95bMiniSize')] || DIVISOR.medium;
19
+ const far = round(100 - 100 / divisor - GAP);
20
+ const near = round(GAP);
21
+ videoElement.style.width = `${window.innerWidth / divisor}px`;
22
+ videoElement.style.height = `${window.innerHeight / divisor}px`;
23
+ videoElement.style.top = (position[0] === 'b' ? far : near) + 'vh';
24
+ videoElement.style.left = (position[1] === 'r' ? far : near) + 'vw';
25
+ }
26
+
27
+ // "Pause while browsing": pause once the reloaded video has really started, so YouTube's own play() call
28
+ // is not interrupted. Going back to full screen starts the video again from this position (upstream behavior).
29
+ export function s95bMiniEntered(videoElement) {
30
+ if (configRead('s95bMiniBrowse') !== 'pause') return;
31
+ const pauseOnce = () => {
32
+ videoElement.removeEventListener('playing', pauseOnce);
33
+ if (window.isPipPlaying) videoElement.pause();
34
+ };
35
+ videoElement.addEventListener('playing', pauseOnce);
36
+ }
@@ -0,0 +1,30 @@
1
+ /* S95B Performance Edition – GPL-3.0-only
2
+ * Codec and default-quality choices, shared by the yellow menu and the playback settings popup.
3
+ * Both write TizenTube's own config keys (preferredVideoCodec, preferredVideoQuality).
4
+ */
5
+ import { configRead } from '../config.js';
6
+
7
+ export const CODECS = ['any', 'vp9', 'av01', 'avc1'];
8
+ export const QUALITIES = ['auto', '2160p', '1440p', '1080p', '720p'];
9
+
10
+ export function codecLabel(t, value) {
11
+ return value === 'any' ? t('automatic') : value === 'vp9' ? 'VP9' : value === 'av01' ? 'AV1' : 'H.264';
12
+ }
13
+ export function qualityLabel(t, value) {
14
+ return value === 'auto' ? t('automatic') : value === '2160p' ? '2160p (4K)' : String(value);
15
+ }
16
+ export function currentCodec() {
17
+ const value = configRead('preferredVideoCodec');
18
+ return CODECS.indexOf(value) >= 0 ? value : 'any';
19
+ }
20
+
21
+ export function codecMenu(t) {
22
+ return { name: t('codec'), icon: 'VIDEO_QUALITY', value: null, menuId: 's95b-codec',
23
+ menuHeader: { title: t('codec'), subtitle: t('codecHint') },
24
+ options: CODECS.map(codec => ({ name: codecLabel(t, codec), key: 'preferredVideoCodec', value: codec })) };
25
+ }
26
+ export function qualityMenu(t) {
27
+ return { name: t('quality'), icon: 'VIDEO_QUALITY', value: null, menuId: 's95b-quality',
28
+ menuHeader: { title: t('quality'), subtitle: t('qualityHint') },
29
+ options: QUALITIES.map(quality => ({ name: qualityLabel(t, quality), key: 'preferredVideoQuality', value: quality })) };
30
+ }
@@ -0,0 +1,29 @@
1
+ /* S95B Performance Edition – GPL-3.0-only
2
+ * Two entries for YouTube's playback settings popup (opened while a video plays): video codec and
3
+ * default quality for all videos. They open the same YouTube-drawn choice lists as the yellow menu.
4
+ * buttonItem is handed in by resolveCommand.js, which already imports it.
5
+ */
6
+ import { configRead } from '../config.js';
7
+ import { translate } from './s95bI18n.js';
8
+ import { codecMenu, qualityMenu, codecLabel, qualityLabel, currentCodec } from './s95bOptions.js';
9
+
10
+ export function s95bPlaybackItems(buttonItem) {
11
+ const lang = configRead('s95bLanguage') === 'en' ? 'en' : 'de';
12
+ const t = key => translate(lang, key);
13
+ const entry = (menu, subtitle) => buttonItem({ title: menu.name, subtitle }, { icon: menu.icon }, [{
14
+ customAction: {
15
+ action: 'OPTIONS_SHOW',
16
+ parameters: {
17
+ options: menu.options,
18
+ selectedIndex: Math.max(0, menu.options.findIndex(o => o.value === configRead(o.key))),
19
+ update: false,
20
+ menuId: menu.menuId,
21
+ menuHeader: menu.menuHeader
22
+ }
23
+ }
24
+ }]);
25
+ return [
26
+ entry(codecMenu(t), codecLabel(t, currentCodec())),
27
+ entry(qualityMenu(t), qualityLabel(t, configRead('preferredVideoQuality')))
28
+ ];
29
+ }
@@ -1,5 +1,6 @@
1
1
  import { configWrite, configRead } from './config.js';
2
2
  import { enablePip } from './features/pictureInPicture.js';
3
+ import { s95bPlaybackItems } from './features/s95bPlayback.js';
3
4
  import modernUI, { optionShow } from './ui/settings.js';
4
5
  import { speedSettings } from './ui/speedUI.js';
5
6
  import { showToast, buttonItem, showModal, QrCodeRenderer, overlayPanelItemListRenderer, overlayMessageRenderer } from './ui/ytUI.js';
@@ -96,7 +97,7 @@ export function patchResolveCommand() {
96
97
  }
97
98
  }
98
99
 
99
- cmd.openPopupAction.popup.overlaySectionRenderer.overlay.overlayTwoPanelRenderer.actionPanel.overlayPanelRenderer.content.overlayPanelItemListRenderer.items.splice(2, 0,
100
+ if (configRead('enableMPButton')) cmd.openPopupAction.popup.overlaySectionRenderer.overlay.overlayTwoPanelRenderer.actionPanel.overlayPanelRenderer.content.overlayPanelItemListRenderer.items.splice(2, 0,
100
101
  buttonItem(
101
102
  { title: t('player.miniPlayer') },
102
103
  { icon: 'CLEAR_COOKIES' }, [
@@ -151,6 +152,10 @@ export function patchResolveCommand() {
151
152
  ])
152
153
  );
153
154
  }
155
+ // S95B: video codec and default quality entries; a failure here never breaks the popup.
156
+ try {
157
+ cmd.openPopupAction.popup.overlaySectionRenderer.overlay.overlayTwoPanelRenderer.actionPanel.overlayPanelRenderer.content.overlayPanelItemListRenderer.items.splice(1, 0, ...s95bPlaybackItems(buttonItem));
158
+ } catch (e) { console.warn('[S95B] playback settings entries skipped:', e); }
154
159
  } else if (cmd?.watchEndpoint?.videoId) {
155
160
  window.isPipPlaying = false;
156
161
  const ytlrPlayerContainer = document.querySelector('ytlr-player-container');
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@oxymoron.13/tizentube-s95b",
3
3
  "appName": "TizenTube S95B Performance (Fork)",
4
- "version": "0.2.3",
4
+ "version": "0.3.1",
5
5
  "description": "Unofficial fork of TizenTube (@foxreis/tizentube) for TizenBrew: reversible performance profile, German/English settings, smaller bundle.",
6
6
  "packageType": "mods",
7
7
  "websiteURL": "https://youtube.com/tv?additionalDataUrl=http%3A%2F%2Flocalhost%3A8085%2Fdial%2Fapps%2FYouTube",
@@ -115,4 +115,7 @@ setInterval(() => {
115
115
 
116
116
  app.listen(PORT, () => {
117
117
  dialServer.start();
118
+ }).on('error', (e) => {
119
+ // S95B: without a handler, EADDRINUSE (e.g. original TizenTube running too) is an unhandled error event.
120
+ console.error('[S95B] service port ' + PORT + ' unavailable:', e && e.code);
118
121
  });