Skip to content

Commit

Permalink
fix: Use custom controls for iOS video (#7729) (#7737)
Browse files Browse the repository at this point in the history
## **Description**

<!--
Write a short description of the changes included in this pull request,
also include relevant motivation and context. Have in mind the following
questions:
1. What is the reason for the change?
2. What is the improvement/solution?
-->

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
(TheWidlarzGroup/react-native-video#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**

<!-- If applicable, add screenshots and/or recordings to visualize the
before and after of your change. -->

### **Before**

<!-- [screenshots/recordings] -->

### **After**
<img width="537" alt="Screenshot 2023-11-09 at 9 19 57 AM"
src="https://github.com/MetaMask/metamask-mobile/assets/10508597/134b6eb2-8017-4b2c-ae7d-12a28a47d10a">


<!-- [screenshots/recordings] -->

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.
  • Loading branch information
Cal-L authored and chrisleewilcox committed Nov 13, 2023
1 parent 088a1ae commit 517178c
Showing 1 changed file with 126 additions and 23 deletions.
149 changes: 126 additions & 23 deletions app/components/Views/MediaPlayer/index.js
Original file line number Diff line number Diff line change
@@ -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 (
<View style={style}>
{loading && (
Expand All @@ -44,17 +114,50 @@ function MediaPlayer({ uri, style, onClose, textTracks, selectedTextTrack }) {
selectedTextTrack={selectedTextTrack}
/>
) : (
<Video
onLoad={onLoad}
onError={onError}
style={style}
muted
source={source}
controls
textTracks={textTracks}
selectedTextTrack={selectedTextTrack}
ignoreSilentSwitch="ignore"
/>
<>
<Video
onLoad={onLoad}
onError={onError}
style={style}
muted={isMuted}
paused={!isPlaying}
source={source}
controls={false}
fullscreen={false}
textTracks={textTracks}
selectedTextTrack={selectedTextTrack}
ignoreSilentSwitch="ignore"
ref={videoRef}
/>
{/**
* Use custom controls for iOS since iOS 17.2+ begins crashing. https://github.com/react-native-video/react-native-video/issues/3329
*/}
<TapGestureHandler onEnded={onPressVideoControls}>
<Animated.View style={videoControlsStyle}>
<View style={styles.playButtonCircle}>
<Ionicons
name={isPlaying ? 'ios-play' : 'ios-pause'}
size={24}
color={colors.overlay.inverse}
style={styles.playIcon}
/>
</View>
</Animated.View>
</TapGestureHandler>
{isPlaying ? (
<TouchableOpacity
activeOpacity={1}
style={styles.volumeButtonCircle}
onPress={onPressVolumeControls}
>
<Ionicons
name={isMuted ? 'ios-volume-off' : 'ios-volume-mute'}
size={isMuted ? 22 : 28}
color={colors.overlay.inverse}
/>
</TouchableOpacity>
) : null}
</>
)}
</View>
);
Expand Down

0 comments on commit 517178c

Please sign in to comment.