Add option to retry video stream on failure

## Issue
Closes 4475 Option to retry video stream on failure
This commit is contained in:
infiinte-persistence 2020-07-15 19:20:27 +08:00 committed by Sean Yesmunt
parent 115c456318
commit 7bef092013
3 changed files with 59 additions and 19 deletions
CHANGELOG.md
static
ui/component/viewers/videoViewer/internal

View file

@ -9,7 +9,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Added ### Added
- Allow zooming on Desktop _community pr!_ ([#4513](https://github.com/lbryio/lbry-desktop/pull/4513)) - Allow zooming on Desktop _community pr!_ ([#4513](https://github.com/lbryio/lbry-desktop/pull/4513))
- Show "YT Creator" label in File Page as well _community pr!_ ([#4523](https://github.com/lbryio/lbry-desktop/pull/4523)) - Show "YT Creator" label in File Page as well _community pr!_ ([#4523](https://github.com/lbryio/lbry-desktop/pull/4523))
- Add option to retry video stream on failure _community pr!_ ([#4541](https://github.com/lbryio/lbry-desktop/pull/4541))
### Changed ### Changed

View file

@ -1162,6 +1162,7 @@
"No information will be sent directly to LBRY, Inc. or third-parties about your usage. Note that as\n peer-to-peer software, your IP address and potentially other system information can be sent to other\n users, though this information is not stored permanently.": "No information will be sent directly to LBRY, Inc. or third-parties about your usage. Note that as\n peer-to-peer software, your IP address and potentially other system information can be sent to other\n users, though this information is not stored permanently.", "No information will be sent directly to LBRY, Inc. or third-parties about your usage. Note that as\n peer-to-peer software, your IP address and potentially other system information can be sent to other\n users, though this information is not stored permanently.": "No information will be sent directly to LBRY, Inc. or third-parties about your usage. Note that as\n peer-to-peer software, your IP address and potentially other system information can be sent to other\n users, though this information is not stored permanently.",
"%view_count% Views": "%view_count% Views", "%view_count% Views": "%view_count% Views",
"Tap to unmute": "Tap to unmute", "Tap to unmute": "Tap to unmute",
"Retry": "Retry",
"Playing in %seconds_left% seconds...": "Playing in %seconds_left% seconds...", "Playing in %seconds_left% seconds...": "Playing in %seconds_left% seconds...",
"Autoplay timer paused.": "Autoplay timer paused.", "Autoplay timer paused.": "Autoplay timer paused.",
"0 Bytes": "0 Bytes", "0 Bytes": "0 Bytes",

View file

@ -1,5 +1,5 @@
// @flow // @flow
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import Button from 'component/button'; import Button from 'component/button';
import * as ICONS from 'constants/icons'; import * as ICONS from 'constants/icons';
import classnames from 'classnames'; import classnames from 'classnames';
@ -7,6 +7,7 @@ import videojs from 'video.js/dist/alt/video.core.novtt.min.js';
import 'video.js/dist/alt/video-js-cdn.min.css'; import 'video.js/dist/alt/video-js-cdn.min.css';
import eventTracking from 'videojs-event-tracking'; import eventTracking from 'videojs-event-tracking';
import isUserTyping from 'util/detect-typing'; import isUserTyping from 'util/detect-typing';
const isDev = process.env.NODE_ENV !== 'production';
export type Player = { export type Player = {
on: (string, (any) => void) => void, on: (string, (any) => void) => void,
@ -79,6 +80,7 @@ properties for this component should be kept to ONLY those that if changed shoul
*/ */
export default React.memo<Props>(function VideoJs(props: Props) { export default React.memo<Props>(function VideoJs(props: Props) {
const { startMuted, source, sourceType, poster, isAudio, onPlayerReady } = props; const { startMuted, source, sourceType, poster, isAudio, onPlayerReady } = props;
const [reload, setReload] = useState('initial');
let player: ?Player; let player: ?Player;
const containerRef = useRef(); const containerRef = useRef();
const videoJsOptions = { const videoJsOptions = {
@ -97,17 +99,33 @@ export default React.memo<Props>(function VideoJs(props: Props) {
videoJsOptions.muted = startMuted; videoJsOptions.muted = startMuted;
const tapToUnmuteRef = useRef(); const tapToUnmuteRef = useRef();
const tapToRetryRef = useRef();
const TAP = {
UNMUTE: 'UNMUTE',
RETRY: 'RETRY',
};
function showTapButton(tapButton, newState) {
let theRef;
switch (tapButton) {
case TAP.UNMUTE:
theRef = tapToUnmuteRef;
break;
case TAP.RETRY:
theRef = tapToRetryRef;
break;
default:
if (isDev) throw new Error('showTapButton: unexpected ref');
return;
}
function showTapToUnmute(newState: boolean) {
// Use the DOM to control the state of the button to prevent re-renders. // Use the DOM to control the state of the button to prevent re-renders.
// The button only needs to appear once per session. if (theRef.current) {
if (tapToUnmuteRef.current) { const curState = theRef.current.style.visibility === 'visible';
const curState = tapToUnmuteRef.current.style.visibility === 'visible';
if (newState !== curState) { if (newState !== curState) {
tapToUnmuteRef.current.style.visibility = newState ? 'visible' : 'hidden'; theRef.current.style.visibility = newState ? 'visible' : 'hidden';
} }
} else if (process.env.NODE_ENV === 'development') {
throw new Error('[videojs.jsx] Empty video ref should not happen');
} }
} }
@ -118,32 +136,44 @@ export default React.memo<Props>(function VideoJs(props: Props) {
player.volume(1.0); player.volume(1.0);
} }
} }
showTapToUnmute(false); showTapButton(TAP.UNMUTE, false);
}
function retryVideoAfterFailure() {
if (player) {
setReload(Date.now());
showTapButton(TAP.RETRY, false);
}
} }
function onInitialPlay() { function onInitialPlay() {
if (player && (player.muted() || player.volume() === 0)) { if (player && (player.muted() || player.volume() === 0)) {
// The css starts as "hidden". We make it visible here without // The css starts as "hidden". We make it visible here without
// re-rendering the whole thing. // re-rendering the whole thing.
showTapToUnmute(true); showTapButton(TAP.UNMUTE, true);
} }
showTapButton(TAP.RETRY, false);
} }
function onVolumeChange() { function onVolumeChange() {
if (player && !player.muted()) { if (player && !player.muted()) {
showTapToUnmute(false); showTapButton(TAP.UNMUTE, false);
} }
} }
function onError() { function onError() {
showTapToUnmute(false); showTapButton(TAP.UNMUTE, false);
showTapButton(TAP.RETRY, true);
if (player && player.loadingSpinner) { if (player && player.loadingSpinner) {
player.loadingSpinner.hide(); player.loadingSpinner.hide();
} }
} }
function onEnded() { function onEnded() {
showTapToUnmute(false); showTapButton(TAP.UNMUTE, false);
showTapButton(TAP.RETRY, false);
} }
function handleKeyDown(e: KeyboardEvent) { function handleKeyDown(e: KeyboardEvent) {
@ -225,9 +255,9 @@ export default React.memo<Props>(function VideoJs(props: Props) {
}); });
return ( return (
// $FlowFixMe reload && (
<div className={classnames('video-js-parent', { 'video-js-parent--ios': IS_IOS })} ref={containerRef}> // $FlowFixMe
{ <div className={classnames('video-js-parent', { 'video-js-parent--ios': IS_IOS })} ref={containerRef}>
<Button <Button
label={__('Tap to unmute')} label={__('Tap to unmute')}
button="link" button="link"
@ -236,7 +266,15 @@ export default React.memo<Props>(function VideoJs(props: Props) {
onClick={unmuteAndHideHint} onClick={unmuteAndHideHint}
ref={tapToUnmuteRef} ref={tapToUnmuteRef}
/> />
} <Button
</div> label={__('Retry')}
button="link"
icon={ICONS.REFRESH}
className="video-js--tap-to-unmute"
onClick={retryVideoAfterFailure}
ref={tapToRetryRef}
/>
</div>
)
); );
}); });