import {useCallback, useEffect, useRef, useState} from 'react' import {ActivityIndicator, View} from 'react-native' import {ImageBackground} from 'expo-image' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' import { createPlaybackTelemetry, type PlaybackTelemetry, } from '#/lib/media/video/playbackTelemetry' import {userStyle} from '#/lib/userstyles' import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' import {atoms as a, platform} from '#/alf' import {Button} from '#/components/Button' import {useThrottledValue} from '#/components/hooks/useThrottledValue' import {ConstrainedImage} from '#/components/images/AutoSizedImage' import {PlayButtonIcon} from '#/components/video/PlayButtonIcon' import {useAnalytics} from '#/analytics' import {type app} from '#/lexicons' import {GifPresentationControls} from './GifPresentationControls' import {VideoEmbedInnerNative} from './VideoEmbedInner/VideoEmbedInnerNative' import * as VideoFallback from './VideoEmbedInner/VideoFallback' interface Props { embed: app.bsky.embed.video.View post?: app.bsky.feed.defs.PostView } export function VideoEmbed({embed, post}: Props) { const [key, setKey] = useState(0) const renderError = useCallback( (error: unknown) => ( setKey(key + 1)} /> ), [key], ) let aspectRatio: number | undefined const dims = embed.aspectRatio if (dims) { aspectRatio = dims.width / dims.height if (Number.isNaN(aspectRatio)) { aspectRatio = undefined } } let constrained: number | undefined if (aspectRatio !== undefined) { const ratio = 1 / 2 // max of 1:2 ratio in feeds constrained = Math.max(aspectRatio, ratio) } const contents = ( ) return ( {contents} ) } function InnerWrapper({embed, post}: Props) { const {_} = useLingui() const ax = useAnalytics() const ref = useRef<{togglePlayback: () => void}>(null) const [status, setStatus] = useState<'playing' | 'paused' | 'pending'>( 'pending', ) const [isLoading, setIsLoading] = useState(false) const [isActive, setIsActive] = useState(false) const showSpinner = useThrottledValue(isActive && isLoading, 100) /* * Created lazily on first activation so videos that are never scrolled into * the active position cost nothing. */ const telemetryRef = useRef(null) const impressionTrackedRef = useRef(false) const playbackStartTrackedRef = useRef(false) useEffect(() => { return () => { telemetryRef.current?.deactivated() } }, []) const showOverlay = !isActive || isLoading || (status === 'paused' && !isActive) || status === 'pending' if (!isActive && status !== 'pending') { setStatus('pending') } return ( <> { setStatus(s) if (s === 'playing') { telemetryRef.current?.playing() } }} setIsLoading={loading => { setIsLoading(loading) if (!loading) { telemetryRef.current?.ready() } }} setIsActive={active => { setIsActive(active) if (active) { if (!impressionTrackedRef.current) { impressionTrackedRef.current = true ax.metric('video:impression', { postUri: post?.uri, postAuthorDid: post?.author.did, context: 'embed', presentation: embed.presentation === 'gif' ? 'gif' : 'video', }) } if (telemetryRef.current == null) { telemetryRef.current = createPlaybackTelemetry({ surface: 'feed', presentation: embed.presentation === 'gif' ? 'gif' : 'video', }) } telemetryRef.current.activated() } else { telemetryRef.current?.deactivated() } }} onPlaybackStart={autoplay => { if (playbackStartTrackedRef.current) return playbackStartTrackedRef.current = true ax.metric('video:playback:start', { postUri: post?.uri, postAuthorDid: post?.author.did, context: 'embed', presentation: embed.presentation === 'gif' ? 'gif' : 'video', autoplay, }) }} onError={error => { telemetryRef.current?.error(error) ax.metric('video:playback:failed', { surface: 'feed', presentation: embed.presentation === 'gif' ? 'gif' : 'video', errorClass: 'PlayerError', errorMessage: error.slice(0, 256), playlist: embed.playlist, }) }} ref={ref} /> {showOverlay && (embed.presentation === 'gif' ? ( { ref.current?.togglePlayback() }} altText={embed.alt} /> ) : ( ))} ) } function VideoError({retry}: {error: unknown; retry: () => void}) { return ( An error occurred while loading the video. Please try again later. ) }