Message / Video: Better progressive loading

This commit is contained in:
Alexander Zinchuk 2022-11-27 19:16:47 +01:00
parent 128d25047f
commit a050b10aa8
19 changed files with 192 additions and 151 deletions

View File

@ -110,7 +110,7 @@ const ProfilePhoto: FC<OwnProps> = ({
disablePictureInPicture disablePictureInPicture
loop loop
playsInline playsInline
onPlay={markVideoReady} onReady={markVideoReady}
/> />
) : ( ) : (
<img <img

View File

@ -170,7 +170,7 @@ const StickerView: FC<OwnProps> = ({
muted muted
loop={!loopLimit} loop={!loopLimit}
disablePictureInPicture disablePictureInPicture
onPlay={markPlayerReady} onReady={markPlayerReady}
onEnded={onVideoEnded} onEnded={onVideoEnded}
/> />
) : ( ) : (

View File

@ -338,7 +338,7 @@ function getNodes(origin: MediaViewerOrigin, message?: ApiMessage) {
return { return {
container, container,
mediaEl: mediaEls?.[mediaEls.length - 1], mediaEl: mediaEls?.[0],
}; };
} }

View File

@ -73,7 +73,7 @@ export const useMediaProps = ({
return getChatAvatarHash(avatarOwner, isFull ? 'big' : 'normal'); return getChatAvatarHash(avatarOwner, isFull ? 'big' : 'normal');
} }
} }
return message && getMessageMediaHash(message, isFull ? 'viewerFull' : 'viewerPreview'); return message && getMessageMediaHash(message, isFull ? 'full' : 'preview');
}, [avatarOwner, message, avatarMedia, mediaId]); }, [avatarOwner, message, avatarMedia, mediaId]);
const pictogramBlobUrl = useMedia( const pictogramBlobUrl = useMedia(
@ -97,7 +97,7 @@ export const useMediaProps = ({
} = useMediaWithLoadProgress( } = useMediaWithLoadProgress(
getMediaHash(true), getMediaHash(true),
undefined, undefined,
message && getMessageMediaFormat(message, 'viewerFull'), message && getMessageMediaFormat(message, 'full'),
undefined, undefined,
delay, delay,
); );

View File

@ -81,9 +81,9 @@ const MessageListContent: FC<OwnProps> = ({
const { openHistoryCalendar } = getActions(); const { openHistoryCalendar } = getActions();
const { const {
observeIntersectionForMedia,
observeIntersectionForReading, observeIntersectionForReading,
observeIntersectionForAnimatedStickers, observeIntersectionForLoading,
observeIntersectionForPlaying,
} = useMessageObservers(type, containerRef, memoFirstUnreadIdRef); } = useMessageObservers(type, containerRef, memoFirstUnreadIdRef);
const { const {
@ -144,8 +144,8 @@ const MessageListContent: FC<OwnProps> = ({
key={message.id} key={message.id}
message={message} message={message}
observeIntersectionForReading={observeIntersectionForReading} observeIntersectionForReading={observeIntersectionForReading}
observeIntersectionForLoading={observeIntersectionForMedia} observeIntersectionForLoading={observeIntersectionForLoading}
observeIntersectionForPlaying={observeIntersectionForAnimatedStickers} observeIntersectionForPlaying={observeIntersectionForPlaying}
memoFirstUnreadIdRef={memoFirstUnreadIdRef} memoFirstUnreadIdRef={memoFirstUnreadIdRef}
appearanceOrder={messageCountToAnimate - ++appearanceIndex} appearanceOrder={messageCountToAnimate - ++appearanceIndex}
isLastInList={isLastInList} isLastInList={isLastInList}
@ -196,8 +196,8 @@ const MessageListContent: FC<OwnProps> = ({
key={key} key={key}
message={message} message={message}
observeIntersectionForBottom={observeIntersectionForReading} observeIntersectionForBottom={observeIntersectionForReading}
observeIntersectionForMedia={observeIntersectionForMedia} observeIntersectionForLoading={observeIntersectionForLoading}
observeIntersectionForAnimatedStickers={observeIntersectionForAnimatedStickers} observeIntersectionForPlaying={observeIntersectionForPlaying}
album={album} album={album}
noAvatars={noAvatars} noAvatars={noAvatars}
withAvatar={position.isLastInGroup && withUsers && !isOwn && !(message.id === threadTopMessageId)} withAvatar={position.isLastInGroup && withUsers && !isOwn && !(message.id === threadTopMessageId)}

View File

@ -7,9 +7,9 @@ import { IS_ANDROID, IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver'; import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
import useBackgroundMode from '../../../hooks/useBackgroundMode'; import useBackgroundMode from '../../../hooks/useBackgroundMode';
const INTERSECTION_THROTTLE_FOR_MEDIA = IS_ANDROID ? 1000 : 350;
const INTERSECTION_MARGIN_FOR_MEDIA = IS_SINGLE_COLUMN_LAYOUT ? 300 : 500;
const INTERSECTION_THROTTLE_FOR_READING = 150; const INTERSECTION_THROTTLE_FOR_READING = 150;
const INTERSECTION_THROTTLE_FOR_MEDIA = IS_ANDROID ? 1000 : 350;
const INTERSECTION_MARGIN_FOR_LOADING = IS_SINGLE_COLUMN_LAYOUT ? 300 : 500;
export default function useMessageObservers( export default function useMessageObservers(
type: MessageListType, type: MessageListType,
@ -18,14 +18,6 @@ export default function useMessageObservers(
) { ) {
const { markMessageListRead, markMentionsRead, animateUnreadReaction } = getActions(); const { markMessageListRead, markMentionsRead, animateUnreadReaction } = getActions();
const {
observe: observeIntersectionForMedia,
} = useIntersectionObserver({
rootRef: containerRef,
throttleMs: INTERSECTION_THROTTLE_FOR_MEDIA,
margin: INTERSECTION_MARGIN_FOR_MEDIA,
});
const { const {
observe: observeIntersectionForReading, freeze: freezeForReading, unfreeze: unfreezeForReading, observe: observeIntersectionForReading, freeze: freezeForReading, unfreeze: unfreezeForReading,
} = useIntersectionObserver({ } = useIntersectionObserver({
@ -78,14 +70,22 @@ export default function useMessageObservers(
useBackgroundMode(freezeForReading, unfreezeForReading); useBackgroundMode(freezeForReading, unfreezeForReading);
const { observe: observeIntersectionForAnimatedStickers } = useIntersectionObserver({ const {
observe: observeIntersectionForLoading,
} = useIntersectionObserver({
rootRef: containerRef,
throttleMs: INTERSECTION_THROTTLE_FOR_MEDIA,
margin: INTERSECTION_MARGIN_FOR_LOADING,
});
const { observe: observeIntersectionForPlaying } = useIntersectionObserver({
rootRef: containerRef, rootRef: containerRef,
throttleMs: INTERSECTION_THROTTLE_FOR_MEDIA, throttleMs: INTERSECTION_THROTTLE_FOR_MEDIA,
}); });
return { return {
observeIntersectionForMedia,
observeIntersectionForReading, observeIntersectionForReading,
observeIntersectionForAnimatedStickers, observeIntersectionForLoading,
observeIntersectionForPlaying,
}; };
} }

View File

@ -84,7 +84,7 @@ const Album: FC<OwnProps & StateProps> = ({
<PhotoWithSelect <PhotoWithSelect
id={`album-media-${getMessageHtmlId(message.id)}`} id={`album-media-${getMessageHtmlId(message.id)}`}
message={message} message={message}
observeIntersection={observeIntersection} observeIntersectionForLoading={observeIntersection}
canAutoLoad={canAutoLoad} canAutoLoad={canAutoLoad}
shouldAffectAppendix={shouldAffectAppendix} shouldAffectAppendix={shouldAffectAppendix}
uploadProgress={uploadProgress} uploadProgress={uploadProgress}
@ -101,7 +101,7 @@ const Album: FC<OwnProps & StateProps> = ({
<VideoWithSelect <VideoWithSelect
id={`album-media-${getMessageHtmlId(message.id)}`} id={`album-media-${getMessageHtmlId(message.id)}`}
message={message} message={message}
observeIntersection={observeIntersection} observeIntersectionForLoading={observeIntersection}
canAutoLoad={canAutoLoad} canAutoLoad={canAutoLoad}
canAutoPlay={canAutoPlay} canAutoPlay={canAutoPlay}
uploadProgress={uploadProgress} uploadProgress={uploadProgress}

View File

@ -143,8 +143,8 @@ type OwnProps =
{ {
message: ApiMessage; message: ApiMessage;
observeIntersectionForBottom: ObserveFn; observeIntersectionForBottom: ObserveFn;
observeIntersectionForMedia: ObserveFn; observeIntersectionForLoading: ObserveFn;
observeIntersectionForAnimatedStickers: ObserveFn; observeIntersectionForPlaying: ObserveFn;
album?: IAlbum; album?: IAlbum;
noAvatars?: boolean; noAvatars?: boolean;
withAvatar?: boolean; withAvatar?: boolean;
@ -235,8 +235,8 @@ const Message: FC<OwnProps & StateProps> = ({
message, message,
chatUsername, chatUsername,
observeIntersectionForBottom, observeIntersectionForBottom,
observeIntersectionForMedia, observeIntersectionForLoading,
observeIntersectionForAnimatedStickers, observeIntersectionForPlaying,
album, album,
noAvatars, noAvatars,
withAvatar, withAvatar,
@ -634,7 +634,7 @@ const Message: FC<OwnProps & StateProps> = ({
text={hiddenName} text={hiddenName}
lastSyncTime={lastSyncTime} lastSyncTime={lastSyncTime}
onClick={(avatarUser || avatarChat) ? handleAvatarClick : undefined} onClick={(avatarUser || avatarChat) ? handleAvatarClick : undefined}
observeIntersection={observeIntersectionForMedia} observeIntersection={observeIntersectionForLoading}
animationLevel={animationLevel} animationLevel={animationLevel}
withVideo withVideo
/> />
@ -693,16 +693,16 @@ const Message: FC<OwnProps & StateProps> = ({
noUserColors={isOwn} noUserColors={isOwn}
isProtected={isProtected} isProtected={isProtected}
sender={replyMessageSender} sender={replyMessageSender}
observeIntersectionForLoading={observeIntersectionForMedia} observeIntersectionForLoading={observeIntersectionForLoading}
observeIntersectionForPlaying={observeIntersectionForAnimatedStickers} observeIntersectionForPlaying={observeIntersectionForPlaying}
onClick={handleReplyClick} onClick={handleReplyClick}
/> />
)} )}
{sticker && ( {sticker && (
<Sticker <Sticker
message={message} message={message}
observeIntersection={observeIntersectionForMedia} observeIntersection={observeIntersectionForLoading}
observeIntersectionForPlaying={observeIntersectionForAnimatedStickers} observeIntersectionForPlaying={observeIntersectionForPlaying}
shouldLoop={shouldLoopStickers} shouldLoop={shouldLoopStickers}
lastSyncTime={lastSyncTime} lastSyncTime={lastSyncTime}
shouldPlayEffect={( shouldPlayEffect={(
@ -719,7 +719,7 @@ const Message: FC<OwnProps & StateProps> = ({
customEmojiId={animatedCustomEmoji} customEmojiId={animatedCustomEmoji}
withEffects={isUserId(chatId)} withEffects={isUserId(chatId)}
isOwn={isOwn} isOwn={isOwn}
observeIntersection={observeIntersectionForMedia} observeIntersection={observeIntersectionForLoading}
lastSyncTime={lastSyncTime} lastSyncTime={lastSyncTime}
forceLoadPreview={isLocal} forceLoadPreview={isLocal}
messageId={messageId} messageId={messageId}
@ -732,7 +732,7 @@ const Message: FC<OwnProps & StateProps> = ({
emoji={animatedEmoji} emoji={animatedEmoji}
withEffects={isUserId(chatId)} withEffects={isUserId(chatId)}
isOwn={isOwn} isOwn={isOwn}
observeIntersection={observeIntersectionForMedia} observeIntersection={observeIntersectionForLoading}
lastSyncTime={lastSyncTime} lastSyncTime={lastSyncTime}
forceLoadPreview={isLocal} forceLoadPreview={isLocal}
messageId={messageId} messageId={messageId}
@ -744,7 +744,7 @@ const Message: FC<OwnProps & StateProps> = ({
<Album <Album
album={album!} album={album!}
albumLayout={albumLayout!} albumLayout={albumLayout!}
observeIntersection={observeIntersectionForMedia} observeIntersection={observeIntersectionForLoading}
isOwn={isOwn} isOwn={isOwn}
isProtected={isProtected} isProtected={isProtected}
hasCustomAppendix={hasCustomAppendix} hasCustomAppendix={hasCustomAppendix}
@ -762,7 +762,7 @@ const Message: FC<OwnProps & StateProps> = ({
{!isAlbum && photo && ( {!isAlbum && photo && (
<Photo <Photo
message={message} message={message}
observeIntersection={observeIntersectionForMedia} observeIntersection={observeIntersectionForLoading}
noAvatars={noAvatars} noAvatars={noAvatars}
canAutoLoad={canAutoLoadMedia} canAutoLoad={canAutoLoadMedia}
uploadProgress={uploadProgress} uploadProgress={uploadProgress}
@ -777,7 +777,7 @@ const Message: FC<OwnProps & StateProps> = ({
{!isAlbum && video && video.isRound && ( {!isAlbum && video && video.isRound && (
<RoundVideo <RoundVideo
message={message} message={message}
observeIntersection={observeIntersectionForMedia} observeIntersection={observeIntersectionForLoading}
canAutoLoad={canAutoLoadMedia} canAutoLoad={canAutoLoadMedia}
lastSyncTime={lastSyncTime} lastSyncTime={lastSyncTime}
isDownloading={isDownloading} isDownloading={isDownloading}
@ -786,7 +786,8 @@ const Message: FC<OwnProps & StateProps> = ({
{!isAlbum && video && !video.isRound && ( {!isAlbum && video && !video.isRound && (
<Video <Video
message={message} message={message}
observeIntersection={observeIntersectionForMedia} observeIntersectionForLoading={observeIntersectionForLoading}
observeIntersectionForPlaying={observeIntersectionForPlaying}
noAvatars={noAvatars} noAvatars={noAvatars}
canAutoLoad={canAutoLoadMedia} canAutoLoad={canAutoLoadMedia}
canAutoPlay={canAutoPlayMedia} canAutoPlay={canAutoPlayMedia}
@ -824,7 +825,7 @@ const Message: FC<OwnProps & StateProps> = ({
{document && ( {document && (
<Document <Document
message={message} message={message}
observeIntersection={observeIntersectionForMedia} observeIntersection={observeIntersectionForLoading}
canAutoLoad={canAutoLoadMedia} canAutoLoad={canAutoLoadMedia}
autoLoadFileMaxSizeMb={autoLoadFileMaxSizeMb} autoLoadFileMaxSizeMb={autoLoadFileMaxSizeMb}
uploadProgress={uploadProgress} uploadProgress={uploadProgress}
@ -876,8 +877,8 @@ const Message: FC<OwnProps & StateProps> = ({
emojiSize={emojiSize} emojiSize={emojiSize}
highlight={highlight} highlight={highlight}
isProtected={isProtected} isProtected={isProtected}
observeIntersectionForLoading={observeIntersectionForMedia} observeIntersectionForLoading={observeIntersectionForLoading}
observeIntersectionForPlaying={observeIntersectionForAnimatedStickers} observeIntersectionForPlaying={observeIntersectionForPlaying}
withTranslucentThumbs={isCustomShape} withTranslucentThumbs={isCustomShape}
/> />
{metaPosition === 'in-text' && renderReactionsAndMeta()} {metaPosition === 'in-text' && renderReactionsAndMeta()}
@ -887,7 +888,7 @@ const Message: FC<OwnProps & StateProps> = ({
{webPage && ( {webPage && (
<WebPage <WebPage
message={message} message={message}
observeIntersection={observeIntersectionForMedia} observeIntersection={observeIntersectionForLoading}
noAvatars={noAvatars} noAvatars={noAvatars}
canAutoLoad={canAutoLoadMedia} canAutoLoad={canAutoLoadMedia}
canAutoPlay={canAutoPlayMedia} canAutoPlay={canAutoPlayMedia}
@ -960,8 +961,8 @@ const Message: FC<OwnProps & StateProps> = ({
<CustomEmoji <CustomEmoji
documentId={senderEmojiStatus.documentId} documentId={senderEmojiStatus.documentId}
loopLimit={EMOJI_STATUS_LOOP_LIMIT} loopLimit={EMOJI_STATUS_LOOP_LIMIT}
observeIntersectionForLoading={observeIntersectionForMedia} observeIntersectionForLoading={observeIntersectionForLoading}
observeIntersectionForPlaying={observeIntersectionForAnimatedStickers} observeIntersectionForPlaying={observeIntersectionForPlaying}
/> />
)} )}
{!asForwarded && !senderEmojiStatus && senderIsPremium && <PremiumIcon />} {!asForwarded && !senderEmojiStatus && senderIsPremium && <PremiumIcon />}

View File

@ -88,7 +88,9 @@ const Photo: FC<OwnProps> = ({
const fullMediaData = localBlobUrl || mediaData; const fullMediaData = localBlobUrl || mediaData;
const [withThumb] = useState(!fullMediaData); const [withThumb] = useState(!fullMediaData);
const thumbRef = useBlurredMediaThumbRef(message, fullMediaData); const noThumb = Boolean(fullMediaData);
const thumbRef = useBlurredMediaThumbRef(message, noThumb);
const thumbClassNames = useMediaTransition(!noThumb);
const { const {
loadProgress: downloadProgress, loadProgress: downloadProgress,
@ -105,7 +107,6 @@ const Photo: FC<OwnProps> = ({
); );
const wasLoadDisabled = usePrevious(isLoadAllowed) === false; const wasLoadDisabled = usePrevious(isLoadAllowed) === false;
const transitionClassNames = useMediaTransition(fullMediaData);
const { const {
shouldRender: shouldRenderSpinner, shouldRender: shouldRenderSpinner,
transitionClassNames: spinnerClassNames, transitionClassNames: spinnerClassNames,
@ -169,21 +170,21 @@ const Photo: FC<OwnProps> = ({
style={style} style={style}
onClick={isUploading ? undefined : handleClick} onClick={isUploading ? undefined : handleClick}
> >
{withThumb && (
<canvas
ref={thumbRef}
className="thumbnail"
style={`width: ${width}px; height: ${height}px;${aspectRatio}`}
/>
)}
<img <img
src={fullMediaData} src={fullMediaData}
className={`full-media ${transitionClassNames}`} className="full-media"
width={width} width={width}
height={height} height={height}
alt="" alt=""
draggable={!isProtected} draggable={!isProtected}
/> />
{withThumb && (
<canvas
ref={thumbRef}
className={buildClassName('thumbnail', thumbClassNames)}
style={`width: ${width}px; height: ${height}px;${aspectRatio}`}
/>
)}
{isProtected && <span className="protector" />} {isProtected && <span className="protector" />}
{shouldRenderSpinner && !shouldRenderDownloadButton && ( {shouldRenderSpinner && !shouldRenderDownloadButton && (
<div className={`media-loading ${spinnerClassNames}`}> <div className={`media-loading ${spinnerClassNames}`}>

View File

@ -4,13 +4,6 @@
height: 15rem; height: 15rem;
cursor: pointer; cursor: pointer;
.thumbnail-wrapper {
width: 15rem;
height: 15rem;
border-radius: 50%;
overflow: hidden;
}
.video-wrapper { .video-wrapper {
position: absolute; position: absolute;
left: 0; left: 0;
@ -19,6 +12,10 @@
overflow: hidden; overflow: hidden;
} }
canvas {
border-radius: 50%;
}
.progress { .progress {
position: absolute; position: absolute;
top: 0; top: 0;

View File

@ -82,7 +82,7 @@ const RoundVideo: FC<OwnProps> = ({
const isTransferring = (isLoadAllowed && !isBuffered) || isDownloading; const isTransferring = (isLoadAllowed && !isBuffered) || isDownloading;
const wasLoadDisabled = usePrevious(isLoadAllowed) === false; const wasLoadDisabled = usePrevious(isLoadAllowed) === false;
const transitionClassNames = useMediaTransition(mediaData); const thumbClassNames = useMediaTransition(!mediaData);
const { const {
shouldRender: shouldSpinnerRender, shouldRender: shouldSpinnerRender,
transitionClassNames: spinnerClassNames, transitionClassNames: spinnerClassNames,
@ -180,30 +180,19 @@ const RoundVideo: FC<OwnProps> = ({
setProgress(playerEl.currentTime / playerEl.duration); setProgress(playerEl.currentTime / playerEl.duration);
}, []); }, []);
const videoClassName = buildClassName('full-media', transitionClassNames);
return ( return (
<div <div
ref={ref} ref={ref}
className="RoundVideo media-inner" className="RoundVideo media-inner"
onClick={handleClick} onClick={handleClick}
> >
{withThumb && (
<div className="thumbnail-wrapper">
<canvas
ref={thumbRef}
className="thumbnail"
style={`width: ${ROUND_VIDEO_DIMENSIONS_PX}px; height: ${ROUND_VIDEO_DIMENSIONS_PX}px`}
/>
</div>
)}
{mediaData && ( {mediaData && (
<div className="video-wrapper"> <div className="video-wrapper">
<OptimizedVideo <OptimizedVideo
canPlay={shouldPlay} canPlay={shouldPlay}
ref={playerRef} ref={playerRef}
src={mediaData} src={mediaData}
className={videoClassName} className="full-media"
width={ROUND_VIDEO_DIMENSIONS_PX} width={ROUND_VIDEO_DIMENSIONS_PX}
height={ROUND_VIDEO_DIMENSIONS_PX} height={ROUND_VIDEO_DIMENSIONS_PX}
autoPlay autoPlay
@ -218,6 +207,13 @@ const RoundVideo: FC<OwnProps> = ({
/> />
</div> </div>
)} )}
{withThumb && (
<canvas
ref={thumbRef}
className={buildClassName('thumbnail', thumbClassNames)}
style={`width: ${ROUND_VIDEO_DIMENSIONS_PX}px; height: ${ROUND_VIDEO_DIMENSIONS_PX}px`}
/>
)}
<div className="progress" ref={playingProgressRef} /> <div className="progress" ref={playingProgressRef} />
{shouldSpinnerRender && ( {shouldSpinnerRender && (
<div className={`media-loading ${spinnerClassNames}`}> <div className={`media-loading ${spinnerClassNames}`}>

View File

@ -11,21 +11,22 @@ import { calculateVideoDimensions } from '../../common/helpers/mediaDimensions';
import { import {
getMediaTransferState, getMediaTransferState,
getMessageMediaFormat, getMessageMediaFormat,
getMessageMediaHash, getMessageMediaHash, getMessageMediaThumbDataUri,
getMessageVideo, getMessageVideo,
getMessageWebPageVideo, getMessageWebPageVideo,
isForwardedMessage, isForwardedMessage,
isOwnMessage, isOwnMessage,
} from '../../../global/helpers'; } from '../../../global/helpers';
import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
import * as mediaLoader from '../../../util/mediaLoader';
import { useIsIntersecting } from '../../../hooks/useIntersectionObserver'; import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
import useMediaWithLoadProgress from '../../../hooks/useMediaWithLoadProgress'; import useMediaWithLoadProgress from '../../../hooks/useMediaWithLoadProgress';
import useMedia from '../../../hooks/useMedia'; 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 useMediaTransition from '../../../hooks/useMediaTransition'; import useMediaTransition from '../../../hooks/useMediaTransition';
import useBlurredMediaThumbRef from './hooks/useBlurredMediaThumbRef'; import useBlurredMediaThumbRef from './hooks/useBlurredMediaThumbRef';
import useFlag from '../../../hooks/useFlag';
import ProgressSpinner from '../../ui/ProgressSpinner'; import ProgressSpinner from '../../ui/ProgressSpinner';
import OptimizedVideo from '../../ui/OptimizedVideo'; import OptimizedVideo from '../../ui/OptimizedVideo';
@ -33,7 +34,8 @@ import OptimizedVideo from '../../ui/OptimizedVideo';
export type OwnProps = { export type OwnProps = {
id?: string; id?: string;
message: ApiMessage; message: ApiMessage;
observeIntersection: ObserveFn; observeIntersectionForLoading: ObserveFn;
observeIntersectionForPlaying?: ObserveFn;
noAvatars?: boolean; noAvatars?: boolean;
canAutoLoad?: boolean; canAutoLoad?: boolean;
canAutoPlay?: boolean; canAutoPlay?: boolean;
@ -50,7 +52,8 @@ export type OwnProps = {
const Video: FC<OwnProps> = ({ const Video: FC<OwnProps> = ({
id, id,
message, message,
observeIntersection, observeIntersectionForLoading,
observeIntersectionForPlaying,
noAvatars, noAvatars,
canAutoLoad, canAutoLoad,
canAutoPlay, canAutoPlay,
@ -71,32 +74,41 @@ const Video: FC<OwnProps> = ({
const video = (getMessageVideo(message) || getMessageWebPageVideo(message))!; const video = (getMessageVideo(message) || getMessageWebPageVideo(message))!;
const localBlobUrl = video.blobUrl; const localBlobUrl = video.blobUrl;
const isIntersecting = useIsIntersecting(ref, observeIntersection); const isIntersectingForLoading = useIsIntersecting(ref, observeIntersectionForLoading);
const isIntersectingForPlaying = (
useIsIntersecting(ref, observeIntersectionForPlaying)
&& isIntersectingForLoading
);
const wasIntersectedRef = useRef(isIntersectingForLoading);
if (isIntersectingForPlaying && !wasIntersectedRef.current) {
wasIntersectedRef.current = true;
}
const [isLoadAllowed, setIsLoadAllowed] = useState(canAutoLoad); const [isLoadAllowed, setIsLoadAllowed] = useState(canAutoLoad);
const shouldLoad = Boolean(isLoadAllowed && isIntersecting && lastSyncTime); const shouldLoad = Boolean(isLoadAllowed && isIntersectingForLoading && lastSyncTime);
const [isPlayAllowed, setIsPlayAllowed] = useState(canAutoPlay); const [isPlayAllowed, setIsPlayAllowed] = useState(canAutoPlay);
const previewBlobUrl = useMedia( const fullMediaHash = getMessageMediaHash(message, 'inline');
getMessageMediaHash(message, 'pictogram'), const [isFullMediaPreloaded] = useState(Boolean(fullMediaHash && mediaLoader.getFromMemory(fullMediaHash)));
!(isIntersecting && lastSyncTime),
getMessageMediaFormat(message, 'pictogram'),
lastSyncTime,
);
const previewClassNames = useMediaTransition(previewBlobUrl);
const { mediaData, loadProgress } = useMediaWithLoadProgress( const { mediaData, loadProgress } = useMediaWithLoadProgress(
getMessageMediaHash(message, 'inline'), fullMediaHash, !shouldLoad, getMessageMediaFormat(message, 'inline'), lastSyncTime,
!shouldLoad,
getMessageMediaFormat(message, 'inline'),
lastSyncTime,
); );
const fullMediaData = localBlobUrl || mediaData; const fullMediaData = localBlobUrl || mediaData;
const isInline = Boolean(isIntersecting && fullMediaData); const [isPlayerReady, markPlayerReady] = useFlag();
// Thumbnail is always rendered, so we can only disable blur if we have a preview const hasThumb = Boolean(getMessageMediaThumbDataUri(message));
const [withThumb] = useState(!previewBlobUrl);
const thumbRef = useBlurredMediaThumbRef(message, previewBlobUrl); const previewMediaHash = getMessageMediaHash(message, 'preview');
const [isPreviewPreloaded] = useState(Boolean(previewMediaHash && mediaLoader.getFromMemory(previewMediaHash)));
const canLoadPreview = isIntersectingForLoading && lastSyncTime;
const previewBlobUrl = useMedia(previewMediaHash, !canLoadPreview, undefined, lastSyncTime);
const previewClassNames = useMediaTransition((hasThumb || previewBlobUrl) && !isPlayerReady);
const noThumb = !hasThumb || previewBlobUrl || isPlayerReady;
const thumbRef = useBlurredMediaThumbRef(message, noThumb);
const thumbClassNames = useMediaTransition(!noThumb);
const isInline = fullMediaData && wasIntersectedRef.current;
const { loadProgress: downloadProgress } = useMediaWithLoadProgress( const { loadProgress: downloadProgress } = useMediaWithLoadProgress(
getMessageMediaHash(message, 'download'), getMessageMediaHash(message, 'download'),
@ -105,21 +117,20 @@ const Video: FC<OwnProps> = ({
lastSyncTime, lastSyncTime,
); );
const { isBuffered, bufferingHandlers } = useBuffering(!canAutoLoad);
const { isUploading, isTransferring, transferProgress } = getMediaTransferState( const { isUploading, isTransferring, transferProgress } = getMediaTransferState(
message, message,
uploadProgress || (isDownloading ? downloadProgress : loadProgress), uploadProgress || (isDownloading ? downloadProgress : loadProgress),
(shouldLoad && !isBuffered) || isDownloading, (shouldLoad && !isPlayerReady && !isFullMediaPreloaded) || isDownloading,
); );
const wasLoadDisabled = usePrevious(isLoadAllowed) === false; const wasLoadDisabled = usePrevious(isLoadAllowed) === false;
const { const {
shouldRender: shouldRenderSpinner, shouldRender: shouldRenderSpinner,
transitionClassNames: spinnerClassNames, transitionClassNames: spinnerClassNames,
} = useShowTransition(isTransferring, undefined, wasLoadDisabled); } = useShowTransition(isTransferring, undefined, wasLoadDisabled);
const { const {
shouldRender: shouldRenderPlayButton,
transitionClassNames: playButtonClassNames, transitionClassNames: playButtonClassNames,
} = useShowTransition(isLoadAllowed && !isPlayAllowed && !shouldRenderSpinner); } = useShowTransition(Boolean((isLoadAllowed || fullMediaData) && !isPlayAllowed && !shouldRenderSpinner));
const [playProgress, setPlayProgress] = useState<number>(0); const [playProgress, setPlayProgress] = useState<number>(0);
const handleTimeUpdate = useCallback((e: React.SyntheticEvent<HTMLVideoElement>) => { const handleTimeUpdate = useCallback((e: React.SyntheticEvent<HTMLVideoElement>) => {
@ -149,10 +160,11 @@ const Video: FC<OwnProps> = ({
}, [isUploading, isDownloading, fullMediaData, isPlayAllowed, onClick, onCancelUpload, message]); }, [isUploading, isDownloading, fullMediaData, isPlayAllowed, onClick, onCancelUpload, message]);
const className = buildClassName('media-inner dark', !isUploading && 'interactive'); const className = buildClassName('media-inner dark', !isUploading && 'interactive');
const aspectRatio = withAspectRatio ? `aspect-ratio: ${(width / height).toFixed(3)}/ 1` : '';
const style = dimensions const dimensionsStyle = dimensions ? ` left: ${dimensions.x}px; top: ${dimensions.y}px;` : '';
? `width: ${width}px; height: ${height}px; left: ${dimensions.x}px; top: ${dimensions.y}px;${aspectRatio}` const aspectRatioStyle = withAspectRatio ? ` aspect-ratio: ${(width / height).toFixed(3)}/ 1;` : '';
: ''; const style = `width: ${width}px; height: ${height}px;${dimensionsStyle}${aspectRatioStyle}`;
return ( return (
<div <div
ref={ref} ref={ref}
@ -161,47 +173,42 @@ const Video: FC<OwnProps> = ({
style={style} style={style}
onClick={isUploading ? undefined : handleClick} onClick={isUploading ? undefined : handleClick}
> >
{withThumb ? (
<canvas
ref={thumbRef}
className="thumbnail"
style={`width: ${width}px; height: ${height}px;${aspectRatio}`}
/>
) : (
<img
src={previewBlobUrl}
className={buildClassName('thumbnail', previewClassNames)}
style={`width: ${width}px; height: ${height}px;${aspectRatio}`}
alt=""
draggable={!isProtected}
/>
)}
{isInline && ( {isInline && (
<OptimizedVideo <OptimizedVideo
ref={videoRef} ref={videoRef}
canPlay={isPlayAllowed}
src={fullMediaData} src={fullMediaData}
className="full-media" className="full-media"
canPlay={isPlayAllowed && isIntersectingForPlaying}
width={width} width={width}
height={height} height={height}
muted muted
loop loop
playsInline playsInline
// eslint-disable-next-line react/jsx-props-no-spreading
{...bufferingHandlers}
draggable={!isProtected} draggable={!isProtected}
onTimeUpdate={handleTimeUpdate} onTimeUpdate={handleTimeUpdate}
style={aspectRatio} onReady={markPlayerReady}
/>
)}
<img
src={previewBlobUrl}
className={buildClassName('thumbnail', previewClassNames)}
alt=""
draggable={!isProtected}
/>
{hasThumb && !isPreviewPreloaded && (
<canvas
ref={thumbRef}
className={buildClassName('thumbnail', thumbClassNames)}
/> />
)} )}
{isProtected && <span className="protector" />} {isProtected && <span className="protector" />}
{shouldRenderPlayButton && <i className={buildClassName('icon-large-play', playButtonClassNames)} />} <i className={buildClassName('icon-large-play', playButtonClassNames)} />
{shouldRenderSpinner && ( {shouldRenderSpinner && (
<div className={buildClassName('media-loading', spinnerClassNames)}> <div className={buildClassName('media-loading', spinnerClassNames)}>
<ProgressSpinner progress={transferProgress} onClick={handleClick} /> <ProgressSpinner progress={transferProgress} onClick={handleClick} />
</div> </div>
)} )}
{!isLoadAllowed && ( {!isLoadAllowed && !fullMediaData && (
<i className="icon-download" /> <i className="icon-download" />
)} )}
{isTransferring ? ( {isTransferring ? (

View File

@ -121,7 +121,7 @@ const WebPage: FC<OwnProps> = ({
{!inPreview && video && ( {!inPreview && video && (
<Video <Video
message={message} message={message}
observeIntersection={observeIntersection!} observeIntersectionForLoading={observeIntersection!}
noAvatars={noAvatars} noAvatars={noAvatars}
canAutoLoad={canAutoLoad} canAutoLoad={canAutoLoad}
canAutoPlay={canAutoPlay} canAutoPlay={canAutoPlay}

View File

@ -500,7 +500,7 @@
&.interactive { &.interactive {
cursor: pointer; cursor: pointer;
&.dark video { &.dark video, &.dark canvas {
background-color: #232323; background-color: #232323;
} }
} }

View File

@ -4,10 +4,10 @@ import { IS_CANVAS_FILTER_SUPPORTED, IS_SINGLE_COLUMN_LAYOUT } from '../../../..
import { getMessageMediaThumbDataUri } from '../../../../global/helpers'; import { getMessageMediaThumbDataUri } from '../../../../global/helpers';
import useCanvasBlur from '../../../../hooks/useCanvasBlur'; import useCanvasBlur from '../../../../hooks/useCanvasBlur';
export default function useBlurredMediaThumbRef(message: ApiMessage, fullMediaData?: string) { export default function useBlurredMediaThumbRef(message: ApiMessage, isDisabled?: boolean | string) {
return useCanvasBlur( return useCanvasBlur(
getMessageMediaThumbDataUri(message), getMessageMediaThumbDataUri(message),
Boolean(fullMediaData), Boolean(isDisabled),
IS_SINGLE_COLUMN_LAYOUT && !IS_CANVAS_FILTER_SUPPORTED, IS_SINGLE_COLUMN_LAYOUT && !IS_CANVAS_FILTER_SUPPORTED,
); );
} }

View File

@ -1,33 +1,62 @@
import React, { memo, useRef } from '../../lib/teact/teact'; import React, { memo, useCallback, useRef } from '../../lib/teact/teact';
import useVideoAutoPause from '../middle/message/hooks/useVideoAutoPause'; import useVideoAutoPause from '../middle/message/hooks/useVideoAutoPause';
import useVideoCleanup from '../../hooks/useVideoCleanup'; import useVideoCleanup from '../../hooks/useVideoCleanup';
import useBuffering from '../../hooks/useBuffering';
import useOnChange from '../../hooks/useOnChange';
type OwnProps = type OwnProps =
{ {
canPlay: boolean;
ref?: React.RefObject<HTMLVideoElement>; ref?: React.RefObject<HTMLVideoElement>;
canPlay: boolean;
onReady?: NoneToVoidFunction;
} }
& React.DetailedHTMLProps<React.VideoHTMLAttributes<HTMLVideoElement>, HTMLVideoElement>; & React.DetailedHTMLProps<React.VideoHTMLAttributes<HTMLVideoElement>, HTMLVideoElement>;
function OptimizedVideo({ function OptimizedVideo({
ref, ref,
canPlay, canPlay,
onReady,
onTimeUpdate,
...restProps ...restProps
}: OwnProps) { }: OwnProps) {
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
const localRef = useRef<HTMLVideoElement>(null); const localRef = useRef<HTMLVideoElement>(null);
if (!ref) { if (!ref) {
ref = localRef; ref = localRef;
} }
const { handlePlaying } = useVideoAutoPause(ref, canPlay); const { handlePlaying: handlePlayingForAutoPause } = useVideoAutoPause(ref, canPlay);
useVideoCleanup(ref, []); useVideoCleanup(ref, []);
const isReadyRef = useRef(false);
const handleReady = useCallback(() => {
if (!isReadyRef.current) {
onReady?.();
isReadyRef.current = true;
}
}, [onReady]);
// This is only needed for browsers not allowing autoplay
const { isBuffered, bufferingHandlers } = useBuffering(true, onTimeUpdate);
const { onPlaying: handlePlayingForBuffering, ...otherBufferingHandlers } = bufferingHandlers;
useOnChange(([prevIsBuffered]) => {
if (prevIsBuffered === undefined) {
return;
}
handleReady();
}, [isBuffered]);
const handlePlaying = useCallback((e) => {
handlePlayingForAutoPause();
handlePlayingForBuffering(e);
handleReady();
}, [handlePlayingForAutoPause, handlePlayingForBuffering, handleReady]);
return ( return (
// eslint-disable-next-line react/jsx-props-no-spreading // eslint-disable-next-line react/jsx-props-no-spreading
<video ref={ref} autoPlay {...restProps} onPlaying={handlePlaying} /> <video ref={ref} autoPlay {...restProps} {...otherBufferingHandlers} onPlaying={handlePlaying} />
); );
} }

View File

@ -26,8 +26,8 @@ type Target =
'micro' 'micro'
| 'pictogram' | 'pictogram'
| 'inline' | 'inline'
| 'viewerPreview' | 'preview'
| 'viewerFull' | 'full'
| 'download'; | 'download';
export function getMessageContent(message: ApiMessage) { export function getMessageContent(message: ApiMessage) {
@ -191,9 +191,9 @@ export function getMessageMediaHash(
return `${base}?size=m`; return `${base}?size=m`;
case 'inline': case 'inline':
return !hasMessageLocalBlobUrl(message) ? getVideoOrAudioBaseHash(messageVideo, base) : undefined; return !hasMessageLocalBlobUrl(message) ? getVideoOrAudioBaseHash(messageVideo, base) : undefined;
case 'viewerPreview': case 'preview':
return `${base}?size=m`; return `${base}?size=x`;
case 'viewerFull': case 'full':
return getVideoOrAudioBaseHash(messageVideo, base); return getVideoOrAudioBaseHash(messageVideo, base);
case 'download': case 'download':
return `${base}?download`; return `${base}?download`;
@ -207,9 +207,9 @@ export function getMessageMediaHash(
return `${base}?size=m`; return `${base}?size=m`;
case 'inline': case 'inline':
return !hasMessageLocalBlobUrl(message) ? `${base}?size=x` : undefined; return !hasMessageLocalBlobUrl(message) ? `${base}?size=x` : undefined;
case 'viewerPreview': case 'preview':
return `${base}?size=x`; return `${base}?size=x`;
case 'viewerFull': case 'full':
case 'download': case 'download':
return `${base}?size=z`; return `${base}?size=z`;
} }
@ -220,13 +220,13 @@ export function getMessageMediaHash(
case 'micro': case 'micro':
case 'pictogram': case 'pictogram':
case 'inline': case 'inline':
case 'viewerPreview': case 'preview':
if (!getDocumentHasPreview(document) || hasMessageLocalBlobUrl(message)) { if (!getDocumentHasPreview(document) || hasMessageLocalBlobUrl(message)) {
return undefined; return undefined;
} }
return `${base}?size=m`; return `${base}?size=m`;
case 'viewerFull': case 'full':
case 'download': case 'download':
return base; return base;
} }
@ -325,7 +325,7 @@ export function getMessageMediaFormat(
} }
if (fullVideo && IS_PROGRESSIVE_SUPPORTED && ( if (fullVideo && IS_PROGRESSIVE_SUPPORTED && (
target === 'viewerFull' || target === 'inline' target === 'full' || target === 'inline'
)) { )) {
return ApiMediaFormat.Progressive; return ApiMediaFormat.Progressive;
} }

View File

@ -14,7 +14,7 @@ const DEBOUNCE = 200;
*/ */
export type BufferedRange = { start: number; end: number }; export type BufferedRange = { start: number; end: number };
const useBuffering = (noInitiallyBuffered = false) => { const useBuffering = (noInitiallyBuffered = false, onTimeUpdate?: AnyToVoidFunction) => {
const [isBuffered, setIsBuffered] = useState(!noInitiallyBuffered); const [isBuffered, setIsBuffered] = useState(!noInitiallyBuffered);
const [bufferedProgress, setBufferedProgress] = useState(0); const [bufferedProgress, setBufferedProgress] = useState(0);
const [bufferedRanges, setBufferedRanges] = useState<BufferedRange[]>([]); const [bufferedRanges, setBufferedRanges] = useState<BufferedRange[]>([]);
@ -24,6 +24,10 @@ const useBuffering = (noInitiallyBuffered = false) => {
}, []); }, []);
const handleBuffering = useCallback<BufferingEvent>((e) => { const handleBuffering = useCallback<BufferingEvent>((e) => {
if (e.type === 'timeupdate') {
onTimeUpdate?.(e);
}
const media = e.currentTarget as HTMLMediaElement; const media = e.currentTarget as HTMLMediaElement;
if (!isSafariPatchInProgress(media)) { if (!isSafariPatchInProgress(media)) {
@ -36,7 +40,7 @@ const useBuffering = (noInitiallyBuffered = false) => {
setIsBufferedDebounced(media.readyState >= MIN_READY_STATE || media.currentTime > 0); setIsBufferedDebounced(media.readyState >= MIN_READY_STATE || media.currentTime > 0);
} }
}, [setIsBufferedDebounced]); }, [onTimeUpdate, setIsBufferedDebounced]);
const bufferingHandlers = { const bufferingHandlers = {
onLoadedData: handleBuffering, onLoadedData: handleBuffering,

View File

@ -15,8 +15,14 @@
background-size: contain; background-size: contain;
} }
.thumbnail ~ .thumbnail, .thumbnail {
.thumbnail ~ .full-media, position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.media-loading { .media-loading {
position: absolute; position: absolute;
} }