···55@@ -41,6 +41,11 @@ sealed class PlayerEvent {
66 override val name = "playToEnd"
77 }
88-88+99+ data class PlayerTimeRemainingChanged(val timeRemaining: Double): PlayerEvent() {
1010+ override val name = "timeRemainingChange"
1111+ override val arguments = arrayOf(timeRemaining)
···3232 setTimeBarInteractive(requireLinearPlayback)
3333+ setShowSubtitleButton(true)
3434 }
3535-3535+3636 @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
3737@@ -27,7 +28,8 @@ internal fun PlayerView.setTimeBarInteractive(interactive: Boolean) {
3838-3838+3939 @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
4040 internal fun PlayerView.setFullscreenButtonVisibility(visible: Boolean) {
4141- val fullscreenButton = findViewById<android.widget.ImageButton>(androidx.media3.ui.R.id.exo_fullscreen)
···9393+ "onEnterFullscreen",
9494+ "onExitFullscreen"
9595 )
9696-9696+9797 Prop("player") { view: VideoView, player: VideoPlayer ->
9898diff --git a/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoPlayer.kt b/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoPlayer.kt
9999index 58f00af..5ad8237 100644
···101101+++ b/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoPlayer.kt
102102@@ -1,5 +1,6 @@
103103 package expo.modules.video
104104-104104+105105+import ProgressTracker
106106 import android.content.Context
107107 import android.view.SurfaceView
···111111 .setLooper(context.mainLooper)
112112 .build()
113113+ var progressTracker: ProgressTracker? = null
114114-114114+115115 val serviceConnection = PlaybackServiceConnection(WeakReference(player))
116116-116116+117117 var playing by IgnoreSameSet(false) { new, old ->
118118 sendEvent(PlayerEvent.IsPlayingChanged(new, old))
119119+ addOrRemoveProgressTracker()
120120 }
121121-121121+122122 var uncommittedSource: VideoSource? = source
123123@@ -141,6 +144,9 @@ class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSou
124124 }
125125-125125+126126 override fun close() {
127127+ this.progressTracker?.remove()
128128+ this.progressTracker = null
···133133@@ -228,7 +234,7 @@ class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSou
134134 listeners.removeAll { it.get() == videoPlayerListener }
135135 }
136136-136136+137137- private fun sendEvent(event: PlayerEvent) {
138138+ fun sendEvent(event: PlayerEvent) {
139139 // Emits to the native listeners
···173173 val onPictureInPictureStop by EventDispatcher<Unit>()
174174+ val onEnterFullscreen by EventDispatcher()
175175+ val onExitFullscreen by EventDispatcher()
176176-176176+177177 var willEnterPiP: Boolean = false
178178 var isInFullscreen: Boolean = false
179179@@ -154,6 +156,7 @@ class VideoView(context: Context, appContext: AppContext) : ExpoView(context, ap
···183183+ onEnterFullscreen(mapOf())
184184 isInFullscreen = true
185185 }
186186-186186+187187@@ -162,6 +165,7 @@ class VideoView(context: Context, appContext: AppContext) : ExpoView(context, ap
188188 val fullScreenButton: ImageButton = playerView.findViewById(androidx.media3.ui.R.id.exo_fullscreen)
189189 fullScreenButton.setImageResource(androidx.media3.ui.R.drawable.exo_icon_fullscreen_enter)
···191191+ this.onExitFullscreen(mapOf())
192192 isInFullscreen = false
193193 }
194194-194194+195195diff --git a/node_modules/expo-video/build/VideoPlayer.types.d.ts b/node_modules/expo-video/build/VideoPlayer.types.d.ts
196196-index a09fcfe..65fe29a 100644
196196+index a09fcfe..5eac9e5 100644
197197--- a/node_modules/expo-video/build/VideoPlayer.types.d.ts
198198+++ b/node_modules/expo-video/build/VideoPlayer.types.d.ts
199199@@ -128,6 +128,8 @@ export type VideoPlayerEvents = {
···219219 }
220220 //# sourceMappingURL=VideoView.types.d.ts.map
221221\ No newline at end of file
222222+diff --git a/node_modules/expo-video/ios/VideoManager.swift b/node_modules/expo-video/ios/VideoManager.swift
223223+index 094a8b0..412fd0c 100644
224224+--- a/node_modules/expo-video/ios/VideoManager.swift
225225++++ b/node_modules/expo-video/ios/VideoManager.swift
226226+@@ -51,45 +51,45 @@ class VideoManager {
227227+ // MARK: - Audio Session Management
228228+229229+ internal func setAppropriateAudioSessionOrWarn() {
230230+- let audioSession = AVAudioSession.sharedInstance()
231231+- var audioSessionCategoryOptions: AVAudioSession.CategoryOptions = []
232232+-
233233+- let isAnyPlayerPlaying = videoPlayers.allObjects.contains { player in
234234+- player.isPlaying
235235+- }
236236+- let areAllPlayersMuted = videoPlayers.allObjects.allSatisfy { player in
237237+- player.isMuted
238238+- }
239239+- let needsPiPSupport = videoViews.allObjects.contains { view in
240240+- view.allowPictureInPicture
241241+- }
242242+- let anyPlayerShowsNotification = videoPlayers.allObjects.contains { player in
243243+- player.showNowPlayingNotification
244244+- }
245245+- // The notification won't be shown if we allow the audio to mix with others
246246+- let shouldAllowMixing = (!isAnyPlayerPlaying || areAllPlayersMuted) && !anyPlayerShowsNotification
247247+- let isOutputtingAudio = !areAllPlayersMuted && isAnyPlayerPlaying
248248+- let shouldUpdateToAllowMixing = !audioSession.categoryOptions.contains(.mixWithOthers) && shouldAllowMixing
249249+-
250250+- if shouldAllowMixing {
251251+- audioSessionCategoryOptions.insert(.mixWithOthers)
252252+- }
253253+-
254254+- if isOutputtingAudio || needsPiPSupport || shouldUpdateToAllowMixing || anyPlayerShowsNotification {
255255+- do {
256256+- try audioSession.setCategory(.playback, mode: .moviePlayback)
257257+- } catch {
258258+- log.warn("Failed to set audio session category. This might cause issues with audio playback and Picture in Picture. \(error.localizedDescription)")
259259+- }
260260+- }
261261+-
262262+- // Make sure audio session is active if any video is playing
263263+- if isAnyPlayerPlaying {
264264+- do {
265265+- try audioSession.setActive(true)
266266+- } catch {
267267+- log.warn("Failed to activate the audio session. This might cause issues with audio playback. \(error.localizedDescription)")
268268+- }
269269+- }
270270++// let audioSession = AVAudioSession.sharedInstance()
271271++// var audioSessionCategoryOptions: AVAudioSession.CategoryOptions = []
272272++//
273273++// let isAnyPlayerPlaying = videoPlayers.allObjects.contains { player in
274274++// player.isPlaying
275275++// }
276276++// let areAllPlayersMuted = videoPlayers.allObjects.allSatisfy { player in
277277++// player.isMuted
278278++// }
279279++// let needsPiPSupport = videoViews.allObjects.contains { view in
280280++// view.allowPictureInPicture
281281++// }
282282++// let anyPlayerShowsNotification = videoPlayers.allObjects.contains { player in
283283++// player.showNowPlayingNotification
284284++// }
285285++// // The notification won't be shown if we allow the audio to mix with others
286286++// let shouldAllowMixing = (!isAnyPlayerPlaying || areAllPlayersMuted) && !anyPlayerShowsNotification
287287++// let isOutputtingAudio = !areAllPlayersMuted && isAnyPlayerPlaying
288288++// let shouldUpdateToAllowMixing = !audioSession.categoryOptions.contains(.mixWithOthers) && shouldAllowMixing
289289++//
290290++// if shouldAllowMixing {
291291++// audioSessionCategoryOptions.insert(.mixWithOthers)
292292++// }
293293++//
294294++// if isOutputtingAudio || needsPiPSupport || shouldUpdateToAllowMixing || anyPlayerShowsNotification {
295295++// do {
296296++// try audioSession.setCategory(.playback, mode: .moviePlayback)
297297++// } catch {
298298++// log.warn("Failed to set audio session category. This might cause issues with audio playback and Picture in Picture. \(error.localizedDescription)")
299299++// }
300300++// }
301301++//
302302++// // Make sure audio session is active if any video is playing
303303++// if isAnyPlayerPlaying {
304304++// do {
305305++// try audioSession.setActive(true)
306306++// } catch {
307307++// log.warn("Failed to activate the audio session. This might cause issues with audio playback. \(error.localizedDescription)")
308308++// }
309309++// }
310310+ }
311311+ }
222312diff --git a/node_modules/expo-video/ios/VideoModule.swift b/node_modules/expo-video/ios/VideoModule.swift
223313index c537a12..e4a918f 100644
224314--- a/node_modules/expo-video/ios/VideoModule.swift
···232322+ "onEnterFullscreen",
233323+ "onExitFullscreen"
234324 )
235235-325325+236326 Prop("player") { (view, player: VideoPlayer?) in
237327diff --git a/node_modules/expo-video/ios/VideoPlayer.swift b/node_modules/expo-video/ios/VideoPlayer.swift
238238-index 3315b88..f482390 100644
328328+index 3315b88..733ab1f 100644
239329--- a/node_modules/expo-video/ios/VideoPlayer.swift
240330+++ b/node_modules/expo-video/ios/VideoPlayer.swift
241331@@ -185,6 +185,10 @@ internal final class VideoPlayer: SharedRef<AVPlayer>, Hashable, VideoPlayerObse
242332 safeEmit(event: "sourceChange", arguments: newVideoPlayerItem?.videoSource, oldVideoPlayerItem?.videoSource)
243333 }
244244-334334+245335+ func onPlayerTimeRemainingChanged(player: AVPlayer, timeRemaining: Double) {
246336+ safeEmit(event: "timeRemainingChange", arguments: timeRemaining)
247337+ }
···250340 if self.appContext != nil {
251341 self.emit(event: event, arguments: repeat each arguments)
252342diff --git a/node_modules/expo-video/ios/VideoPlayerObserver.swift b/node_modules/expo-video/ios/VideoPlayerObserver.swift
253253-index d289e26..de9a26f 100644
343343+index d289e26..ea4d96f 100644
254344--- a/node_modules/expo-video/ios/VideoPlayerObserver.swift
255345+++ b/node_modules/expo-video/ios/VideoPlayerObserver.swift
256346@@ -21,6 +21,7 @@ protocol VideoPlayerObserverDelegate: AnyObject {
···259349 func onPlayerItemStatusChanged(player: AVPlayer, oldStatus: AVPlayerItem.Status?, newStatus: AVPlayerItem.Status)
260350+ func onPlayerTimeRemainingChanged(player: AVPlayer, timeRemaining: Double)
261351 }
262262-352352+263353 // Default implementations for the delegate
264354@@ -33,6 +34,7 @@ extension VideoPlayerObserverDelegate {
265355 func onItemChanged(player: AVPlayer, oldVideoPlayerItem: VideoPlayerItem?, newVideoPlayerItem: VideoPlayerItem?) {}
···267357 func onPlayerItemStatusChanged(player: AVPlayer, oldStatus: AVPlayerItem.Status?, newStatus: AVPlayerItem.Status) {}
268358+ func onPlayerTimeRemainingChanged(player: AVPlayer, timeRemaining: Double) {}
269359 }
270270-360360+271361 // Wrapper used to store WeakReferences to the observer delegate
272362@@ -91,6 +93,7 @@ class VideoPlayerObserver {
273363 private var playerVolumeObserver: NSKeyValueObservation?
274364 private var playerCurrentItemObserver: NSKeyValueObservation?
275365 private var playerIsMutedObserver: NSKeyValueObservation?
276366+ private var playerPeriodicTimeObserver: Any?
277277-367367+278368 // Current player item observers
279369 private var playbackBufferEmptyObserver: NSKeyValueObservation?
280370@@ -152,6 +155,9 @@ class VideoPlayerObserver {
···285375+ player?.removeTimeObserver(playerPeriodicTimeObserver)
286376+ }
287377 }
288288-378378+289379 private func initializeCurrentPlayerItemObservers(player: AVPlayer, playerItem: AVPlayerItem) {
290380@@ -270,6 +276,7 @@ class VideoPlayerObserver {
291291-381381+292382 if isPlaying != (player.timeControlStatus == .playing) {
293383 isPlaying = player.timeControlStatus == .playing
294384+ addPeriodicTimeObserverIfNeeded()
295385 }
296386 }
297297-387387+298388@@ -310,4 +317,28 @@ class VideoPlayerObserver {
299389 }
300390 }
···329419--- a/node_modules/expo-video/ios/VideoView.swift
330420+++ b/node_modules/expo-video/ios/VideoView.swift
331421@@ -41,6 +41,8 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
332332-422422+333423 let onPictureInPictureStart = EventDispatcher()
334424 let onPictureInPictureStop = EventDispatcher()
335425+ let onEnterFullscreen = EventDispatcher()
336426+ let onExitFullscreen = EventDispatcher()
337337-427427+338428 public override var bounds: CGRect {
339429 didSet {
340430@@ -163,6 +165,7 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
···344434+ onEnterFullscreen()
345435 isFullscreen = true
346436 }
347347-437437+348438@@ -179,6 +182,7 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
349439 if wasPlaying {
350440 self.player?.pointer.play()
···364454+
365455+ timeRemainingChange(timeRemaining: number): void;
366456 };
367367-457457+368458 /**
369459diff --git a/node_modules/expo-video/src/VideoView.types.ts b/node_modules/expo-video/src/VideoView.types.ts
370460index 29fe5db..e1fbf59 100644
+3
patches/expo-video+1.2.4.patch.md
···4455This patch adds two props to `VideoView`: `onEnterFullscreen` and `onExitFullscreen` which do exactly what they say on
66the tin.
77+88+This patch also removes the audio session management that Expo does on its own, as we handle audio session management
99+ourselves.