Show "unmute" option on videos automatically muted by browser.
Implementation: - The code is placed in <VideoJs> instead of <VideoViewer> as we need to control the video itself. It's more self-contained here, rather than trying to pass refs around between parent and child. - useState cannot be used as it will cause a re-render when the hint it clicked and dismissed. The DOM is used to hide the button.
This commit is contained in:
parent
bbda69dc5f
commit
a20ea08ac7
5 changed files with 90 additions and 2 deletions
|
@ -1228,6 +1228,7 @@
|
||||||
"Sharing information with LBRY, Inc. allows us to report to publishers how their content is doing, as\n well as track basic usage and performance. This is the minimum required to earn rewards from LBRY, Inc.": "Sharing information with LBRY, Inc. allows us to report to publishers how their content is doing, as\n well as track basic usage and performance. This is the minimum required to earn rewards from LBRY, Inc.",
|
"Sharing information with LBRY, Inc. allows us to report to publishers how their content is doing, as\n well as track basic usage and performance. This is the minimum required to earn rewards from LBRY, Inc.": "Sharing information with LBRY, Inc. allows us to report to publishers how their content is doing, as\n well as track basic usage and performance. This is the minimum required to earn rewards from LBRY, Inc.",
|
||||||
"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",
|
||||||
"0 Bytes": "0 Bytes",
|
"0 Bytes": "0 Bytes",
|
||||||
"Bytes": "Bytes",
|
"Bytes": "Bytes",
|
||||||
"KB": "KB",
|
"KB": "KB",
|
||||||
|
|
|
@ -370,6 +370,13 @@ export const icons = {
|
||||||
<path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z" />
|
<path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z" />
|
||||||
</g>
|
</g>
|
||||||
),
|
),
|
||||||
|
[ICONS.VOLUME_MUTED]: buildIcon(
|
||||||
|
<g>
|
||||||
|
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
|
||||||
|
<line x1="23" y1="9" x2="17" y2="15" />
|
||||||
|
<line x1="17" y1="9" x2="23" y2="15" />
|
||||||
|
</g>
|
||||||
|
),
|
||||||
[ICONS.IMAGE]: buildIcon(
|
[ICONS.IMAGE]: buildIcon(
|
||||||
<g>
|
<g>
|
||||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import * as ICONS from 'constants/icons';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import videojs from 'video.js/dist/alt/video.core.novtt.min.js';
|
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';
|
||||||
|
import isDev from 'electron-is-dev';
|
||||||
|
|
||||||
export type Player = {
|
export type Player = {
|
||||||
on: (string, (any) => void) => void,
|
on: (string, (any) => void) => void,
|
||||||
|
one: (string, (any) => void) => void,
|
||||||
isFullscreen: () => boolean,
|
isFullscreen: () => boolean,
|
||||||
exitFullscreen: () => boolean,
|
exitFullscreen: () => boolean,
|
||||||
requestFullscreen: () => boolean,
|
requestFullscreen: () => boolean,
|
||||||
|
@ -93,6 +97,50 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
||||||
|
|
||||||
videoJsOptions.muted = startMuted;
|
videoJsOptions.muted = startMuted;
|
||||||
|
|
||||||
|
const tapToUnmuteRef = useRef();
|
||||||
|
|
||||||
|
function showTapToUnmute(newState: boolean) {
|
||||||
|
// Use the DOM to control the state of the button to prevent re-renders.
|
||||||
|
// The button only needs to appear once per session.
|
||||||
|
if (tapToUnmuteRef.current) {
|
||||||
|
const curState = tapToUnmuteRef.current.style.visibility === 'visible';
|
||||||
|
if (newState !== curState) {
|
||||||
|
tapToUnmuteRef.current.style.visibility = newState ? 'visible' : 'hidden';
|
||||||
|
}
|
||||||
|
} else if (isDev) {
|
||||||
|
throw new Error('[videojs.jsx] Empty video ref should not happen');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function unmuteAndHideHint() {
|
||||||
|
if (player) {
|
||||||
|
player.muted(false);
|
||||||
|
}
|
||||||
|
showTapToUnmute(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onInitialPlay() {
|
||||||
|
if (player && player.muted()) {
|
||||||
|
// The css starts as "hidden". We make it visible here without
|
||||||
|
// re-rendering the whole thing.
|
||||||
|
showTapToUnmute(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onVolumeChange() {
|
||||||
|
if (player && !player.muted()) {
|
||||||
|
showTapToUnmute(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onError() {
|
||||||
|
showTapToUnmute(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onEnded() {
|
||||||
|
showTapToUnmute(false);
|
||||||
|
}
|
||||||
|
|
||||||
function handleKeyDown(e: KeyboardEvent) {
|
function handleKeyDown(e: KeyboardEvent) {
|
||||||
const videoNode: ?HTMLVideoElement = containerRef.current && containerRef.current.querySelector('video, audio');
|
const videoNode: ?HTMLVideoElement = containerRef.current && containerRef.current.querySelector('video, audio');
|
||||||
|
|
||||||
|
@ -145,6 +193,11 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
||||||
|
|
||||||
player = videojs(el, videoJsOptions, () => {
|
player = videojs(el, videoJsOptions, () => {
|
||||||
if (player) {
|
if (player) {
|
||||||
|
player.one('play', onInitialPlay);
|
||||||
|
player.on('volumechange', onVolumeChange);
|
||||||
|
player.on('error', onError);
|
||||||
|
player.on('ended', onEnded);
|
||||||
|
|
||||||
onPlayerReady(player);
|
onPlayerReady(player);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -166,6 +219,19 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
return <div className={classnames('video-js-parent', { 'video-js-parent--ios': IS_IOS })} ref={containerRef} />;
|
<div className={classnames('video-js-parent', { 'video-js-parent--ios': IS_IOS })} ref={containerRef}>
|
||||||
|
{
|
||||||
|
<Button
|
||||||
|
label={__('Tap to unmute')}
|
||||||
|
button="link"
|
||||||
|
icon={ICONS.VOLUME_MUTED}
|
||||||
|
className="video-js--tap-to-unmute"
|
||||||
|
onClick={unmuteAndHideHint}
|
||||||
|
ref={tapToUnmuteRef}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -90,6 +90,7 @@ export const MORE_VERTICAL = 'MoreVertical';
|
||||||
export const IMAGE = 'Image';
|
export const IMAGE = 'Image';
|
||||||
export const AUDIO = 'HeadPhones';
|
export const AUDIO = 'HeadPhones';
|
||||||
export const VIDEO = 'Video';
|
export const VIDEO = 'Video';
|
||||||
|
export const VOLUME_MUTED = 'VolumeX';
|
||||||
export const TEXT = 'FileText';
|
export const TEXT = 'FileText';
|
||||||
export const DOWNLOADABLE = 'Downloadable';
|
export const DOWNLOADABLE = 'Downloadable';
|
||||||
export const REPOST = 'Repeat';
|
export const REPOST = 'Repeat';
|
||||||
|
|
|
@ -345,6 +345,19 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.video-js--tap-to-unmute {
|
||||||
|
visibility: hidden; // Start off as hidden.
|
||||||
|
z-index: 2;
|
||||||
|
position: absolute;
|
||||||
|
top: 5%;
|
||||||
|
left: 5%;
|
||||||
|
padding: var(--spacing-xs) var(--spacing-m); // Make it comfy for touch.
|
||||||
|
color: var(--color-gray-1);
|
||||||
|
background: black;
|
||||||
|
border: 1px solid var(--color-gray-4);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
.file-render {
|
.file-render {
|
||||||
.video-js {
|
.video-js {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
Loading…
Reference in a new issue