Skip to content

Commit

Permalink
Streaming video overlay for better mobile support
Browse files Browse the repository at this point in the history
  • Loading branch information
killergerbah committed Feb 22, 2024
1 parent bcfae2c commit 02a2668
Show file tree
Hide file tree
Showing 37 changed files with 749 additions and 95 deletions.
87 changes: 26 additions & 61 deletions common/app/components/Controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import MenuItem from '@material-ui/core/MenuItem';
import useMediaQuery from '@material-ui/core/useMediaQuery';
import MoreVertIcon from '@material-ui/icons/MoreVert';
import { isMobile } from 'react-device-detect';
import SubtitleOffsetInput from './SubtitleOffsetInput';
const useControlStyles = makeStyles((theme) => ({
container: {
position: 'absolute',
Expand Down Expand Up @@ -680,7 +681,6 @@ export default function Controls({
const lastNumberInputChangeTimestampRef = useRef<number>(Date.now());
const lastShowRef = useRef<boolean>(true);
const forceShowRef = useRef<boolean>(false);
const [offsetInputWidth, setOffsetInputWidth] = useState<number>(5);
const [playbackRateInputWidth, setPlaybackRateInputWidth] = useState<number>(5);
const offsetInputRef = useRef<HTMLInputElement>();
const playbackRateInputRef = useRef<HTMLInputElement>();
Expand Down Expand Up @@ -766,22 +766,6 @@ export default function Controls({

useEffect(() => onShow?.(show), [onShow, show]);

const updateOffset = useCallback((offset: number) => {
if (offsetInputRef.current) {
if (offset === 0) {
offsetInputRef.current.value = '';
setOffsetInputWidth(5);
} else {
const offsetSeconds = offset / 1000;
const value = offsetSeconds >= 0 ? '+' + offsetSeconds.toFixed(2) : String(offsetSeconds.toFixed(2));
offsetInputRef.current.value = value;
lastNumberInputChangeTimestampRef.current = Date.now();
setOffsetInputWidth(value.length);
}
offsetInputRef.current.blur();
}
}, []);

const updatePlaybackRate = useCallback((playbackRate: number) => {
if (playbackRateInputRef.current) {
if (playbackRate === 1) {
Expand All @@ -797,27 +781,22 @@ export default function Controls({
}
}, []);

const handleOffsetChange = useCallback(
(offset: number) => {
lastNumberInputChangeTimestampRef.current = Date.now();
onOffsetChange(offset);
},
[onOffsetChange]
);

useEffect(() => {
if (disableKeyEvents) {
return;
}

function handleKey(event: KeyboardEvent) {
if (event.key === 'Enter') {
if (offsetInputRef.current === document.activeElement) {
const newOffset = Number(offsetInputRef.current.value);

if (newOffset === offset) {
updateOffset(offset);
return;
}

if (Number.isNaN(newOffset)) {
return;
}

onOffsetChange(newOffset * 1000);
} else if (playbackRateInputRef.current === document.activeElement) {
if (playbackRateInputRef.current === document.activeElement) {
const newPlaybackRate = Number(playbackRateInputRef.current.value);

if (playbackRate === newPlaybackRate) {
Expand All @@ -839,32 +818,25 @@ export default function Controls({
return () => {
window.removeEventListener('keydown', handleKey);
};
}, [
onOffsetChange,
onPlaybackRateChange,
updateOffset,
updatePlaybackRate,
offset,
playbackRate,
disableKeyEvents,
]);
}, [onOffsetChange, onPlaybackRateChange, updatePlaybackRate, offset, playbackRate, disableKeyEvents]);

const handleNumberInputClicked = useCallback((e: React.MouseEvent<HTMLInputElement>) => {
const inputElement = e.target as HTMLInputElement;
inputElement.setSelectionRange(0, inputElement.value?.length || 0);
}, []);

useEffect(() => {
const interval = setInterval(() => {
forceUpdate();
}, 100);

return () => clearInterval(interval);
}, [forceUpdate]);
clock.onEvent('settime', () => forceUpdate());
}, [clock, forceUpdate]);

useEffect(() => {
updateOffset(offset);
}, [offset, updateOffset]);
if (!show || !playing) {
return;
}

const interval = setInterval(() => forceUpdate(), 100);
return () => clearInterval(interval);
}, [show, playing, forceUpdate]);

useEffect(() => {
updatePlaybackRate(playbackRate);
Expand Down Expand Up @@ -1116,19 +1088,12 @@ export default function Controls({
</Grid>
{offsetEnabled && !showVolumeBar && !isReallySmallScreen && (
<Grid item>
<Tooltip title={t('controls.subtitleOffset')!}>
<Input
style={{
width: `${offsetInputWidth}ch`,
}}
inputRef={offsetInputRef}
disableUnderline={true}
className={classes.numberInput}
placeholder={'±' + Number(0).toFixed(2)}
onClick={handleNumberInputClicked}
onChange={(e) => setOffsetInputWidth(Math.max(5, e.target.value.length))}
/>
</Tooltip>
<SubtitleOffsetInput
inputRef={offsetInputRef}
offset={offset}
onOffset={handleOffsetChange}
disableKeyEvents={disableKeyEvents}
/>
</Grid>
)}
{playbackRateEnabled && !showVolumeBar && !isReallySmallScreen && (
Expand Down
1 change: 1 addition & 0 deletions common/app/components/SettingsDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export default function SettingsDialog({
anki={anki}
extensionInstalled={extension.installed}
extensionSupportsAppIntegration={extension.supportsAppIntegration}
extensionSupportsOverlay={extension.supportsStreamingVideoOverlay}
insideApp
chromeKeyBinds={extension.extensionCommands}
onOpenChromeExtensionShortcuts={extension.openShortcuts}
Expand Down
107 changes: 107 additions & 0 deletions common/app/components/SubtitleOffsetInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import Input from '@material-ui/core/Input';
import Tooltip from '@material-ui/core/Tooltip';
import React, { MutableRefObject, useCallback, useEffect, useRef, useState } from 'react';
import { makeStyles } from '@material-ui/core/styles';
import { useTranslation } from 'react-i18next';

const useStyles = makeStyles({
input: {
height: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
fontSize: 20,
marginLeft: 10,
width: 100,
color: '#fff',
pointerEvents: 'auto',
},
});

interface Props {
inputRef: MutableRefObject<HTMLInputElement | undefined>;
offset: number;
onOffset: (offset: number) => void;
disableKeyEvents?: boolean;
}

export default function SubtitleOffsetInput({ inputRef, offset, onOffset, disableKeyEvents }: Props) {
const { t } = useTranslation();
const classes = useStyles();
const [offsetInputWidth, setOffsetInputWidth] = useState<number>(5);
const handleNumberInputClicked = useCallback((e: React.MouseEvent<HTMLInputElement>) => {
const inputElement = e.target as HTMLInputElement;
inputElement.setSelectionRange(0, inputElement.value?.length || 0);
}, []);

const updateOffset = useCallback(
(offset: number) => {
if (!inputRef.current) {
return;
}

if (offset === 0) {
inputRef.current.value = '';
setOffsetInputWidth(5);
} else {
const offsetSeconds = offset / 1000;
const value = offsetSeconds >= 0 ? '+' + offsetSeconds.toFixed(2) : String(offsetSeconds.toFixed(2));
inputRef.current.value = value;
setOffsetInputWidth(value.length);
}

inputRef.current.blur();
},
[inputRef]
);
useEffect(() => {
updateOffset(offset);
}, [offset, updateOffset]);

useEffect(() => {
if (disableKeyEvents) {
return;
}

function handleKey(event: KeyboardEvent) {
if (event.key === 'Enter') {
if (inputRef.current !== null && inputRef.current === document.activeElement) {
const newOffset = Number(inputRef.current.value);

if (newOffset === offset) {
updateOffset(offset);
return;
}

if (Number.isNaN(newOffset)) {
return;
}

onOffset(newOffset * 1000);
}
}
}

window.addEventListener('keydown', handleKey);

return () => {
window.removeEventListener('keydown', handleKey);
};
}, [updateOffset, onOffset, disableKeyEvents, inputRef, offset]);

return (
<Tooltip title={t('controls.subtitleOffset')!}>
<Input
style={{
width: `${offsetInputWidth}ch`,
}}
inputRef={inputRef}
disableUnderline={true}
className={classes.input}
placeholder={'±' + Number(0).toFixed(2)}
onClick={handleNumberInputClicked}
onChange={(e) => setOffsetInputWidth(Math.max(5, e.target.value.length))}
/>
</Tooltip>
);
}
6 changes: 0 additions & 6 deletions common/app/components/SubtitlePlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -295,12 +295,6 @@ const SubtitleRow = React.memo(function SubtitleRow({
);
});

interface ResizeHandleStylesProps {
isResizing: boolean;
appBarHidden: boolean;
appBarHeight: number;
}

interface ResizeHandleProps extends React.HTMLAttributes<HTMLDivElement> {
isResizing: boolean;
}
Expand Down
4 changes: 4 additions & 0 deletions common/app/services/chrome-extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ export default class ChromeExtension {
window.addEventListener('message', this.windowEventListener);
}

get supportsStreamingVideoOverlay() {
return this.installed && gte(this.version, '1.1.0');
}

get supportsAppIntegration() {
return this.installed && gte(this.version, '1.0.0');
}
Expand Down
5 changes: 3 additions & 2 deletions common/app/services/clock.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
export type ClockEvent = 'stop' | 'start';
export type ClockEvent = 'stop' | 'start' | 'settime';

export default class Clock {
private _accumulated: number;
private _started: boolean;
private _startTime?: number;
private _rate = 1;
private _callbacks: { [event in ClockEvent]: (() => void)[] } = { stop: [], start: [] };
private _callbacks: { [event in ClockEvent]: (() => void)[] } = { stop: [], start: [], settime: [] };

constructor() {
this._accumulated = 0;
Expand Down Expand Up @@ -65,6 +65,7 @@ export default class Clock {
} else {
this._accumulated = time;
}
this._fireEvent('settime');
}

progress(max: number) {
Expand Down
18 changes: 18 additions & 0 deletions common/components/SettingsForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,7 @@ interface Props {
anki: Anki;
extensionInstalled: boolean;
extensionSupportsAppIntegration: boolean;
extensionSupportsOverlay: boolean;
insideApp?: boolean;
settings: AsbplayerSettings;
scrollToId?: string;
Expand All @@ -545,6 +546,7 @@ export default function SettingsForm({
settings,
extensionInstalled,
extensionSupportsAppIntegration,
extensionSupportsOverlay,
insideApp,
scrollToId,
chromeKeyBinds,
Expand Down Expand Up @@ -696,6 +698,7 @@ export default function SettingsForm({
streamingCondensedPlaybackMinimumSkipIntervalMs,
streamingScreenshotDelay,
streamingSubtitleListPreference,
streamingEnableOverlay,
webSocketClientEnabled,
webSocketServerUrl,
} = settings;
Expand Down Expand Up @@ -1556,6 +1559,21 @@ export default function SettingsForm({
label={t('extension.settings.openSubtitleList')}
labelPlacement="start"
/>
{extensionSupportsOverlay && (
<LabelWithHoverEffect
className={classes.switchLabel}
control={
<Switch
checked={streamingEnableOverlay}
onChange={(e) =>
handleSettingChanged('streamingEnableOverlay', e.target.checked)
}
/>
}
label={t('extension.settings.enableOverlay')}
labelPlacement="start"
/>
)}
<LabelWithHoverEffect
className={classes.switchLabel}
control={
Expand Down
3 changes: 2 additions & 1 deletion common/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,8 @@
"subtitles": "Untertitel",
"syncing": "Syncronisation",
"takeScreenshot": "Screenshot erstellen",
"updateAvailable": "Aktualisierung verfügbar"
"updateAvailable": "Aktualisierung verfügbar",
"enableOverlay": "Enable controls overlay"
},
"videoDataSync": {
"emptySubtitleTrack": "Empty",
Expand Down
3 changes: 2 additions & 1 deletion common/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,8 @@
"subtitles": "Subtitles",
"syncing": "Syncing",
"takeScreenshot": "Take screenshot",
"updateAvailable": "Update Available"
"updateAvailable": "Update Available",
"enableOverlay": "Enable controls overlay"
},
"videoDataSync": {
"emptySubtitleTrack": "Empty",
Expand Down
3 changes: 2 additions & 1 deletion common/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,8 @@
"subtitles": "Subtítulos",
"syncing": "Sincronización",
"takeScreenshot": "Tomar captura de pantalla",
"updateAvailable": "Actualización Disponible"
"updateAvailable": "Actualización Disponible",
"enableOverlay": "Enable controls overlay"
},
"videoDataSync": {
"emptySubtitleTrack": "En Blanco",
Expand Down
3 changes: 2 additions & 1 deletion common/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,8 @@
"subtitles": "字幕",
"syncing": "同期",
"takeScreenshot": "スクリーンショットを撮る",
"updateAvailable": "アップデートがあります"
"updateAvailable": "アップデートがあります",
"enableOverlay": "コントロールオーバーレイを表示"
},
"videoDataSync": {
"emptySubtitleTrack": "",
Expand Down
Loading

0 comments on commit 02a2668

Please sign in to comment.