Skip to content

Commit

Permalink
Merge pull request #57353 from HezekielT/fix/ios-handle-unsupported-v…
Browse files Browse the repository at this point in the history
…ideo-formats

Fix: Handle unsupported videos in iOS
  • Loading branch information
puneetlath authored Mar 7, 2025
2 parents 5466c1d + 8c1cf87 commit d01c94e
Show file tree
Hide file tree
Showing 10 changed files with 98 additions and 4 deletions.
14 changes: 14 additions & 0 deletions assets/images/video-slash.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 22 additions & 0 deletions patches/expo-av+15.0.1.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
diff --git a/node_modules/expo-av/ios/EXAV/EXAVPlayerData.m b/node_modules/expo-av/ios/EXAV/EXAVPlayerData.m
index 99dc808..01e4bb9 100644
--- a/node_modules/expo-av/ios/EXAV/EXAVPlayerData.m
+++ b/node_modules/expo-av/ios/EXAV/EXAVPlayerData.m
@@ -158,8 +158,16 @@ NSString *const EXAVPlayerDataObserverMetadataKeyPath = @"timedMetadata";
// unless we preload, the asset will not necessarily load the duration by the time we try to play it.
// http://stackoverflow.com/questions/20581567/avplayer-and-avfoundationerrordomain-code-11819
EX_WEAKIFY(self);
- [avAsset loadValuesAsynchronouslyForKeys:@[ @"duration" ] completionHandler:^{
+ [avAsset loadValuesAsynchronouslyForKeys:@[ @"isPlayable", @"duration" ] completionHandler:^{
EX_ENSURE_STRONGIFY(self);
+ NSError *error = nil;
+ AVKeyValueStatus status = [avAsset statusOfValueForKey:@"isPlayable" error:&error];
+
+ if (status == AVKeyValueStatusLoaded && !avAsset.isPlayable) {
+ NSString *errorMessage = @"Load encountered an error: [AVAsset isPlayable:] returned false. The asset does not contains a playable content or is not supported by the device.";
+ [self _finishLoadWithError:errorMessage];
+ return;
+ }

// We prepare three items for AVQueuePlayer, so when the first finishes playing,
// second can start playing and the third can start preparing to play.
4 changes: 2 additions & 2 deletions src/components/AttachmentOfflineIndicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ function AttachmentOfflineIndicator({isPreview = false}: AttachmentOfflineIndica
/>
{!isPreview && (
<View>
<Text style={[styles.notFoundTextHeader]}>{translate('common.youAppearToBeOffline')}</Text>
<Text>{translate('common.attachementWillBeAvailableOnceBackOnline')}</Text>
<Text style={[styles.notFoundTextHeader, styles.ph10]}>{translate('common.youAppearToBeOffline')}</Text>
<Text style={[styles.textAlignCenter, styles.ph11, styles.textSupporting]}>{translate('common.attachementWillBeAvailableOnceBackOnline')}</Text>
</View>
)}
</View>
Expand Down
2 changes: 2 additions & 0 deletions src/components/Icon/Expensicons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ import UserEye from '@assets/images/user-eye.svg';
import UserPlus from '@assets/images/user-plus.svg';
import User from '@assets/images/user.svg';
import Users from '@assets/images/users.svg';
import VideoSlash from '@assets/images/video-slash.svg';
import VolumeHigh from '@assets/images/volume-high.svg';
import VolumeLow from '@assets/images/volume-low.svg';
import Wallet from '@assets/images/wallet.svg';
Expand Down Expand Up @@ -383,6 +384,7 @@ export {
User,
UserCheck,
Users,
VideoSlash,
VolumeHigh,
VolumeLow,
Wallet,
Expand Down
10 changes: 9 additions & 1 deletion src/components/VideoPlayer/BaseVideoPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import shouldReplayVideo from './shouldReplayVideo';
import type {VideoPlayerProps, VideoWithOnFullScreenUpdate} from './types';
import useHandleNativeVideoControls from './useHandleNativeVideoControls';
import * as VideoUtils from './utils';
import VideoErrorIndicator from './VideoErrorIndicator';
import VideoPlayerControls from './VideoPlayerControls';

function BaseVideoPlayer({
Expand Down Expand Up @@ -75,6 +76,7 @@ function BaseVideoPlayer({
const [isLoading, setIsLoading] = useState(true);
const [isEnded, setIsEnded] = useState(false);
const [isBuffering, setIsBuffering] = useState(true);
const [hasError, setHasError] = useState(false);
// we add "#t=0.001" at the end of the URL to skip first milisecond of the video and always be able to show proper video preview when video is paused at the beginning
const [sourceURL] = useState(() => VideoUtils.addSkipTimeTagToURL(url.includes('blob:') || url.includes('file:///') ? url : addEncryptedAuthTokenToURL(url), 0.001));
const [isPopoverVisible, setIsPopoverVisible] = useState(false);
Expand Down Expand Up @@ -489,11 +491,17 @@ function BaseVideoPlayer({
}}
onPlaybackStatusUpdate={handlePlaybackStatusUpdate}
onFullscreenUpdate={handleFullscreenUpdate}
onError={() => {
setHasError(true);
}}
/>
</View>
)}
</PressableWithoutFeedback>
{((isLoading && !isOffline) || (isBuffering && !isPlaying)) && <FullScreenLoadingIndicator style={[styles.opacity1, styles.bgTransparent]} />}
{hasError && !isBuffering && !isOffline && <VideoErrorIndicator isPreview={isPreview} />}
{((isLoading && !isOffline && !hasError) || (isBuffering && !isPlaying && !hasError)) && (
<FullScreenLoadingIndicator style={[styles.opacity1, styles.bgTransparent]} />
)}
{isLoading && (isOffline || !isBuffering) && <AttachmentOfflineIndicator isPreview={isPreview} />}
{controlStatusState !== CONST.VIDEO_PLAYER.CONTROLS_STATUS.HIDE && !isLoading && (isPopoverVisible || isHovered || canUseTouchScreen || isEnded) && (
<VideoPlayerControls
Expand Down
40 changes: 40 additions & 0 deletions src/components/VideoPlayer/VideoErrorIndicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react';
import {View} from 'react-native';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import variables from '@styles/variables';

type VideoErrorIndicatorProps = {
/** Whether it is a preview or not */
isPreview?: boolean;
};

function VideoErrorIndicator({isPreview = false}: VideoErrorIndicatorProps) {
const theme = useTheme();
const styles = useThemeStyles();
const {translate} = useLocalize();

return (
<View style={[styles.flexColumn, styles.alignItemsCenter, styles.justifyContentCenter, styles.pAbsolute, styles.h100, styles.w100]}>
<Icon
fill={isPreview ? theme.border : theme.icon}
src={Expensicons.VideoSlash}
width={variables.eReceiptEmptyIconWidth}
height={variables.eReceiptEmptyIconWidth}
/>
{!isPreview && (
<View>
<Text style={[styles.notFoundTextHeader, styles.ph11]}>{translate('common.errorOccuredWhileTryingToPlayVideo')}</Text>
</View>
)}
</View>
);
}

VideoErrorIndicator.displayName = 'VideoErrorIndicator';

export default VideoErrorIndicator;
4 changes: 3 additions & 1 deletion src/components/VideoPlayerContexts/PlaybackContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ function PlaybackContextProvider({children}: ChildrenProps) {
if ('durationMillis' in status && status.durationMillis === status.positionMillis) {
newStatus.positionMillis = 0;
}
playVideoPromiseRef.current = currentVideoPlayerRef.current?.setStatusAsync(newStatus);
playVideoPromiseRef.current = currentVideoPlayerRef.current?.setStatusAsync(newStatus).catch((error: AVPlaybackStatus) => {
return error;
});
});
}, [currentVideoPlayerRef]);

Expand Down
1 change: 1 addition & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,7 @@ const translations = {
youAppearToBeOffline: 'You appear to be offline.',
thisFeatureRequiresInternet: 'This feature requires an active internet connection.',
attachementWillBeAvailableOnceBackOnline: 'Attachment will become available once back online.',
errorOccuredWhileTryingToPlayVideo: 'An error occurred while trying to play this video.',
areYouSure: 'Are you sure?',
verify: 'Verify',
yesContinue: 'Yes, continue',
Expand Down
1 change: 1 addition & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,7 @@ const translations = {
youAppearToBeOffline: 'Parece que estás desconectado.',
thisFeatureRequiresInternet: 'Esta función requiere una conexión a Internet activa.',
attachementWillBeAvailableOnceBackOnline: 'El archivo adjunto estará disponible cuando vuelvas a estar en línea.',
errorOccuredWhileTryingToPlayVideo: 'Se produjo un error al intentar reproducir este video.',
areYouSure: '¿Estás seguro?',
verify: 'Verifique',
yesContinue: 'Sí, continuar',
Expand Down
4 changes: 4 additions & 0 deletions src/styles/utils/spacing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,10 @@ export default {
paddingHorizontal: 40,
},

ph11: {
paddingHorizontal: 44,
},

ph15: {
paddingHorizontal: 60,
},
Expand Down

0 comments on commit d01c94e

Please sign in to comment.