videojs: refactor i18n (breaking up 'events' file)

## Issue
I couldn't find where the i18n went because the "events" refactor is just as confusing as the original -- unrelated things are still lumped together in a file.

Also, factoring based on events isn't useful -- it is features that drive what events are needed, not the other way around. This forces features to register events here, and do other things elsewhere? It will be more intuitive to have a one-file-per-feature structure.

## Change
Use existing frameworks to encapsulate things to manageable units/features:
(1) the React useEffect files (can be used isolate out React features like 'tap-to-mute' handling).
(2) the videojs plugins framework.
This commit is contained in:
infinite-persistence 2022-05-17 13:47:35 +08:00
parent ddc6c7ac98
commit 36ddc69c13
No known key found for this signature in database
GPG key ID: B9C3252EDC3D0AA0
3 changed files with 124 additions and 90 deletions

View file

@ -0,0 +1,101 @@
/**
* Although videojs has its own i18n system, it is not trivial to pass our
* translation data to it. Instead of maintaining two sets of localization
* systems, we'll just ignore the videojs one and update the strings ourselves
* through events.
*
* We use events because that's the only way to update components with dynamic
* text like "Play/Pause" in videojs. We can't just define the set of texts at
* initialization, unless we use custom components to do that.
*
* For the case of 'MuteToggle', videojs changes the text at 'loadstart', so
* this event was chosen as the "lowest common denominator" to update the other
* static-text components.
*/
// @flow
import videojs from 'video.js';
import type { Player } from '../../videojs';
const VERSION = '1.0.0';
const defaultOptions = {};
function setLabel(controlBar, childName, label) {
try {
controlBar.getChild(childName).controlText(label);
} catch (e) {
// We want to be notified, at least on dev, over any structural changes,
// so don't check for null children and let the error surface.
// @if process.env.NODE_ENV!='production'
console.error(e);
// @endif
}
}
function resolveCtrlText(e, player) {
if (player) {
const ctrlBar = player.getChild('controlBar');
switch (e.type) {
case 'play':
setLabel(ctrlBar, 'PlayToggle', __('Pause (space)'));
break;
case 'pause':
setLabel(ctrlBar, 'PlayToggle', __('Play (space)'));
break;
case 'volumechange':
ctrlBar
.getChild('VolumePanel')
.getChild('MuteToggle')
.controlText(player.muted() || player.volume() === 0 ? __('Unmute (m)') : __('Mute (m)'));
break;
case 'fullscreenchange':
setLabel(ctrlBar, 'FullscreenToggle', player.isFullscreen() ? __('Exit Fullscreen (f)') : __('Fullscreen (f)'));
break;
case 'loadstart':
// --- Do everything ---
setLabel(ctrlBar, 'PlaybackRateMenuButton', __('Playback Rate (<, >)'));
setLabel(ctrlBar, 'QualityButton', __('Quality'));
setLabel(ctrlBar, 'PlayNextButton', __('Play Next (SHIFT+N)'));
setLabel(ctrlBar, 'PlayPreviousButton', __('Play Previous (SHIFT+P)'));
setLabel(ctrlBar, 'TheaterModeButton', __('Toggle Theater Mode (t)'));
setLabel(ctrlBar, 'AutoplayNextButton', __('Toggle Autoplay Next'));
ctrlBar
.getChild('VolumePanel')
.getChild('MuteToggle')
.controlText(player.muted() || player.volume() === 0 ? __('Unmute (m)') : __('Mute (m)'));
resolveCtrlText({ type: 'play' });
resolveCtrlText({ type: 'pause' });
resolveCtrlText({ type: 'volumechange' });
resolveCtrlText({ type: 'fullscreenchange' });
break;
default:
// @if process.env.NODE_ENV!='production'
throw Error('Unexpected: ' + e.type);
// @endif
}
}
}
function onPlayerReady(player: Player, options: {}) {
const h = (e) => resolveCtrlText(e, player);
player.on('play', h);
player.on('pause', h);
player.on('loadstart', h);
player.on('fullscreenchange', h);
player.on('volumechange', h);
}
/**
* Odysee custom i18n plugin.
* @param options
*/
function i18n(options: {}) {
this.ready(() => onPlayerReady(this, videojs.mergeOptions(defaultOptions, options)));
}
videojs.registerPlugin('i18n', i18n);
i18n.VERSION = VERSION;
export default i18n;

View file

@ -10,13 +10,6 @@ const TAP = {
RETRY: 'RETRY', RETRY: 'RETRY',
}; };
const setLabel = (controlBar, childName, label) => {
const c = controlBar.getChild(childName);
if (c) {
c.controlText(label);
}
};
const VideoJsEvents = ({ const VideoJsEvents = ({
tapToUnmuteRef, tapToUnmuteRef,
tapToRetryRef, tapToRetryRef,
@ -96,63 +89,6 @@ const VideoJsEvents = ({
}); });
} }
// Override the player's control text. We override to:
// 1. Add keyboard shortcut to the tool-tip.
// 2. Override videojs' i18n and use our own (don't want to have 2 systems).
//
// Notes:
// - For dynamic controls (e.g. play/pause), those unfortunately need to be
// updated again at their event-listener level (that's just the way videojs
// updates the text), hence the need to listen to 'play', 'pause' and 'volumechange'
// on top of just 'loadstart'.
// - videojs changes the MuteToggle text at 'loadstart', so this was chosen
// as the listener to update static texts.
function resolveCtrlText(e) {
const player = playerRef.current;
if (player) {
const ctrlBar = player.getChild('controlBar');
switch (e.type) {
case 'play':
setLabel(ctrlBar, 'PlayToggle', __('Pause (space)'));
break;
case 'pause':
setLabel(ctrlBar, 'PlayToggle', __('Play (space)'));
break;
case 'volumechange':
ctrlBar
.getChild('VolumePanel')
.getChild('MuteToggle')
.controlText(player.muted() || player.volume() === 0 ? __('Unmute (m)') : __('Mute (m)'));
break;
case 'fullscreenchange':
setLabel(
ctrlBar,
'FullscreenToggle',
player.isFullscreen() ? __('Exit Fullscreen (f)') : __('Fullscreen (f)')
);
break;
case 'loadstart':
// --- Do everything ---
setLabel(ctrlBar, 'PlaybackRateMenuButton', __('Playback Rate (<, >)'));
setLabel(ctrlBar, 'QualityButton', __('Quality'));
setLabel(ctrlBar, 'PlayNextButton', __('Play Next (SHIFT+N)'));
setLabel(ctrlBar, 'PlayPreviousButton', __('Play Previous (SHIFT+P)'));
setLabel(ctrlBar, 'TheaterModeButton', videoTheaterMode ? __('Default Mode (t)') : __('Theater Mode (t)'));
setLabel(ctrlBar, 'AutoplayNextButton', autoplaySetting ? __('Autoplay Next On') : __('Autoplay Next Off'));
resolveCtrlText({ type: 'play' });
resolveCtrlText({ type: 'pause' });
resolveCtrlText({ type: 'volumechange' });
resolveCtrlText({ type: 'fullscreenchange' });
break;
default:
if (isDev) throw Error('Unexpected: ' + e.type);
break;
}
}
}
function onInitialPlay() { function onInitialPlay() {
const player = playerRef.current; const player = playerRef.current;
@ -281,13 +217,8 @@ const VideoJsEvents = ({
function initializeEvents() { function initializeEvents() {
const player = playerRef.current; const player = playerRef.current;
// Add various event listeners to player
player.one('play', onInitialPlay); player.one('play', onInitialPlay);
player.on('play', resolveCtrlText);
player.on('pause', resolveCtrlText);
player.on('loadstart', resolveCtrlText);
player.on('fullscreenchange', resolveCtrlText);
player.on('volumechange', resolveCtrlText);
player.on('volumechange', onVolumeChange); player.on('volumechange', onVolumeChange);
player.on('error', onError); player.on('error', onError);
// custom tracking plugin, event used for watchman data, and marking view/getting rewards // custom tracking plugin, event used for watchman data, and marking view/getting rewards
@ -316,7 +247,6 @@ const VideoJsEvents = ({
setTimeout(() => { setTimeout(() => {
// Do not jump ahead if user has paused the player // Do not jump ahead if user has paused the player
if (player.paused()) return; if (player.paused()) return;
player.liveTracker.seekToLiveEdge(); player.liveTracker.seekToLiveEdge();
}, 5 * 1000); }, 5 * 1000);
}); });

View file

@ -20,6 +20,7 @@ import Chromecast from './chromecast';
import playerjs from 'player.js'; import playerjs from 'player.js';
import qualityLevels from 'videojs-contrib-quality-levels'; import qualityLevels from 'videojs-contrib-quality-levels';
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import i18n from './plugins/videojs-i18n/plugin';
import recsys from './plugins/videojs-recsys/plugin'; import recsys from './plugins/videojs-recsys/plugin';
// import runAds from './ads'; // import runAds from './ads';
import videojs from 'video.js'; import videojs from 'video.js';
@ -37,12 +38,16 @@ export type Player = {
claimSrcOriginal: ?{ src: string, type: string }, claimSrcOriginal: ?{ src: string, type: string },
claimSrcVhs: ?{ src: string, type: string }, claimSrcVhs: ?{ src: string, type: string },
isLivestream?: boolean, isLivestream?: boolean,
// -- original -- // -- plugins ---
mobileUi: (any) => void,
chromecast: (any) => void,
overlay: (any) => void,
hlsQualitySelector: ?any,
i18n: (any) => void,
// -- base videojs --
controlBar: { addChild: (string, any) => void }, controlBar: { addChild: (string, any) => void },
loadingSpinner: any, loadingSpinner: any,
autoplay: (any) => boolean, autoplay: (any) => boolean,
chromecast: (any) => void,
hlsQualitySelector: ?any,
tech: (?boolean) => { vhs: ?any }, tech: (?boolean) => { vhs: ?any },
currentTime: (?number) => number, currentTime: (?number) => number,
dispose: () => void, dispose: () => void,
@ -52,11 +57,9 @@ export type Player = {
exitFullscreen: () => boolean, exitFullscreen: () => boolean,
getChild: (string) => any, getChild: (string) => any,
isFullscreen: () => boolean, isFullscreen: () => boolean,
mobileUi: (any) => void,
muted: (?boolean) => boolean, muted: (?boolean) => boolean,
on: (string, (any) => void) => void, on: (string, (any) => void) => void,
one: (string, (any) => void) => void, one: (string, (any) => void) => void,
overlay: (any) => void,
play: () => Promise<any>, play: () => Promise<any>,
playbackRate: (?number) => number, playbackRate: (?number) => number,
readyState: () => number, readyState: () => number,
@ -104,21 +107,19 @@ type Props = {
const IS_IOS = platform.isIOS(); const IS_IOS = platform.isIOS();
if (!Object.keys(videojs.getPlugins()).includes('eventTracking')) { const PLUGIN_MAP = {
videojs.registerPlugin('eventTracking', eventTracking); eventTracking: eventTracking,
} hlsQualitySelector: hlsQualitySelector,
qualityLevels: qualityLevels,
recsys: recsys,
i18n: i18n,
};
if (!Object.keys(videojs.getPlugins()).includes('hlsQualitySelector')) { Object.entries(PLUGIN_MAP).forEach(([pluginName, plugin]) => {
videojs.registerPlugin('hlsQualitySelector', hlsQualitySelector); if (!Object.keys(videojs.getPlugins()).includes(pluginName)) {
} videojs.registerPlugin(pluginName, plugin);
}
if (!Object.keys(videojs.getPlugins()).includes('qualityLevels')) { });
videojs.registerPlugin('qualityLevels', qualityLevels);
}
if (!Object.keys(videojs.getPlugins()).includes('recsys')) {
videojs.registerPlugin('recsys', recsys);
}
// **************************************************************************** // ****************************************************************************
// VideoJs // VideoJs
@ -277,6 +278,8 @@ export default React.memo<Props>(function VideoJs(props: Props) {
// Initialize mobile UI. // Initialize mobile UI.
player.mobileUi(); player.mobileUi();
player.i18n();
if (!embedded) { if (!embedded) {
window.player.bigPlayButton && window.player.bigPlayButton.hide(); window.player.bigPlayButton && window.player.bigPlayButton.hide();
} else { } else {