From 428607d9a31f7da94ccc3b44b4499a67f2ffbd1f Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 5 Sep 2024 16:03:00 +0100 Subject: [PATCH] [Video] throw HLS errors to be caught by error boundary (#5166) * throw HLS errors to be caught by error boundary * wording tweak * do the same on native * fix type error --- src/view/com/util/post-embeds/VideoEmbed.tsx | 12 +++++-- .../com/util/post-embeds/VideoEmbed.web.tsx | 35 +++++++++++-------- .../VideoEmbedInner/VideoEmbedInnerWeb.tsx | 27 +++++++++++++- 3 files changed, 55 insertions(+), 19 deletions(-) diff --git a/src/view/com/util/post-embeds/VideoEmbed.tsx b/src/view/com/util/post-embeds/VideoEmbed.tsx index d10a6fe69b..2dafce7ba6 100644 --- a/src/view/com/util/post-embeds/VideoEmbed.tsx +++ b/src/view/com/util/post-embeds/VideoEmbed.tsx @@ -1,7 +1,7 @@ import React, {useCallback, useEffect, useId, useState} from 'react' import {View} from 'react-native' import {Image} from 'expo-image' -import {VideoPlayerStatus} from 'expo-video' +import {PlayerError, VideoPlayerStatus} from 'expo-video' import {AppBskyEmbedVideo} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -78,6 +78,12 @@ function InnerWrapper({embed}: Props) { (playerStatus === 'waitingToPlayAtSpecifiedRate' || playerStatus === 'loading') + // 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 @@ -92,10 +98,10 @@ function InnerWrapper({embed}: Props) { ) const statusSub = player.addListener( 'statusChange', - (status, _oldStatus, error) => { + (status, _oldStatus, playerError) => { setPlayerStatus(status) if (status === 'error') { - throw error + setError(playerError ?? new Error('Unknown player error')) } }, ) diff --git a/src/view/com/util/post-embeds/VideoEmbed.web.tsx b/src/view/com/util/post-embeds/VideoEmbed.web.tsx index 0001a7af5a..a25f946416 100644 --- a/src/view/com/util/post-embeds/VideoEmbed.web.tsx +++ b/src/view/com/util/post-embeds/VideoEmbed.web.tsx @@ -1,13 +1,15 @@ import React, {useCallback, useEffect, useRef, useState} from 'react' import {View} from 'react-native' import {AppBskyEmbedVideo} from '@atproto/api' -import {Trans} from '@lingui/macro' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' import {clamp} from '#/lib/numbers' import {useGate} from '#/lib/statsig/statsig' import { HLSUnsupportedError, VideoEmbedInnerWeb, + VideoNotFoundError, } from '#/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb' import {atoms as a} from '#/alf' import {ErrorBoundary} from '../ErrorBoundary' @@ -152,23 +154,26 @@ function ViewportObserver({ } function VideoError({error, retry}: {error: unknown; retry: () => void}) { - const isHLS = error instanceof HLSUnsupportedError + const {_} = useLingui() + + let showRetryButton = true + let text = null + + if (error instanceof VideoNotFoundError) { + text = _(msg`Video not found.`) + } else if (error instanceof HLSUnsupportedError) { + showRetryButton = false + text = _( + msg`Your browser does not support the video format. Please try a different browser.`, + ) + } else { + text = _(msg`An error occurred while loading the video. Please try again.`) + } return ( - - {isHLS ? ( - - Your browser does not support the video format. Please try a - different browser. - - ) : ( - - An error occurred while loading the video. Please try again later. - - )} - - {!isHLS && } + {text} + {showRetryButton && } ) } diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx b/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx index 77295c00c7..a30c0e1e9b 100644 --- a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx +++ b/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx @@ -23,6 +23,12 @@ export function VideoEmbedInnerWeb({ const [hasSubtitleTrack, setHasSubtitleTrack] = useState(false) const figId = useId() + // send error up to error boundary + const [error, setError] = useState(null) + if (error) { + throw error + } + const hlsRef = useRef(undefined) useEffect(() => { @@ -38,12 +44,25 @@ export function VideoEmbedInnerWeb({ // initial value, later on it's managed by Controls hls.autoLevelCapping = 0 - hls.on(Hls.Events.SUBTITLE_TRACKS_UPDATED, (event, data) => { + hls.on(Hls.Events.SUBTITLE_TRACKS_UPDATED, (_event, data) => { if (data.subtitleTracks.length > 0) { setHasSubtitleTrack(true) } }) + hls.on(Hls.Events.ERROR, (_event, data) => { + if (data.fatal) { + if ( + data.details === 'manifestLoadError' && + data.response?.code === 404 + ) { + setError(new VideoNotFoundError()) + } else { + setError(data.error) + } + } + }) + return () => { hlsRef.current = undefined hls.detachMedia() @@ -104,3 +123,9 @@ export class HLSUnsupportedError extends Error { super('HLS is not supported') } } + +export class VideoNotFoundError extends Error { + constructor() { + super('Video not found') + } +}