From 517178cd1863c9114d1fdac6edbac1a9ddd80627 Mon Sep 17 00:00:00 2001 From: Cal Leung Date: Fri, 10 Nov 2023 14:42:37 -0800 Subject: [PATCH] fix: Use custom controls for iOS video (#7729) (#7737) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** It was discovered that the video begins crashing the app for iOS 17.2+ physical devices. While there is no solution in the latest react-native-video package at the moment (https://github.com/react-native-video/react-native-video/issues/3329), we can patch it by removing the `controls` prop and temporarily use custom controls (a play/pause + mute controls). The tradeoff is that other features such as full screen and seeking will not be available on iOS. ## **Related issues** Fixes: #7729 ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** Screenshot 2023-11-09 at 9 19 57 AM iOS 17.2 Interaction https://github.com/MetaMask/metamask-mobile/assets/10508597/8976d5b7-d276-4834-9496-0ba8608e25e3 Video with play/pause + mute controls on settings https://github.com/MetaMask/metamask-mobile/assets/10508597/492fb78d-3703-4714-bed1-8ddd7b631efa Video with play/pause + mute controls on onboarding https://github.com/MetaMask/metamask-mobile/assets/10508597/14780058-c595-4fb9-8aab-9987120c8126 Android remains the same https://recordit.co/pTuloJmuIn ## **Pre-merge author checklist** - [ ] I’ve followed [MetaMask Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've clearly explained what problem this PR is solving and how it is solved. - [ ] I've linked related issues - [ ] I've included manual testing steps - [ ] I've included screenshots/recordings if applicable - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. - [ ] I’ve properly set the pull request status: - [ ] In case it's not yet "ready for review", I've set it to "draft". - [ ] In case it's "ready for review", I've changed it from "draft" to "non-draft". ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/components/Views/MediaPlayer/index.js | 149 ++++++++++++++++++---- 1 file changed, 126 insertions(+), 23 deletions(-) diff --git a/app/components/Views/MediaPlayer/index.js b/app/components/Views/MediaPlayer/index.js index 5574b6f3d51..8167fb4a01d 100644 --- a/app/components/Views/MediaPlayer/index.js +++ b/app/components/Views/MediaPlayer/index.js @@ -1,32 +1,102 @@ -import React, { useState } from 'react'; +import React, { useRef, useState } from 'react'; import PropTypes from 'prop-types'; -import { StyleSheet, View, ViewPropTypes } from 'react-native'; +import { + StyleSheet, + TouchableOpacity, + View, + ViewPropTypes, +} from 'react-native'; import AndroidMediaPlayer from './AndroidMediaPlayer'; import Video from 'react-native-video'; import Device from '../../../util/device'; import Loader from './Loader'; +import Ionicons from 'react-native-vector-icons/Ionicons'; +import { TapGestureHandler } from 'react-native-gesture-handler'; +import Animated, { + useAnimatedStyle, + useSharedValue, + withDelay, + withSequence, + withTiming, +} from 'react-native-reanimated'; +import { useStyles } from '../../../component-library/hooks'; -const styles = StyleSheet.create({ - loaderContainer: { - position: 'absolute', - zIndex: 999, - width: '100%', - height: '100%', - }, -}); +const styleSheet = ({ theme: { colors }, vars: { isPlaying } }) => + StyleSheet.create({ + loaderContainer: { + position: 'absolute', + zIndex: 999, + width: '100%', + height: '100%', + }, + playButtonCircle: { + backgroundColor: colors.overlay.default, + height: 44, + width: 44, + borderRadius: 22, + justifyContent: 'center', + alignItems: 'center', + }, + videoControlsStyle: { + ...StyleSheet.absoluteFillObject, + justifyContent: 'center', + alignItems: 'center', + }, + playIcon: { left: isPlaying ? 2 : 0 }, + volumeButtonCircle: { + backgroundColor: colors.overlay.default, + position: 'absolute', + right: 16, + top: 36, + height: 36, + width: 36, + borderRadius: 18, + justifyContent: 'center', + alignItems: 'center', + }, + }); function MediaPlayer({ uri, style, onClose, textTracks, selectedTextTrack }) { const [loading, setLoading] = useState(true); const [error, setError] = useState(false); + const videoRef = useRef(); + const [isPlaying, setIsPlaying] = useState(true); + const [isMuted, setIsMuted] = useState(true); + const videoControlsOpacity = useSharedValue(0); + const { + styles, + theme: { colors }, + } = useStyles(styleSheet, { isPlaying }); - const onLoad = () => setLoading(false); + const onLoad = () => { + setLoading(false); + setIsPlaying(true); + }; - const onError = () => setError(true); + const onError = () => { + setError(true); + setIsPlaying(false); + }; // Video source can be either a number returned by import for bundled files // or an object of the form { uri: 'http://...' } for remote files const source = Number.isInteger(uri) ? uri : { uri }; + const videoControlsStyle = useAnimatedStyle(() => ({ + ...styles.videoControlsStyle, + opacity: videoControlsOpacity.value, + })); + + const onPressVideoControls = () => { + videoControlsOpacity.value = withSequence( + withTiming(1), + withDelay(500, withTiming(0)), + ); + setIsPlaying(!isPlaying); + }; + + const onPressVolumeControls = () => setIsMuted(!isMuted); + return ( {loading && ( @@ -44,17 +114,50 @@ function MediaPlayer({ uri, style, onClose, textTracks, selectedTextTrack }) { selectedTextTrack={selectedTextTrack} /> ) : ( - );