From dde72b48e12de0b87da60000b7705e475c795429 Mon Sep 17 00:00:00 2001 From: Hailey Date: Tue, 3 Sep 2024 08:41:14 -0700 Subject: [PATCH] [Video] Manage foreground/background playback on the native side (#5104) --- patches/expo-video+1.2.4.patch | 93 ++++++++++++++++++- patches/expo-video+1.2.4.patch.md | 11 ++- .../VideoEmbedInner/VideoEmbedInnerNative.tsx | 29 +----- 3 files changed, 104 insertions(+), 29 deletions(-) diff --git a/patches/expo-video+1.2.4.patch b/patches/expo-video+1.2.4.patch index f9e91a43..13fe25ed 100644 --- a/patches/expo-video+1.2.4.patch +++ b/patches/expo-video+1.2.4.patch @@ -80,6 +80,57 @@ index 0000000..0249e23 + } +} \ No newline at end of file +diff --git a/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoManager.kt b/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoManager.kt +index 4b6c6d8..e20f51a 100644 +--- a/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoManager.kt ++++ b/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoManager.kt +@@ -1,5 +1,6 @@ + package expo.modules.video + ++import android.provider.MediaStore.Video + import androidx.annotation.OptIn + import androidx.media3.common.util.UnstableApi + import expo.modules.kotlin.AppContext +@@ -15,6 +16,8 @@ object VideoManager { + // Keeps track of all existing VideoPlayers, and whether they are attached to a VideoView + private var videoPlayersToVideoViews = mutableMapOf>() + ++ private var previouslyPlayingViews: MutableList? = null ++ + private lateinit var audioFocusManager: AudioFocusManager + + fun onModuleCreated(appContext: AppContext) { +@@ -69,16 +72,24 @@ object VideoManager { + return videoPlayersToVideoViews[videoPlayer]?.isNotEmpty() ?: false + } + +- fun onAppForegrounded() = Unit ++ fun onAppForegrounded() { ++ val previouslyPlayingViews = this.previouslyPlayingViews ?: return ++ for (videoView in previouslyPlayingViews) { ++ val player = videoView.videoPlayer?.player ?: continue ++ player.play() ++ } ++ this.previouslyPlayingViews = null ++ } + + fun onAppBackgrounded() { ++ val previouslyPlayingViews = mutableListOf() + for (videoView in videoViews.values) { +- if (videoView.videoPlayer?.staysActiveInBackground == false && +- !videoView.willEnterPiP && +- !videoView.isInFullscreen +- ) { +- videoView.videoPlayer?.player?.pause() ++ val player = videoView.videoPlayer?.player ?: continue ++ if (player.isPlaying) { ++ player.pause() ++ previouslyPlayingViews.add(videoView) + } + } ++ this.previouslyPlayingViews = previouslyPlayingViews + } + } diff --git a/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoModule.kt b/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoModule.kt index ec3da2a..5a1397a 100644 --- a/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoModule.kt @@ -220,12 +271,48 @@ index cb9ca6d..ed8bb7e 100644 //# sourceMappingURL=VideoView.types.d.ts.map \ No newline at end of file diff --git a/node_modules/expo-video/ios/VideoManager.swift b/node_modules/expo-video/ios/VideoManager.swift -index 094a8b0..412fd0c 100644 +index 094a8b0..3f00525 100644 --- a/node_modules/expo-video/ios/VideoManager.swift +++ b/node_modules/expo-video/ios/VideoManager.swift -@@ -51,45 +51,45 @@ class VideoManager { +@@ -12,6 +12,7 @@ class VideoManager { + + private var videoViews = NSHashTable.weakObjects() + private var videoPlayers = NSHashTable.weakObjects() ++ private var previouslyPlayingPlayers: [VideoPlayer]? + + func register(videoPlayer: VideoPlayer) { + videoPlayers.add(videoPlayer) +@@ -33,63 +34,70 @@ class VideoManager { + for videoPlayer in videoPlayers.allObjects { + videoPlayer.setTracksEnabled(true) + } ++ ++ if let previouslyPlayingPlayers = self.previouslyPlayingPlayers { ++ previouslyPlayingPlayers.forEach { player in ++ player.pointer.play() ++ } ++ } + } + + func onAppBackgrounded() { ++ var previouslyPlayingPlayers: [VideoPlayer] = [] + for videoView in videoViews.allObjects { + guard let player = videoView.player else { + continue + } +- if player.staysActiveInBackground == true { +- player.setTracksEnabled(videoView.isInPictureInPicture) +- } else if !videoView.isInPictureInPicture { ++ if player.isPlaying { + player.pointer.pause() ++ previouslyPlayingPlayers.append(player) + } + } ++ self.previouslyPlayingPlayers = previouslyPlayingPlayers + } + // MARK: - Audio Session Management - + internal func setAppropriateAudioSessionOrWarn() { - let audioSession = AVAudioSession.sharedInstance() - var audioSessionCategoryOptions: AVAudioSession.CategoryOptions = [] diff --git a/patches/expo-video+1.2.4.patch.md b/patches/expo-video+1.2.4.patch.md index 6dd85308..99c14c28 100644 --- a/patches/expo-video+1.2.4.patch.md +++ b/patches/expo-video+1.2.4.patch.md @@ -2,8 +2,17 @@ ## `expo-video` Patch -This patch adds two props to `VideoView`: `onEnterFullscreen` and `onExitFullscreen` which do exactly what they say on +### `onEnterFullScreen`/`onExitFullScreen` +Adds two props to `VideoView`: `onEnterFullscreen` and `onExitFullscreen` which do exactly what they say on the tin. +### Removing audio session management + This patch also removes the audio session management that Expo does on its own, as we handle audio session management ourselves. + +### Pausing/playing on background/foreground + +Instead of handling the pausing/playing of videos in React, we'll handle them here. There's some logic that we do not +need (around PIP mode) that we can remove, and just pause any playing players on background and then resume them on +foreground. diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx b/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx index 59f9d9f9..fa59b9c9 100644 --- a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx +++ b/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx @@ -5,12 +5,9 @@ import {VideoPlayer, VideoView} from 'expo-video' import {AppBskyEmbedVideo} from '@atproto/api' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {useIsFocused} from '@react-navigation/native' import {HITSLOP_30} from '#/lib/constants' -import {useAppState} from '#/lib/hooks/useAppState' import {clamp} from '#/lib/numbers' -import {logger} from '#/logger' import {useActiveVideoNative} from 'view/com/util/post-embeds/ActiveVideoNativeContext' import {atoms as a, useTheme} from '#/alf' import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute' @@ -29,26 +26,6 @@ export function VideoEmbedInnerNative({ const {_} = useLingui() const {player} = useActiveVideoNative() const ref = useRef(null) - const isScreenFocused = useIsFocused() - const isAppFocused = useAppState() - - useEffect(() => { - try { - if (isAppFocused === 'active' && isScreenFocused && !player.playing) { - PlatformInfo.setAudioCategory(AudioCategory.Ambient) - PlatformInfo.setAudioActive(false) - player.muted = true - player.play() - } else if (player.playing) { - player.pause() - } - } catch (err) { - logger.error( - 'Failed to play/pause while backgrounding/switching screens', - {safeMessage: err}, - ) - } - }, [isAppFocused, player, isScreenFocused]) const enterFullscreen = useCallback(() => { ref.current?.enterFullscreen() @@ -69,7 +46,7 @@ export function VideoEmbedInnerNative({ player={player} style={[a.flex_1, a.rounded_sm]} contentFit="contain" - nativeControls={true} + nativeControls={false} accessibilityIgnoresInvertColors onEnterFullscreen={() => { PlatformInfo.setAudioCategory(AudioCategory.Playback) @@ -80,7 +57,9 @@ export function VideoEmbedInnerNative({ PlatformInfo.setAudioCategory(AudioCategory.Ambient) PlatformInfo.setAudioActive(false) player.muted = true - if (!player.playing) player.play() + if (!player.playing) { + player.play() + } }} accessibilityLabel={ embed.alt ? _(msg`Video: ${embed.alt}`) : _(msg`Video`)