[Perf] Introduce Optimized Video component

This commit is contained in:
Alexander Zinchuk 2022-10-17 17:35:06 +02:00
parent 7d714a9b26
commit 04dee2ce7b
13 changed files with 119 additions and 176 deletions

View File

@ -32,8 +32,8 @@ import useMedia from '../../hooks/useMedia';
import useShowTransition from '../../hooks/useShowTransition'; import useShowTransition from '../../hooks/useShowTransition';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
import { useIsIntersecting } from '../../hooks/useIntersectionObserver'; import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
import useVideoAutoPause from '../middle/message/hooks/useVideoAutoPause';
import useVideoCleanup from '../../hooks/useVideoCleanup'; import OptimizedVideo from '../ui/OptimizedVideo';
import './Avatar.scss'; import './Avatar.scss';
@ -79,8 +79,6 @@ const Avatar: FC<OwnProps> = ({
const { loadFullUser } = getActions(); const { loadFullUser } = getActions();
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
// eslint-disable-next-line no-null/no-null
const videoRef = useRef<HTMLVideoElement>(null);
const videoLoopCountRef = useRef(0); const videoLoopCountRef = useRef(0);
const isIntersecting = useIsIntersecting(ref, observeIntersection); const isIntersecting = useIsIntersecting(ref, observeIntersection);
const isDeleted = user && isDeletedUser(user); const isDeleted = user && isDeletedUser(user);
@ -117,24 +115,14 @@ const Avatar: FC<OwnProps> = ({
const { transitionClassNames } = useShowTransition(hasBlobUrl, undefined, hasBlobUrl, 'slow'); const { transitionClassNames } = useShowTransition(hasBlobUrl, undefined, hasBlobUrl, 'slow');
const { handlePlaying } = useVideoAutoPause(videoRef, shouldPlayVideo); const handleVideoEnded = useCallback((e) => {
useVideoCleanup(videoRef, [shouldPlayVideo]); const video = e.currentTarget;
if (!videoBlobUrl) return;
useEffect(() => { videoLoopCountRef.current += 1;
const video = videoRef.current; if (videoLoopCountRef.current >= LOOP_COUNT || noLoop) {
if (!video || !videoBlobUrl) return undefined; video.style.display = 'none';
}
const returnToStart = () => {
videoLoopCountRef.current += 1;
if (videoLoopCountRef.current >= LOOP_COUNT || noLoop) {
video.style.display = 'none';
} else {
video.play();
}
};
video.addEventListener('ended', returnToStart);
return () => video.removeEventListener('ended', returnToStart);
}, [noLoop, videoBlobUrl]); }, [noLoop, videoBlobUrl]);
const userId = user?.id; const userId = user?.id;
@ -165,15 +153,15 @@ const Avatar: FC<OwnProps> = ({
decoding="async" decoding="async"
/> />
{shouldPlayVideo && ( {shouldPlayVideo && (
<video <OptimizedVideo
ref={videoRef} canPlay
src={videoBlobUrl} src={videoBlobUrl}
className={buildClassName(cn.media, 'avatar-media')} className={buildClassName(cn.media, 'avatar-media')}
muted muted
autoPlay autoPlay
disablePictureInPicture disablePictureInPicture
playsInline playsInline
onPlaying={handlePlaying} onEnded={handleVideoEnded}
/> />
)} )}
</> </>

View File

@ -1,5 +1,5 @@
import React, { import React, {
memo, useCallback, useEffect, useMemo, useRef, useState, memo, useCallback, useEffect, useRef, useState,
} from '../../lib/teact/teact'; } from '../../lib/teact/teact';
import { getGlobal } from '../../global'; import { getGlobal } from '../../global';
@ -8,7 +8,6 @@ import type { ObserveFn } from '../../hooks/useIntersectionObserver';
import { IS_WEBM_SUPPORTED } from '../../util/environment'; import { IS_WEBM_SUPPORTED } from '../../util/environment';
import renderText from './helpers/renderText'; import renderText from './helpers/renderText';
import safePlay from '../../util/safePlay';
import { getPropertyHexColor } from '../../util/themeStyle'; import { getPropertyHexColor } from '../../util/themeStyle';
import { hexToRgb } from '../../util/switchTheme'; import { hexToRgb } from '../../util/switchTheme';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
@ -21,6 +20,7 @@ import useThumbnail from '../../hooks/useThumbnail';
import useCustomEmoji from './hooks/useCustomEmoji'; import useCustomEmoji from './hooks/useCustomEmoji';
import AnimatedSticker from './AnimatedSticker'; import AnimatedSticker from './AnimatedSticker';
import OptimizedVideo from '../ui/OptimizedVideo';
import styles from './CustomEmoji.module.scss'; import styles from './CustomEmoji.module.scss';
@ -77,36 +77,16 @@ const CustomEmoji: FC<OwnProps> = ({
useEnsureCustomEmoji(documentId); useEnsureCustomEmoji(documentId);
useEffect(() => { const handleVideoEnded = useCallback((e) => {
if (!customEmoji?.isVideo) return; if (!loopLimit) return;
const video = ref.current?.querySelector('video');
if (!video || isIntersecting === !video.paused) return;
if (isIntersecting) { loopCountRef.current += 1;
safePlay(video);
} else { if (loopCountRef.current >= loopLimit) {
video.pause(); setShouldLoop(false);
e.currentTarget.currentTime = 0;
} }
}, [customEmoji, isIntersecting]); }, [loopLimit]);
useEffect(() => {
if (!loopLimit) return undefined;
const video = ref.current?.querySelector('video');
if (!mediaData || !video) return undefined;
const returnToStart = () => {
loopCountRef.current += 1;
if (loopCountRef.current >= loopLimit) {
setShouldLoop(false);
video.currentTime = 0;
} else {
video.play();
}
};
video.addEventListener('ended', returnToStart);
return () => video.removeEventListener('ended', returnToStart);
}, [loopLimit, mediaData]);
const handleStickerLoop = useCallback(() => { const handleStickerLoop = useCallback(() => {
if (!loopLimit) return; if (!loopLimit) return;
@ -117,7 +97,7 @@ const CustomEmoji: FC<OwnProps> = ({
} }
}, [loopLimit]); }, [loopLimit]);
const content = useMemo(() => { function renderContent() {
if (!customEmoji || (!thumbDataUri && !mediaData)) { if (!customEmoji || (!thumbDataUri && !mediaData)) {
return (children && renderText(children, ['emoji'])); return (children && renderText(children, ['emoji']));
} }
@ -136,14 +116,15 @@ const CustomEmoji: FC<OwnProps> = ({
if (customEmoji.isVideo) { if (customEmoji.isVideo) {
return ( return (
<video <OptimizedVideo
canPlay={isIntersecting}
className={styles.media} className={styles.media}
src={mediaData}
playsInline playsInline
muted muted
loop={!loopLimit} loop={!loopLimit}
autoPlay={isIntersecting}
src={mediaData}
disablePictureInPicture disablePictureInPicture
onEnded={handleVideoEnded}
/> />
); );
} }
@ -161,10 +142,7 @@ const CustomEmoji: FC<OwnProps> = ({
onLoop={handleStickerLoop} onLoop={handleStickerLoop}
/> />
); );
}, [ }
children, customColor, customEmoji, handleStickerLoop, isIntersecting, loopLimit, mediaData, shouldLoop,
thumbDataUri,
]);
return ( return (
<div <div
@ -179,7 +157,7 @@ const CustomEmoji: FC<OwnProps> = ({
)} )}
onClick={onClick} onClick={onClick}
> >
{content} {renderContent()}
</div> </div>
); );
}; };

View File

@ -13,7 +13,6 @@ import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
import { preventMessageInputBlurWithBubbling } from '../middle/helpers/preventMessageInputBlur'; import { preventMessageInputBlurWithBubbling } from '../middle/helpers/preventMessageInputBlur';
import useMedia from '../../hooks/useMedia'; import useMedia from '../../hooks/useMedia';
import useVideoCleanup from '../../hooks/useVideoCleanup';
import useBuffering from '../../hooks/useBuffering'; import useBuffering from '../../hooks/useBuffering';
import useCanvasBlur from '../../hooks/useCanvasBlur'; import useCanvasBlur from '../../hooks/useCanvasBlur';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
@ -24,6 +23,7 @@ import Spinner from '../ui/Spinner';
import Button from '../ui/Button'; import Button from '../ui/Button';
import Menu from '../ui/Menu'; import Menu from '../ui/Menu';
import MenuItem from '../ui/MenuItem'; import MenuItem from '../ui/MenuItem';
import OptimizedVideo from '../ui/OptimizedVideo';
import './GifButton.scss'; import './GifButton.scss';
@ -48,8 +48,6 @@ const GifButton: FC<OwnProps> = ({
}) => { }) => {
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
// eslint-disable-next-line no-null/no-null
const videoRef = useRef<HTMLVideoElement>(null);
const lang = useLang(); const lang = useLang();
@ -65,8 +63,6 @@ const GifButton: FC<OwnProps> = ({
const shouldRenderSpinner = loadAndPlay && !isBuffered; const shouldRenderSpinner = loadAndPlay && !isBuffered;
const isVideoReady = loadAndPlay && isBuffered; const isVideoReady = loadAndPlay && isBuffered;
useVideoCleanup(videoRef, [shouldRenderVideo]);
const { const {
isContextMenuOpen, contextMenuPosition, isContextMenuOpen, contextMenuPosition,
handleBeforeContextMenu, handleContextMenu, handleBeforeContextMenu, handleContextMenu,
@ -177,8 +173,9 @@ const GifButton: FC<OwnProps> = ({
/> />
)} )}
{shouldRenderVideo && ( {shouldRenderVideo && (
<video <OptimizedVideo
ref={videoRef} canPlay
src={videoData}
autoPlay autoPlay
loop loop
muted muted
@ -187,9 +184,7 @@ const GifButton: FC<OwnProps> = ({
preload="none" preload="none"
// eslint-disable-next-line react/jsx-props-no-spreading // eslint-disable-next-line react/jsx-props-no-spreading
{...bufferingHandlers} {...bufferingHandlers}
> />
<source src={videoData} />
</video>
)} )}
{shouldRenderSpinner && ( {shouldRenderSpinner && (
<Spinner color={previewBlobUrl || hasThumbnail ? 'white' : 'black'} /> <Spinner color={previewBlobUrl || hasThumbnail ? 'white' : 'black'} />

View File

@ -18,11 +18,9 @@ import buildClassName from '../../util/buildClassName';
import { getFirstLetters } from '../../util/textFormat'; import { getFirstLetters } from '../../util/textFormat';
import useMedia from '../../hooks/useMedia'; import useMedia from '../../hooks/useMedia';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
import useVideoAutoPause from '../middle/message/hooks/useVideoAutoPause';
import useVideoCleanup from '../../hooks/useVideoCleanup';
import safePlay from '../../util/safePlay';
import Spinner from '../ui/Spinner'; import Spinner from '../ui/Spinner';
import OptimizedVideo from '../ui/OptimizedVideo';
import './ProfilePhoto.scss'; import './ProfilePhoto.scss';
@ -79,18 +77,11 @@ const ProfilePhoto: FC<OwnProps> = ({
} }
useEffect(() => { useEffect(() => {
if (!videoRef.current) return; if (videoRef.current && !canPlayVideo) {
if (!canPlayVideo) {
videoRef.current.pause();
videoRef.current.currentTime = 0; videoRef.current.currentTime = 0;
} else {
safePlay(videoRef.current);
} }
}, [canPlayVideo]); }, [canPlayVideo]);
const { handlePlaying } = useVideoAutoPause(videoRef, canPlayVideo);
useVideoCleanup(videoRef, []);
const photoHash = getMediaHash('big', 'photo'); const photoHash = getMediaHash('big', 'photo');
const photoBlobUrl = useMedia(photoHash, false, ApiMediaFormat.BlobUrl, lastSyncTime); const photoBlobUrl = useMedia(photoHash, false, ApiMediaFormat.BlobUrl, lastSyncTime);
const videoHash = getMediaHash('normal', 'video'); const videoHash = getMediaHash('normal', 'video');
@ -108,16 +99,15 @@ const ProfilePhoto: FC<OwnProps> = ({
} else if (imageSrc) { } else if (imageSrc) {
if (videoBlobUrl) { if (videoBlobUrl) {
content = ( content = (
<video <OptimizedVideo
canPlay={canPlayVideo}
ref={videoRef} ref={videoRef}
src={imageSrc} src={imageSrc}
className="avatar-media" className="avatar-media"
muted muted
disablePictureInPicture disablePictureInPicture
autoPlay={canPlayVideo}
loop loop
playsInline playsInline
onPlaying={handlePlaying}
/> />
); );
} else { } else {

View File

@ -26,6 +26,7 @@ import AnimatedSticker from './AnimatedSticker';
import Button from '../ui/Button'; import Button from '../ui/Button';
import Menu from '../ui/Menu'; import Menu from '../ui/Menu';
import MenuItem from '../ui/MenuItem'; import MenuItem from '../ui/MenuItem';
import OptimizedVideo from '../ui/OptimizedVideo';
import './StickerButton.scss'; import './StickerButton.scss';
@ -269,10 +270,10 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
<img src={previewBlobUrl} className={previewTransitionClassNames} /> <img src={previewBlobUrl} className={previewTransitionClassNames} />
)} )}
{isVideo && ( {isVideo && (
<video <OptimizedVideo
canPlay={canVideoPlay}
className={previewTransitionClassNames} className={previewTransitionClassNames}
src={videoBlobUrl} src={videoBlobUrl}
autoPlay={canVideoPlay}
loop loop
playsInline playsInline
disablePictureInPicture disablePictureInPicture

View File

@ -1,5 +1,5 @@
import type { FC } from '../../../../lib/teact/teact'; import type { FC } from '../../../../lib/teact/teact';
import React, { memo, useEffect, useRef } from '../../../../lib/teact/teact'; import React, { memo } from '../../../../lib/teact/teact';
import type { ApiThumbnail } from '../../../../api/types'; import type { ApiThumbnail } from '../../../../api/types';
@ -7,9 +7,9 @@ import useMedia from '../../../../hooks/useMedia';
import buildClassName from '../../../../util/buildClassName'; import buildClassName from '../../../../util/buildClassName';
import useCanvasBlur from '../../../../hooks/useCanvasBlur'; import useCanvasBlur from '../../../../hooks/useCanvasBlur';
import useMediaTransition from '../../../../hooks/useMediaTransition'; import useMediaTransition from '../../../../hooks/useMediaTransition';
import safePlay from '../../../../util/safePlay';
import DeviceFrame from '../../../../assets/premium/DeviceFrame.svg'; import DeviceFrame from '../../../../assets/premium/DeviceFrame.svg';
import OptimizedVideo from '../../../ui/OptimizedVideo';
import styles from './PremiumFeaturePreviewVideo.module.scss'; import styles from './PremiumFeaturePreviewVideo.module.scss';
@ -33,19 +33,6 @@ const PremiumFeaturePreviewVideo: FC<OwnProps> = ({
const mediaData = useMedia(`document${videoId}`); const mediaData = useMedia(`document${videoId}`);
const thumbnailRef = useCanvasBlur(videoThumbnail.dataUri); const thumbnailRef = useCanvasBlur(videoThumbnail.dataUri);
const transitionClassNames = useMediaTransition(mediaData); const transitionClassNames = useMediaTransition(mediaData);
// eslint-disable-next-line no-null/no-null
const videoRef = useRef<HTMLVideoElement>(null);
useEffect(() => {
const video = videoRef.current;
if (!video) return;
if (isActive) {
safePlay(video);
} else {
video.pause();
}
}, [isActive]);
return ( return (
<div className={styles.root}> <div className={styles.root}>
@ -59,14 +46,10 @@ const PremiumFeaturePreviewVideo: FC<OwnProps> = ({
> >
<img src={DeviceFrame} alt="" className={styles.frame} /> <img src={DeviceFrame} alt="" className={styles.frame} />
<canvas ref={thumbnailRef} className={styles.video} /> <canvas ref={thumbnailRef} className={styles.video} />
<video <OptimizedVideo
ref={videoRef} canPlay={isActive}
className={buildClassName( className={buildClassName(styles.video, transitionClassNames)}
styles.video,
transitionClassNames,
)}
src={mediaData} src={mediaData}
autoPlay={isActive}
disablePictureInPicture disablePictureInPicture
playsInline playsInline
muted muted

View File

@ -64,6 +64,7 @@ import MessageListContent from './MessageListContent';
import ContactGreeting from './ContactGreeting'; import ContactGreeting from './ContactGreeting';
import NoMessages from './NoMessages'; import NoMessages from './NoMessages';
import Skeleton from '../ui/Skeleton'; import Skeleton from '../ui/Skeleton';
import OptimizedVideo from '../ui/OptimizedVideo';
import './MessageList.scss'; import './MessageList.scss';
@ -563,10 +564,10 @@ const MessageList: FC<OwnProps & StateProps> = ({
/> />
)} )}
{botInfoGifUrl && ( {botInfoGifUrl && (
<video <OptimizedVideo
canPlay
src={botInfoGifUrl} src={botInfoGifUrl}
loop loop
autoPlay
disablePictureInPicture disablePictureInPicture
muted muted
playsInline playsInline

View File

@ -10,6 +10,8 @@ import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
import useMedia from '../../../hooks/useMedia'; import useMedia from '../../../hooks/useMedia';
import useMediaTransition from '../../../hooks/useMediaTransition'; import useMediaTransition from '../../../hooks/useMediaTransition';
import OptimizedVideo from '../../ui/OptimizedVideo';
type OwnProps = { type OwnProps = {
stickerSet: ApiStickerSet; stickerSet: ApiStickerSet;
observeIntersection: ObserveFn; observeIntersection: ObserveFn;
@ -34,7 +36,7 @@ const StickerSetCover: FC<OwnProps> = ({ stickerSet, observeIntersection }) => {
<div ref={ref} className="sticker-set-cover"> <div ref={ref} className="sticker-set-cover">
{firstLetters} {firstLetters}
{isVideo ? ( {isVideo ? (
<video src={mediaData} className={transitionClassNames} loop autoPlay disablePictureInPicture /> <OptimizedVideo canPlay src={mediaData} className={transitionClassNames} loop disablePictureInPicture />
) : ( ) : (
<img src={mediaData} className={transitionClassNames} alt="" /> <img src={mediaData} className={transitionClassNames} alt="" />
)} )}

View File

@ -24,11 +24,10 @@ import useShowTransition from '../../../hooks/useShowTransition';
import useMediaTransition from '../../../hooks/useMediaTransition'; import useMediaTransition from '../../../hooks/useMediaTransition';
import usePrevious from '../../../hooks/usePrevious'; import usePrevious from '../../../hooks/usePrevious';
import useBuffering from '../../../hooks/useBuffering'; import useBuffering from '../../../hooks/useBuffering';
import useVideoCleanup from '../../../hooks/useVideoCleanup';
import useVideoAutoPause from './hooks/useVideoAutoPause';
import useBlurredMediaThumbRef from './hooks/useBlurredMediaThumbRef'; import useBlurredMediaThumbRef from './hooks/useBlurredMediaThumbRef';
import ProgressSpinner from '../../ui/ProgressSpinner'; import ProgressSpinner from '../../ui/ProgressSpinner';
import OptimizedVideo from '../../ui/OptimizedVideo';
import './RoundVideo.scss'; import './RoundVideo.scss';
@ -141,21 +140,6 @@ const RoundVideo: FC<OwnProps> = ({
stopPrevious = stopPlaying; stopPrevious = stopPlaying;
}, [stopPlaying]); }, [stopPlaying]);
useEffect(() => {
if (!playerRef.current) {
return;
}
if (shouldPlay) {
safePlay(playerRef.current);
} else {
playerRef.current.pause();
}
}, [shouldPlay]);
useVideoAutoPause(playerRef, shouldPlay);
useVideoCleanup(playerRef, [mediaData]);
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
if (!mediaData) { if (!mediaData) {
setIsLoadAllowed((isAllowed) => !isAllowed); setIsLoadAllowed((isAllowed) => !isAllowed);
@ -212,8 +196,10 @@ const RoundVideo: FC<OwnProps> = ({
{mediaData && ( {mediaData && (
<div className="video-wrapper"> <div className="video-wrapper">
{/* eslint-disable-next-line jsx-a11y/media-has-caption */} {/* eslint-disable-next-line jsx-a11y/media-has-caption */}
<video <OptimizedVideo
canPlay={shouldPlay}
ref={playerRef} ref={playerRef}
src={mediaData}
className={videoClassName} className={videoClassName}
width={ROUND_VIDEO_DIMENSIONS_PX} width={ROUND_VIDEO_DIMENSIONS_PX}
height={ROUND_VIDEO_DIMENSIONS_PX} height={ROUND_VIDEO_DIMENSIONS_PX}
@ -226,9 +212,7 @@ const RoundVideo: FC<OwnProps> = ({
// eslint-disable-next-line react/jsx-props-no-spreading // eslint-disable-next-line react/jsx-props-no-spreading
{...bufferingHandlers} {...bufferingHandlers}
onTimeUpdate={isActivated ? handleTimeUpdate : undefined} onTimeUpdate={isActivated ? handleTimeUpdate : undefined}
> />
<source src={mediaData} />
</video>
</div> </div>
)} )}
<div className="progress" ref={playingProgressRef} /> <div className="progress" ref={playingProgressRef} />

View File

@ -7,7 +7,6 @@ import { ApiMediaFormat } from '../../../api/types';
import { getStickerDimensions } from '../../common/helpers/mediaDimensions'; import { getStickerDimensions } from '../../common/helpers/mediaDimensions';
import { getMessageMediaFormat, getMessageMediaHash } from '../../../global/helpers'; import { getMessageMediaFormat, getMessageMediaHash } from '../../../global/helpers';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import safePlay from '../../../util/safePlay';
import { IS_WEBM_SUPPORTED } from '../../../util/environment'; import { IS_WEBM_SUPPORTED } from '../../../util/environment';
import { getActions } from '../../../global'; import { getActions } from '../../../global';
@ -20,6 +19,7 @@ import useThumbnail from '../../../hooks/useThumbnail';
import useLang from '../../../hooks/useLang'; import useLang from '../../../hooks/useLang';
import AnimatedSticker from '../../common/AnimatedSticker'; import AnimatedSticker from '../../common/AnimatedSticker';
import OptimizedVideo from '../../ui/OptimizedVideo';
import './Sticker.scss'; import './Sticker.scss';
@ -101,17 +101,6 @@ const Sticker: FC<OwnProps> = ({
onStopEffect?.(); onStopEffect?.();
}, [onStopEffect, stopPlayingEffect]); }, [onStopEffect, stopPlayingEffect]);
useEffect(() => {
if (!isVideo || !ref.current) return;
const video = ref.current.querySelector('video');
if (!video) return;
if (shouldPlay) {
safePlay(video);
} else {
video.pause();
}
}, [isVideo, shouldPlay]);
useEffect(() => { useEffect(() => {
if (hasEffect && shouldPlay && shouldPlayEffect) { if (hasEffect && shouldPlay && shouldPlayEffect) {
startPlayingEffect(); startPlayingEffect();
@ -164,11 +153,11 @@ const Sticker: FC<OwnProps> = ({
/> />
)} )}
{isVideo && canDisplayVideo && isMediaReady && ( {isVideo && canDisplayVideo && isMediaReady && (
<video <OptimizedVideo
canPlay={shouldPlay}
src={mediaData as string} src={mediaData as string}
width={width} width={width}
height={height} height={height}
autoPlay={shouldPlay}
playsInline playsInline
disablePictureInPicture disablePictureInPicture
loop={shouldLoop} loop={shouldLoop}

View File

@ -24,12 +24,11 @@ import useMedia from '../../../hooks/useMedia';
import useShowTransition from '../../../hooks/useShowTransition'; import useShowTransition from '../../../hooks/useShowTransition';
import usePrevious from '../../../hooks/usePrevious'; import usePrevious from '../../../hooks/usePrevious';
import useBuffering from '../../../hooks/useBuffering'; import useBuffering from '../../../hooks/useBuffering';
import useVideoCleanup from '../../../hooks/useVideoCleanup';
import useMediaTransition from '../../../hooks/useMediaTransition'; import useMediaTransition from '../../../hooks/useMediaTransition';
import useVideoAutoPause from './hooks/useVideoAutoPause';
import useBlurredMediaThumbRef from './hooks/useBlurredMediaThumbRef'; import useBlurredMediaThumbRef from './hooks/useBlurredMediaThumbRef';
import ProgressSpinner from '../../ui/ProgressSpinner'; import ProgressSpinner from '../../ui/ProgressSpinner';
import OptimizedVideo from '../../ui/OptimizedVideo';
export type OwnProps = { export type OwnProps = {
id?: string; id?: string;
@ -125,15 +124,12 @@ const Video: FC<OwnProps> = ({
setPlayProgress(Math.max(0, e.currentTarget.currentTime - 1)); setPlayProgress(Math.max(0, e.currentTarget.currentTime - 1));
}, []); }, []);
const duration = (videoRef.current?.duration) || video.duration || 0; const duration = videoRef.current?.duration || video.duration || 0;
const isOwn = isOwnMessage(message); const isOwn = isOwnMessage(message);
const isForwarded = isForwardedMessage(message); const isForwarded = isForwardedMessage(message);
const { width, height } = dimensions || calculateVideoDimensions(video, isOwn, isForwarded, noAvatars); const { width, height } = dimensions || calculateVideoDimensions(video, isOwn, isForwarded, noAvatars);
useVideoAutoPause(videoRef, isInline);
useVideoCleanup(videoRef, [isInline]);
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
if (isUploading) { if (isUploading) {
if (onCancelUpload) { if (onCancelUpload) {
@ -145,7 +141,6 @@ const Video: FC<OwnProps> = ({
setIsLoadAllowed((isAllowed) => !isAllowed); setIsLoadAllowed((isAllowed) => !isAllowed);
} else if (fullMediaData && !isPlayAllowed) { } else if (fullMediaData && !isPlayAllowed) {
setIsPlayAllowed(true); setIsPlayAllowed(true);
videoRef.current!.play();
} else if (onClick) { } else if (onClick) {
onClick(message.id); onClick(message.id);
} }
@ -177,12 +172,13 @@ const Video: FC<OwnProps> = ({
draggable={!isProtected} draggable={!isProtected}
/> />
{isInline && ( {isInline && (
<video <OptimizedVideo
ref={videoRef} ref={videoRef}
canPlay={isPlayAllowed}
src={fullMediaData}
className="full-media" className="full-media"
width={width} width={width}
height={height} height={height}
autoPlay={isPlayAllowed}
muted muted
loop loop
playsInline playsInline
@ -191,9 +187,7 @@ const Video: FC<OwnProps> = ({
draggable={!isProtected} draggable={!isProtected}
onTimeUpdate={handleTimeUpdate} onTimeUpdate={handleTimeUpdate}
style={aspectRatio} style={aspectRatio}
> />
<source src={fullMediaData} />
</video>
)} )}
{isProtected && <span className="protector" />} {isProtected && <span className="protector" />}
{shouldRenderPlayButton && <i className={buildClassName('icon-large-play', playButtonClassNames)} />} {shouldRenderPlayButton && <i className={buildClassName('icon-large-play', playButtonClassNames)} />}

View File

@ -1,4 +1,4 @@
import { useCallback, useRef } from '../../../../lib/teact/teact'; import { useCallback, useEffect, useRef } from '../../../../lib/teact/teact';
import { fastRaf } from '../../../../util/schedulers'; import { fastRaf } from '../../../../util/schedulers';
import safePlay from '../../../../util/safePlay'; import safePlay from '../../../../util/safePlay';
@ -6,7 +6,6 @@ import useBackgroundMode from '../../../../hooks/useBackgroundMode';
import useHeavyAnimationCheck from '../../../../hooks/useHeavyAnimationCheck'; import useHeavyAnimationCheck from '../../../../hooks/useHeavyAnimationCheck';
export default function useVideoAutoPause(playerRef: { current: HTMLVideoElement | null }, canPlay: boolean) { export default function useVideoAutoPause(playerRef: { current: HTMLVideoElement | null }, canPlay: boolean) {
const wasPlaying = useRef(playerRef.current?.paused);
const canPlayRef = useRef(); const canPlayRef = useRef();
canPlayRef.current = canPlay; canPlayRef.current = canPlay;
@ -15,23 +14,15 @@ export default function useVideoAutoPause(playerRef: { current: HTMLVideoElement
const freezePlaying = useCallback(() => { const freezePlaying = useCallback(() => {
isFrozenRef.current = true; isFrozenRef.current = true;
if (!playerRef.current) { playerRef.current?.pause();
return;
}
wasPlaying.current = !playerRef.current.paused;
if (wasPlaying.current) {
playerRef.current.pause();
}
}, [playerRef]); }, [playerRef]);
const unfreezePlaying = useCallback(() => { const unfreezePlaying = useCallback(() => {
isFrozenRef.current = false; isFrozenRef.current = false;
if ( if (
playerRef.current && wasPlaying.current && canPlayRef.current playerRef.current && canPlayRef.current
// At this point HTMLVideoElement can be unmounted from the DOM // At this point `HTMLVideoElement` can be unmounted from the DOM
&& document.body.contains(playerRef.current) && document.body.contains(playerRef.current)
) { ) {
safePlay(playerRef.current); safePlay(playerRef.current);
@ -46,11 +37,24 @@ export default function useVideoAutoPause(playerRef: { current: HTMLVideoElement
useHeavyAnimationCheck(freezePlaying, unfreezePlaying); useHeavyAnimationCheck(freezePlaying, unfreezePlaying);
const handlePlaying = useCallback(() => { const handlePlaying = useCallback(() => {
if (isFrozenRef.current) { if (!canPlayRef.current || isFrozenRef.current) {
wasPlaying.current = true;
playerRef.current!.pause(); playerRef.current!.pause();
} }
}, [playerRef]); }, [playerRef]);
useEffect(() => {
if (!playerRef.current) {
return;
}
if (canPlay) {
if (!isFrozenRef.current) {
safePlay(playerRef.current);
}
} else {
playerRef.current!.pause();
}
}, [canPlay, playerRef]);
return { handlePlaying }; return { handlePlaying };
} }

View File

@ -0,0 +1,34 @@
import React, { memo, useRef } from '../../lib/teact/teact';
import useVideoAutoPause from '../middle/message/hooks/useVideoAutoPause';
import useVideoCleanup from '../../hooks/useVideoCleanup';
type OwnProps =
{
canPlay: boolean;
ref?: React.RefObject<HTMLVideoElement>;
}
& React.DetailedHTMLProps<React.VideoHTMLAttributes<HTMLVideoElement>, HTMLVideoElement>;
function OptimizedVideo({
ref,
canPlay,
...restProps
}: OwnProps) {
// eslint-disable-next-line no-null/no-null
const localRef = useRef<HTMLVideoElement>(null);
if (!ref) {
ref = localRef;
}
const { handlePlaying } = useVideoAutoPause(ref, canPlay);
useVideoCleanup(ref, []);
return (
// eslint-disable-next-line react/jsx-props-no-spreading
<video ref={ref} autoPlay {...restProps} onPlaying={handlePlaying} />
);
}
export default memo(OptimizedVideo);