import React, {useCallback, useEffect, useId, useState} from 'react' import {View} from 'react-native' import {ImageBackground} from 'expo-image' import {PlayerError, VideoPlayerStatus} from 'expo-video' import {AppBskyEmbedVideo} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {clamp} from '#/lib/numbers' import {useAutoplayDisabled} from 'state/preferences' import {VideoEmbedInnerNative} from '#/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative' import {atoms as a} from '#/alf' import {Button} from '#/components/Button' import {useIsWithinMessage} from '#/components/dms/MessageContext' import {Loader} from '#/components/Loader' import {PlayButtonIcon} from '#/components/video/PlayButtonIcon' import {VisibilityView} from '../../../../../modules/expo-bluesky-swiss-army' import {ErrorBoundary} from '../ErrorBoundary' import {useActiveVideoNative} from './ActiveVideoNativeContext' import * as VideoFallback from './VideoEmbedInner/VideoFallback' interface Props { embed: AppBskyEmbedVideo.View } export function VideoEmbed({embed}: Props) { const [key, setKey] = useState(0) const renderError = useCallback( (error: unknown) => ( setKey(key + 1)} /> ), [key], ) let aspectRatio = 16 / 9 if (embed.aspectRatio) { const {width, height} = embed.aspectRatio aspectRatio = width / height aspectRatio = clamp(aspectRatio, 1 / 1, 3 / 1) } return ( ) } function InnerWrapper({embed}: Props) { const {_} = useLingui() const {activeSource, activeViewId, setActiveSource, player} = useActiveVideoNative() const viewId = useId() const [playerStatus, setPlayerStatus] = useState< VideoPlayerStatus | 'paused' >('paused') const [isMuted, setIsMuted] = useState(player.muted) const [isFullscreen, setIsFullscreen] = React.useState(false) const [timeRemaining, setTimeRemaining] = React.useState(0) const isWithinMessage = useIsWithinMessage() const disableAutoplay = useAutoplayDisabled() || isWithinMessage const isActive = embed.playlist === activeSource && activeViewId === viewId // There are some different loading states that we should pay attention to and show a spinner for const isLoading = isActive && (playerStatus === 'waitingToPlayAtSpecifiedRate' || playerStatus === 'loading') // This happens whenever the visibility view decides that another video should start playing const showOverlay = !isActive || isLoading || playerStatus === 'paused' // send error up to error boundary const [error, setError] = useState(null) if (error) { throw error } useEffect(() => { if (isActive) { // eslint-disable-next-line @typescript-eslint/no-shadow const volumeSub = player.addListener('volumeChange', ({isMuted}) => { setIsMuted(isMuted) }) const timeSub = player.addListener( 'timeRemainingChange', secondsRemaining => { setTimeRemaining(secondsRemaining) }, ) const statusSub = player.addListener( 'statusChange', (status, oldStatus, playerError) => { setPlayerStatus(status) if (status === 'error') { setError(playerError ?? new Error('Unknown player error')) } if (status === 'readyToPlay' && oldStatus !== 'readyToPlay') { player.play() } }, ) return () => { volumeSub.remove() timeSub.remove() statusSub.remove() } } }, [player, isActive, disableAutoplay]) // The source might already be active (for example, if you are scrolling a list of quotes and its all the same // video). In those cases, just start playing. Otherwise, setting the active source will result in the video // start playback immediately const startPlaying = (ignoreAutoplayPreference: boolean) => { if (disableAutoplay && !ignoreAutoplayPreference) { return } if (isActive) { player.play() } else { setActiveSource(embed.playlist, viewId) } } const onVisibilityStatusChange = (isVisible: boolean) => { // When `isFullscreen` is true, it means we're actually still exiting the fullscreen player. Ignore these change // events if (isFullscreen) { return } if (isVisible) { startPlaying(false) } else { // Clear the active source so the video view unmounts when autoplay is disabled. Otherwise, leave it mounted // until it gets replaced by another video if (disableAutoplay) { setActiveSource(null, null) } else { player.muted = true if (player.playing) { player.pause() } } } } return ( {isActive ? ( ) : null} ) } function VideoError({retry}: {error: unknown; retry: () => void}) { return ( An error occurred while loading the video. Please try again later. ) }