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
loop
playsInline
onPlay={markVideoReady}
onReady={markVideoReady}
/>
) : (
<img

View File

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

View File

@ -338,7 +338,7 @@ function getNodes(origin: MediaViewerOrigin, message?: ApiMessage) {
return {
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 message && getMessageMediaHash(message, isFull ? 'viewerFull' : 'viewerPreview');
return message && getMessageMediaHash(message, isFull ? 'full' : 'preview');
}, [avatarOwner, message, avatarMedia, mediaId]);
const pictogramBlobUrl = useMedia(
@ -97,7 +97,7 @@ export const useMediaProps = ({
} = useMediaWithLoadProgress(
getMediaHash(true),
undefined,
message && getMessageMediaFormat(message, 'viewerFull'),
message && getMessageMediaFormat(message, 'full'),
undefined,
delay,
);

View File

@ -81,9 +81,9 @@ const MessageListContent: FC<OwnProps> = ({
const { openHistoryCalendar } = getActions();
const {
observeIntersectionForMedia,
observeIntersectionForReading,
observeIntersectionForAnimatedStickers,
observeIntersectionForLoading,
observeIntersectionForPlaying,
} = useMessageObservers(type, containerRef, memoFirstUnreadIdRef);
const {
@ -144,8 +144,8 @@ const MessageListContent: FC<OwnProps> = ({
key={message.id}
message={message}
observeIntersectionForReading={observeIntersectionForReading}
observeIntersectionForLoading={observeIntersectionForMedia}
observeIntersectionForPlaying={observeIntersectionForAnimatedStickers}
observeIntersectionForLoading={observeIntersectionForLoading}
observeIntersectionForPlaying={observeIntersectionForPlaying}
memoFirstUnreadIdRef={memoFirstUnreadIdRef}
appearanceOrder={messageCountToAnimate - ++appearanceIndex}
isLastInList={isLastInList}
@ -196,8 +196,8 @@ const MessageListContent: FC<OwnProps> = ({
key={key}
message={message}
observeIntersectionForBottom={observeIntersectionForReading}
observeIntersectionForMedia={observeIntersectionForMedia}
observeIntersectionForAnimatedStickers={observeIntersectionForAnimatedStickers}
observeIntersectionForLoading={observeIntersectionForLoading}
observeIntersectionForPlaying={observeIntersectionForPlaying}
album={album}
noAvatars={noAvatars}
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 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_MEDIA = IS_ANDROID ? 1000 : 350;
const INTERSECTION_MARGIN_FOR_LOADING = IS_SINGLE_COLUMN_LAYOUT ? 300 : 500;
export default function useMessageObservers(
type: MessageListType,
@ -18,14 +18,6 @@ export default function useMessageObservers(
) {
const { markMessageListRead, markMentionsRead, animateUnreadReaction } = getActions();
const {
observe: observeIntersectionForMedia,
} = useIntersectionObserver({
rootRef: containerRef,
throttleMs: INTERSECTION_THROTTLE_FOR_MEDIA,
margin: INTERSECTION_MARGIN_FOR_MEDIA,
});
const {
observe: observeIntersectionForReading, freeze: freezeForReading, unfreeze: unfreezeForReading,
} = useIntersectionObserver({
@ -78,14 +70,22 @@ export default function useMessageObservers(
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,
throttleMs: INTERSECTION_THROTTLE_FOR_MEDIA,
});
return {
observeIntersectionForMedia,
observeIntersectionForReading,
observeIntersectionForAnimatedStickers,
observeIntersectionForLoading,
observeIntersectionForPlaying,
};
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -500,7 +500,7 @@
&.interactive {
cursor: pointer;
&.dark video {
&.dark video, &.dark canvas {
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 useCanvasBlur from '../../../../hooks/useCanvasBlur';
export default function useBlurredMediaThumbRef(message: ApiMessage, fullMediaData?: string) {
export default function useBlurredMediaThumbRef(message: ApiMessage, isDisabled?: boolean | string) {
return useCanvasBlur(
getMessageMediaThumbDataUri(message),
Boolean(fullMediaData),
Boolean(isDisabled),
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 useVideoCleanup from '../../hooks/useVideoCleanup';
import useBuffering from '../../hooks/useBuffering';
import useOnChange from '../../hooks/useOnChange';
type OwnProps =
{
canPlay: boolean;
ref?: React.RefObject<HTMLVideoElement>;
canPlay: boolean;
onReady?: NoneToVoidFunction;
}
& React.DetailedHTMLProps<React.VideoHTMLAttributes<HTMLVideoElement>, HTMLVideoElement>;
function OptimizedVideo({
ref,
canPlay,
onReady,
onTimeUpdate,
...restProps
}: OwnProps) {
// eslint-disable-next-line no-null/no-null
const localRef = useRef<HTMLVideoElement>(null);
if (!ref) {
ref = localRef;
}
const { handlePlaying } = useVideoAutoPause(ref, canPlay);
const { handlePlaying: handlePlayingForAutoPause } = useVideoAutoPause(ref, canPlay);
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 (
// 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'
| 'pictogram'
| 'inline'
| 'viewerPreview'
| 'viewerFull'
| 'preview'
| 'full'
| 'download';
export function getMessageContent(message: ApiMessage) {
@ -191,9 +191,9 @@ export function getMessageMediaHash(
return `${base}?size=m`;
case 'inline':
return !hasMessageLocalBlobUrl(message) ? getVideoOrAudioBaseHash(messageVideo, base) : undefined;
case 'viewerPreview':
return `${base}?size=m`;
case 'viewerFull':
case 'preview':
return `${base}?size=x`;
case 'full':
return getVideoOrAudioBaseHash(messageVideo, base);
case 'download':
return `${base}?download`;
@ -207,9 +207,9 @@ export function getMessageMediaHash(
return `${base}?size=m`;
case 'inline':
return !hasMessageLocalBlobUrl(message) ? `${base}?size=x` : undefined;
case 'viewerPreview':
case 'preview':
return `${base}?size=x`;
case 'viewerFull':
case 'full':
case 'download':
return `${base}?size=z`;
}
@ -220,13 +220,13 @@ export function getMessageMediaHash(
case 'micro':
case 'pictogram':
case 'inline':
case 'viewerPreview':
case 'preview':
if (!getDocumentHasPreview(document) || hasMessageLocalBlobUrl(message)) {
return undefined;
}
return `${base}?size=m`;
case 'viewerFull':
case 'full':
case 'download':
return base;
}
@ -325,7 +325,7 @@ export function getMessageMediaFormat(
}
if (fullVideo && IS_PROGRESSIVE_SUPPORTED && (
target === 'viewerFull' || target === 'inline'
target === 'full' || target === 'inline'
)) {
return ApiMediaFormat.Progressive;
}

View File

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

View File

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