[Video] Add loading state to player (#5149)
This commit is contained in:
parent
76f493c279
commit
2556698427
5 changed files with 250 additions and 116 deletions
|
@ -5,7 +5,7 @@ index 473f964..f37aff9 100644
|
||||||
@@ -41,6 +41,11 @@ sealed class PlayerEvent {
|
@@ -41,6 +41,11 @@ sealed class PlayerEvent {
|
||||||
override val name = "playToEnd"
|
override val name = "playToEnd"
|
||||||
}
|
}
|
||||||
|
|
||||||
+ data class PlayerTimeRemainingChanged(val timeRemaining: Double): PlayerEvent() {
|
+ data class PlayerTimeRemainingChanged(val timeRemaining: Double): PlayerEvent() {
|
||||||
+ override val name = "timeRemainingChange"
|
+ override val name = "timeRemainingChange"
|
||||||
+ override val arguments = arrayOf(timeRemaining)
|
+ override val arguments = arrayOf(timeRemaining)
|
||||||
|
@ -32,10 +32,10 @@ index 9905e13..47342ff 100644
|
||||||
setTimeBarInteractive(requireLinearPlayback)
|
setTimeBarInteractive(requireLinearPlayback)
|
||||||
+ setShowSubtitleButton(true)
|
+ setShowSubtitleButton(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
|
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
|
||||||
@@ -27,7 +28,8 @@ internal fun PlayerView.setTimeBarInteractive(interactive: Boolean) {
|
@@ -27,7 +28,8 @@ internal fun PlayerView.setTimeBarInteractive(interactive: Boolean) {
|
||||||
|
|
||||||
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
|
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
|
||||||
internal fun PlayerView.setFullscreenButtonVisibility(visible: Boolean) {
|
internal fun PlayerView.setFullscreenButtonVisibility(visible: Boolean) {
|
||||||
- val fullscreenButton = findViewById<android.widget.ImageButton>(androidx.media3.ui.R.id.exo_fullscreen)
|
- val fullscreenButton = findViewById<android.widget.ImageButton>(androidx.media3.ui.R.id.exo_fullscreen)
|
||||||
|
@ -144,7 +144,7 @@ index ec3da2a..5a1397a 100644
|
||||||
+ "onEnterFullscreen",
|
+ "onEnterFullscreen",
|
||||||
+ "onExitFullscreen"
|
+ "onExitFullscreen"
|
||||||
)
|
)
|
||||||
|
|
||||||
Prop("player") { view: VideoView, player: VideoPlayer ->
|
Prop("player") { view: VideoView, player: VideoPlayer ->
|
||||||
diff --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
|
diff --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
|
||||||
index 58f00af..5ad8237 100644
|
index 58f00af..5ad8237 100644
|
||||||
|
@ -152,7 +152,7 @@ index 58f00af..5ad8237 100644
|
||||||
+++ b/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
|
||||||
@@ -1,5 +1,6 @@
|
@@ -1,5 +1,6 @@
|
||||||
package expo.modules.video
|
package expo.modules.video
|
||||||
|
|
||||||
+import ProgressTracker
|
+import ProgressTracker
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.view.SurfaceView
|
import android.view.SurfaceView
|
||||||
|
@ -162,18 +162,18 @@ index 58f00af..5ad8237 100644
|
||||||
.setLooper(context.mainLooper)
|
.setLooper(context.mainLooper)
|
||||||
.build()
|
.build()
|
||||||
+ var progressTracker: ProgressTracker? = null
|
+ var progressTracker: ProgressTracker? = null
|
||||||
|
|
||||||
val serviceConnection = PlaybackServiceConnection(WeakReference(player))
|
val serviceConnection = PlaybackServiceConnection(WeakReference(player))
|
||||||
|
|
||||||
var playing by IgnoreSameSet(false) { new, old ->
|
var playing by IgnoreSameSet(false) { new, old ->
|
||||||
sendEvent(PlayerEvent.IsPlayingChanged(new, old))
|
sendEvent(PlayerEvent.IsPlayingChanged(new, old))
|
||||||
+ addOrRemoveProgressTracker()
|
+ addOrRemoveProgressTracker()
|
||||||
}
|
}
|
||||||
|
|
||||||
var uncommittedSource: VideoSource? = source
|
var uncommittedSource: VideoSource? = source
|
||||||
@@ -141,6 +144,9 @@ class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSou
|
@@ -141,6 +144,9 @@ class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSou
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
+ this.progressTracker?.remove()
|
+ this.progressTracker?.remove()
|
||||||
+ this.progressTracker = null
|
+ this.progressTracker = null
|
||||||
|
@ -184,7 +184,7 @@ index 58f00af..5ad8237 100644
|
||||||
@@ -228,7 +234,7 @@ class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSou
|
@@ -228,7 +234,7 @@ class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSou
|
||||||
listeners.removeAll { it.get() == videoPlayerListener }
|
listeners.removeAll { it.get() == videoPlayerListener }
|
||||||
}
|
}
|
||||||
|
|
||||||
- private fun sendEvent(event: PlayerEvent) {
|
- private fun sendEvent(event: PlayerEvent) {
|
||||||
+ fun sendEvent(event: PlayerEvent) {
|
+ fun sendEvent(event: PlayerEvent) {
|
||||||
// Emits to the native listeners
|
// Emits to the native listeners
|
||||||
|
@ -224,7 +224,7 @@ index a951d80..3932535 100644
|
||||||
val onPictureInPictureStop by EventDispatcher<Unit>()
|
val onPictureInPictureStop by EventDispatcher<Unit>()
|
||||||
+ val onEnterFullscreen by EventDispatcher()
|
+ val onEnterFullscreen by EventDispatcher()
|
||||||
+ val onExitFullscreen by EventDispatcher()
|
+ val onExitFullscreen by EventDispatcher()
|
||||||
|
|
||||||
var willEnterPiP: Boolean = false
|
var willEnterPiP: Boolean = false
|
||||||
var isInFullscreen: Boolean = false
|
var isInFullscreen: Boolean = false
|
||||||
@@ -154,6 +156,7 @@ class VideoView(context: Context, appContext: AppContext) : ExpoView(context, ap
|
@@ -154,6 +156,7 @@ class VideoView(context: Context, appContext: AppContext) : ExpoView(context, ap
|
||||||
|
@ -234,7 +234,7 @@ index a951d80..3932535 100644
|
||||||
+ onEnterFullscreen(mapOf())
|
+ onEnterFullscreen(mapOf())
|
||||||
isInFullscreen = true
|
isInFullscreen = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,6 +165,7 @@ class VideoView(context: Context, appContext: AppContext) : ExpoView(context, ap
|
@@ -162,6 +165,7 @@ class VideoView(context: Context, appContext: AppContext) : ExpoView(context, ap
|
||||||
val fullScreenButton: ImageButton = playerView.findViewById(androidx.media3.ui.R.id.exo_fullscreen)
|
val fullScreenButton: ImageButton = playerView.findViewById(androidx.media3.ui.R.id.exo_fullscreen)
|
||||||
fullScreenButton.setImageResource(androidx.media3.ui.R.drawable.exo_icon_fullscreen_enter)
|
fullScreenButton.setImageResource(androidx.media3.ui.R.drawable.exo_icon_fullscreen_enter)
|
||||||
|
@ -242,9 +242,9 @@ index a951d80..3932535 100644
|
||||||
+ this.onExitFullscreen(mapOf())
|
+ this.onExitFullscreen(mapOf())
|
||||||
isInFullscreen = false
|
isInFullscreen = false
|
||||||
}
|
}
|
||||||
|
|
||||||
diff --git a/node_modules/expo-video/build/VideoPlayer.types.d.ts b/node_modules/expo-video/build/VideoPlayer.types.d.ts
|
diff --git a/node_modules/expo-video/build/VideoPlayer.types.d.ts b/node_modules/expo-video/build/VideoPlayer.types.d.ts
|
||||||
index a09fcfe..5eac9e5 100644
|
index a09fcfe..46cbae7 100644
|
||||||
--- a/node_modules/expo-video/build/VideoPlayer.types.d.ts
|
--- a/node_modules/expo-video/build/VideoPlayer.types.d.ts
|
||||||
+++ b/node_modules/expo-video/build/VideoPlayer.types.d.ts
|
+++ b/node_modules/expo-video/build/VideoPlayer.types.d.ts
|
||||||
@@ -128,6 +128,8 @@ export type VideoPlayerEvents = {
|
@@ -128,6 +128,8 @@ export type VideoPlayerEvents = {
|
||||||
|
@ -256,6 +256,15 @@ index a09fcfe..5eac9e5 100644
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* Describes the current status of the player.
|
* Describes the current status of the player.
|
||||||
|
@@ -136,7 +138,7 @@ export type VideoPlayerEvents = {
|
||||||
|
* - `readyToPlay`: The player has loaded enough data to start playing or to continue playback.
|
||||||
|
* - `error`: The player has encountered an error while loading or playing the video.
|
||||||
|
*/
|
||||||
|
-export type VideoPlayerStatus = 'idle' | 'loading' | 'readyToPlay' | 'error';
|
||||||
|
+export type VideoPlayerStatus = 'idle' | 'loading' | 'readyToPlay' | 'error' | 'waitingToPlayAtSpecifiedRate';
|
||||||
|
export type VideoSource = string | {
|
||||||
|
/**
|
||||||
|
* The URI of the video.
|
||||||
diff --git a/node_modules/expo-video/build/VideoView.types.d.ts b/node_modules/expo-video/build/VideoView.types.d.ts
|
diff --git a/node_modules/expo-video/build/VideoView.types.d.ts b/node_modules/expo-video/build/VideoView.types.d.ts
|
||||||
index cb9ca6d..ed8bb7e 100644
|
index cb9ca6d..ed8bb7e 100644
|
||||||
--- a/node_modules/expo-video/build/VideoView.types.d.ts
|
--- a/node_modules/expo-video/build/VideoView.types.d.ts
|
||||||
|
@ -270,8 +279,21 @@ index cb9ca6d..ed8bb7e 100644
|
||||||
}
|
}
|
||||||
//# sourceMappingURL=VideoView.types.d.ts.map
|
//# sourceMappingURL=VideoView.types.d.ts.map
|
||||||
\ No newline at end of file
|
\ No newline at end of file
|
||||||
|
diff --git a/node_modules/expo-video/ios/Enums/PlayerStatus.swift b/node_modules/expo-video/ios/Enums/PlayerStatus.swift
|
||||||
|
index 6af69ca..189fbbe 100644
|
||||||
|
--- a/node_modules/expo-video/ios/Enums/PlayerStatus.swift
|
||||||
|
+++ b/node_modules/expo-video/ios/Enums/PlayerStatus.swift
|
||||||
|
@@ -6,5 +6,8 @@ internal enum PlayerStatus: String, Enumerable {
|
||||||
|
case idle
|
||||||
|
case loading
|
||||||
|
case readyToPlay
|
||||||
|
+ case waitingToPlayAtSpecifiedRate
|
||||||
|
+ case unlikeToKeepUp
|
||||||
|
+ case playbackBufferEmpty
|
||||||
|
case error
|
||||||
|
}
|
||||||
diff --git a/node_modules/expo-video/ios/VideoManager.swift b/node_modules/expo-video/ios/VideoManager.swift
|
diff --git a/node_modules/expo-video/ios/VideoManager.swift b/node_modules/expo-video/ios/VideoManager.swift
|
||||||
index 094a8b0..3f00525 100644
|
index 094a8b0..16e7081 100644
|
||||||
--- a/node_modules/expo-video/ios/VideoManager.swift
|
--- a/node_modules/expo-video/ios/VideoManager.swift
|
||||||
+++ b/node_modules/expo-video/ios/VideoManager.swift
|
+++ b/node_modules/expo-video/ios/VideoManager.swift
|
||||||
@@ -12,6 +12,7 @@ class VideoManager {
|
@@ -12,6 +12,7 @@ class VideoManager {
|
||||||
|
@ -409,7 +431,7 @@ index c537a12..e4a918f 100644
|
||||||
+ "onEnterFullscreen",
|
+ "onEnterFullscreen",
|
||||||
+ "onExitFullscreen"
|
+ "onExitFullscreen"
|
||||||
)
|
)
|
||||||
|
|
||||||
Prop("player") { (view, player: VideoPlayer?) in
|
Prop("player") { (view, player: VideoPlayer?) in
|
||||||
diff --git a/node_modules/expo-video/ios/VideoPlayer.swift b/node_modules/expo-video/ios/VideoPlayer.swift
|
diff --git a/node_modules/expo-video/ios/VideoPlayer.swift b/node_modules/expo-video/ios/VideoPlayer.swift
|
||||||
index 3315b88..733ab1f 100644
|
index 3315b88..733ab1f 100644
|
||||||
|
@ -418,7 +440,7 @@ index 3315b88..733ab1f 100644
|
||||||
@@ -185,6 +185,10 @@ internal final class VideoPlayer: SharedRef<AVPlayer>, Hashable, VideoPlayerObse
|
@@ -185,6 +185,10 @@ internal final class VideoPlayer: SharedRef<AVPlayer>, Hashable, VideoPlayerObse
|
||||||
safeEmit(event: "sourceChange", arguments: newVideoPlayerItem?.videoSource, oldVideoPlayerItem?.videoSource)
|
safeEmit(event: "sourceChange", arguments: newVideoPlayerItem?.videoSource, oldVideoPlayerItem?.videoSource)
|
||||||
}
|
}
|
||||||
|
|
||||||
+ func onPlayerTimeRemainingChanged(player: AVPlayer, timeRemaining: Double) {
|
+ func onPlayerTimeRemainingChanged(player: AVPlayer, timeRemaining: Double) {
|
||||||
+ safeEmit(event: "timeRemainingChange", arguments: timeRemaining)
|
+ safeEmit(event: "timeRemainingChange", arguments: timeRemaining)
|
||||||
+ }
|
+ }
|
||||||
|
@ -427,7 +449,7 @@ index 3315b88..733ab1f 100644
|
||||||
if self.appContext != nil {
|
if self.appContext != nil {
|
||||||
self.emit(event: event, arguments: repeat each arguments)
|
self.emit(event: event, arguments: repeat each arguments)
|
||||||
diff --git a/node_modules/expo-video/ios/VideoPlayerObserver.swift b/node_modules/expo-video/ios/VideoPlayerObserver.swift
|
diff --git a/node_modules/expo-video/ios/VideoPlayerObserver.swift b/node_modules/expo-video/ios/VideoPlayerObserver.swift
|
||||||
index d289e26..ea4d96f 100644
|
index d289e26..7de8cbf 100644
|
||||||
--- a/node_modules/expo-video/ios/VideoPlayerObserver.swift
|
--- a/node_modules/expo-video/ios/VideoPlayerObserver.swift
|
||||||
+++ b/node_modules/expo-video/ios/VideoPlayerObserver.swift
|
+++ b/node_modules/expo-video/ios/VideoPlayerObserver.swift
|
||||||
@@ -21,6 +21,7 @@ protocol VideoPlayerObserverDelegate: AnyObject {
|
@@ -21,6 +21,7 @@ protocol VideoPlayerObserverDelegate: AnyObject {
|
||||||
|
@ -436,7 +458,7 @@ index d289e26..ea4d96f 100644
|
||||||
func onPlayerItemStatusChanged(player: AVPlayer, oldStatus: AVPlayerItem.Status?, newStatus: AVPlayerItem.Status)
|
func onPlayerItemStatusChanged(player: AVPlayer, oldStatus: AVPlayerItem.Status?, newStatus: AVPlayerItem.Status)
|
||||||
+ func onPlayerTimeRemainingChanged(player: AVPlayer, timeRemaining: Double)
|
+ func onPlayerTimeRemainingChanged(player: AVPlayer, timeRemaining: Double)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default implementations for the delegate
|
// Default implementations for the delegate
|
||||||
@@ -33,6 +34,7 @@ extension VideoPlayerObserverDelegate {
|
@@ -33,6 +34,7 @@ extension VideoPlayerObserverDelegate {
|
||||||
func onItemChanged(player: AVPlayer, oldVideoPlayerItem: VideoPlayerItem?, newVideoPlayerItem: VideoPlayerItem?) {}
|
func onItemChanged(player: AVPlayer, oldVideoPlayerItem: VideoPlayerItem?, newVideoPlayerItem: VideoPlayerItem?) {}
|
||||||
|
@ -444,14 +466,14 @@ index d289e26..ea4d96f 100644
|
||||||
func onPlayerItemStatusChanged(player: AVPlayer, oldStatus: AVPlayerItem.Status?, newStatus: AVPlayerItem.Status) {}
|
func onPlayerItemStatusChanged(player: AVPlayer, oldStatus: AVPlayerItem.Status?, newStatus: AVPlayerItem.Status) {}
|
||||||
+ func onPlayerTimeRemainingChanged(player: AVPlayer, timeRemaining: Double) {}
|
+ func onPlayerTimeRemainingChanged(player: AVPlayer, timeRemaining: Double) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wrapper used to store WeakReferences to the observer delegate
|
// Wrapper used to store WeakReferences to the observer delegate
|
||||||
@@ -91,6 +93,7 @@ class VideoPlayerObserver {
|
@@ -91,6 +93,7 @@ class VideoPlayerObserver {
|
||||||
private var playerVolumeObserver: NSKeyValueObservation?
|
private var playerVolumeObserver: NSKeyValueObservation?
|
||||||
private var playerCurrentItemObserver: NSKeyValueObservation?
|
private var playerCurrentItemObserver: NSKeyValueObservation?
|
||||||
private var playerIsMutedObserver: NSKeyValueObservation?
|
private var playerIsMutedObserver: NSKeyValueObservation?
|
||||||
+ private var playerPeriodicTimeObserver: Any?
|
+ private var playerPeriodicTimeObserver: Any?
|
||||||
|
|
||||||
// Current player item observers
|
// Current player item observers
|
||||||
private var playbackBufferEmptyObserver: NSKeyValueObservation?
|
private var playbackBufferEmptyObserver: NSKeyValueObservation?
|
||||||
@@ -152,6 +155,9 @@ class VideoPlayerObserver {
|
@@ -152,6 +155,9 @@ class VideoPlayerObserver {
|
||||||
|
@ -462,16 +484,36 @@ index d289e26..ea4d96f 100644
|
||||||
+ player?.removeTimeObserver(playerPeriodicTimeObserver)
|
+ player?.removeTimeObserver(playerPeriodicTimeObserver)
|
||||||
+ }
|
+ }
|
||||||
}
|
}
|
||||||
|
|
||||||
private func initializeCurrentPlayerItemObservers(player: AVPlayer, playerItem: AVPlayerItem) {
|
private func initializeCurrentPlayerItemObservers(player: AVPlayer, playerItem: AVPlayerItem) {
|
||||||
@@ -270,6 +276,7 @@ class VideoPlayerObserver {
|
@@ -265,23 +271,24 @@ class VideoPlayerObserver {
|
||||||
|
if player.timeControlStatus != .waitingToPlayAtSpecifiedRate && player.status == .readyToPlay && currentItem?.isPlaybackBufferEmpty != true {
|
||||||
|
status = .readyToPlay
|
||||||
|
} else if player.timeControlStatus == .waitingToPlayAtSpecifiedRate {
|
||||||
|
- status = .loading
|
||||||
|
+ status = .waitingToPlayAtSpecifiedRate
|
||||||
|
}
|
||||||
|
|
||||||
if isPlaying != (player.timeControlStatus == .playing) {
|
if isPlaying != (player.timeControlStatus == .playing) {
|
||||||
isPlaying = player.timeControlStatus == .playing
|
isPlaying = player.timeControlStatus == .playing
|
||||||
+ addPeriodicTimeObserverIfNeeded()
|
+ addPeriodicTimeObserverIfNeeded()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func onIsBufferEmptyChanged(_ playerItem: AVPlayerItem, _ change: NSKeyValueObservedChange<Bool>) {
|
||||||
|
if playerItem.isPlaybackBufferEmpty {
|
||||||
|
- status = .loading
|
||||||
|
+ status = .playbackBufferEmpty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func onPlayerLikelyToKeepUpChanged(_ playerItem: AVPlayerItem, _ change: NSKeyValueObservedChange<Bool>) {
|
||||||
|
if !playerItem.isPlaybackLikelyToKeepUp && playerItem.isPlaybackBufferEmpty {
|
||||||
|
- status = .loading
|
||||||
|
+ status = .unlikeToKeepUp
|
||||||
|
} else if playerItem.isPlaybackLikelyToKeepUp {
|
||||||
|
status = .readyToPlay
|
||||||
|
}
|
||||||
@@ -310,4 +317,28 @@ class VideoPlayerObserver {
|
@@ -310,4 +317,28 @@ class VideoPlayerObserver {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -506,12 +548,12 @@ index f4579e4..10c5908 100644
|
||||||
--- a/node_modules/expo-video/ios/VideoView.swift
|
--- a/node_modules/expo-video/ios/VideoView.swift
|
||||||
+++ b/node_modules/expo-video/ios/VideoView.swift
|
+++ b/node_modules/expo-video/ios/VideoView.swift
|
||||||
@@ -41,6 +41,8 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
|
@@ -41,6 +41,8 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
|
||||||
|
|
||||||
let onPictureInPictureStart = EventDispatcher()
|
let onPictureInPictureStart = EventDispatcher()
|
||||||
let onPictureInPictureStop = EventDispatcher()
|
let onPictureInPictureStop = EventDispatcher()
|
||||||
+ let onEnterFullscreen = EventDispatcher()
|
+ let onEnterFullscreen = EventDispatcher()
|
||||||
+ let onExitFullscreen = EventDispatcher()
|
+ let onExitFullscreen = EventDispatcher()
|
||||||
|
|
||||||
public override var bounds: CGRect {
|
public override var bounds: CGRect {
|
||||||
didSet {
|
didSet {
|
||||||
@@ -163,6 +165,7 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
|
@@ -163,6 +165,7 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
|
||||||
|
@ -521,7 +563,7 @@ index f4579e4..10c5908 100644
|
||||||
+ onEnterFullscreen()
|
+ onEnterFullscreen()
|
||||||
isFullscreen = true
|
isFullscreen = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,6 +182,7 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
|
@@ -179,6 +182,7 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
|
||||||
if wasPlaying {
|
if wasPlaying {
|
||||||
self.player?.pointer.play()
|
self.player?.pointer.play()
|
||||||
|
@ -531,7 +573,7 @@ index f4579e4..10c5908 100644
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
diff --git a/node_modules/expo-video/src/VideoPlayer.types.ts b/node_modules/expo-video/src/VideoPlayer.types.ts
|
diff --git a/node_modules/expo-video/src/VideoPlayer.types.ts b/node_modules/expo-video/src/VideoPlayer.types.ts
|
||||||
index aaf4b63..f438196 100644
|
index aaf4b63..5ff6b7a 100644
|
||||||
--- a/node_modules/expo-video/src/VideoPlayer.types.ts
|
--- a/node_modules/expo-video/src/VideoPlayer.types.ts
|
||||||
+++ b/node_modules/expo-video/src/VideoPlayer.types.ts
|
+++ b/node_modules/expo-video/src/VideoPlayer.types.ts
|
||||||
@@ -151,6 +151,8 @@ export type VideoPlayerEvents = {
|
@@ -151,6 +151,8 @@ export type VideoPlayerEvents = {
|
||||||
|
@ -541,8 +583,17 @@ index aaf4b63..f438196 100644
|
||||||
+
|
+
|
||||||
+ timeRemainingChange(timeRemaining: number): void;
|
+ timeRemainingChange(timeRemaining: number): void;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -160,7 +162,7 @@ export type VideoPlayerEvents = {
|
||||||
|
* - `readyToPlay`: The player has loaded enough data to start playing or to continue playback.
|
||||||
|
* - `error`: The player has encountered an error while loading or playing the video.
|
||||||
|
*/
|
||||||
|
-export type VideoPlayerStatus = 'idle' | 'loading' | 'readyToPlay' | 'error';
|
||||||
|
+export type VideoPlayerStatus = 'idle' | 'loading' | 'readyToPlay' | 'error' | 'waitingToPlayAtSpecifiedRate';
|
||||||
|
|
||||||
|
export type VideoSource =
|
||||||
|
| string
|
||||||
diff --git a/node_modules/expo-video/src/VideoView.types.ts b/node_modules/expo-video/src/VideoView.types.ts
|
diff --git a/node_modules/expo-video/src/VideoView.types.ts b/node_modules/expo-video/src/VideoView.types.ts
|
||||||
index 29fe5db..e1fbf59 100644
|
index 29fe5db..e1fbf59 100644
|
||||||
--- a/node_modules/expo-video/src/VideoView.types.ts
|
--- a/node_modules/expo-video/src/VideoView.types.ts
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
## `expo-video` Patch
|
## `expo-video` Patch
|
||||||
|
|
||||||
### `onEnterFullScreen`/`onExitFullScreen`
|
### `onEnterFullScreen`/`onExitFullScreen`
|
||||||
|
|
||||||
Adds two props to `VideoView`: `onEnterFullscreen` and `onExitFullscreen` which do exactly what they say on
|
Adds two props to `VideoView`: `onEnterFullscreen` and `onExitFullscreen` which do exactly what they say on
|
||||||
the tin.
|
the tin.
|
||||||
|
|
||||||
|
@ -16,3 +17,15 @@ ourselves.
|
||||||
Instead of handling the pausing/playing of videos in React, we'll handle them here. There's some logic that we do not
|
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
|
need (around PIP mode) that we can remove, and just pause any playing players on background and then resume them on
|
||||||
foreground.
|
foreground.
|
||||||
|
|
||||||
|
### Additional `statusChange` Events
|
||||||
|
|
||||||
|
`expo-video` uses the `loading` status for a variety of cases where the video is not actually "loading". We're making
|
||||||
|
those status events more specific here, so that we can determine if a video is truly loading or not. These statuses are:
|
||||||
|
|
||||||
|
- `waitingToPlayAtSpecifiedRate`
|
||||||
|
- `unlikelyToKeepUp`
|
||||||
|
- `playbackBufferEmpty`
|
||||||
|
|
||||||
|
It's unlikely we will ever need to pay attention to these statuses, so they are not being include in the TypeScript
|
||||||
|
types.
|
||||||
|
|
|
@ -19,6 +19,7 @@ export const sizes = {
|
||||||
md: 20,
|
md: 20,
|
||||||
lg: 24,
|
lg: 24,
|
||||||
xl: 28,
|
xl: 28,
|
||||||
|
'2xl': 32,
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useCommonSVGProps(props: Props) {
|
export function useCommonSVGProps(props: Props) {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React, {useCallback, useId, useState} from 'react'
|
import React, {useCallback, useEffect, useId, useState} from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {Image} from 'expo-image'
|
import {Image} from 'expo-image'
|
||||||
|
import {VideoPlayerStatus} from 'expo-video'
|
||||||
import {AppBskyEmbedVideo} from '@atproto/api'
|
import {AppBskyEmbedVideo} from '@atproto/api'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
@ -10,56 +11,40 @@ import {useGate} from '#/lib/statsig/statsig'
|
||||||
import {VideoEmbedInnerNative} from '#/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative'
|
import {VideoEmbedInnerNative} from '#/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative'
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a} from '#/alf'
|
||||||
import {Button} from '#/components/Button'
|
import {Button} from '#/components/Button'
|
||||||
|
import {Loader} from '#/components/Loader'
|
||||||
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
|
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
|
||||||
import {VisibilityView} from '../../../../../modules/expo-bluesky-swiss-army'
|
import {VisibilityView} from '../../../../../modules/expo-bluesky-swiss-army'
|
||||||
import {ErrorBoundary} from '../ErrorBoundary'
|
import {ErrorBoundary} from '../ErrorBoundary'
|
||||||
import {useActiveVideoNative} from './ActiveVideoNativeContext'
|
import {useActiveVideoNative} from './ActiveVideoNativeContext'
|
||||||
import * as VideoFallback from './VideoEmbedInner/VideoFallback'
|
import * as VideoFallback from './VideoEmbedInner/VideoFallback'
|
||||||
|
|
||||||
export function VideoEmbed({embed}: {embed: AppBskyEmbedVideo.View}) {
|
interface Props {
|
||||||
const {_} = useLingui()
|
embed: AppBskyEmbedVideo.View
|
||||||
const {activeSource, activeViewId, setActiveSource, player} =
|
}
|
||||||
useActiveVideoNative()
|
|
||||||
const viewId = useId()
|
|
||||||
|
|
||||||
const [isFullscreen, setIsFullscreen] = React.useState(false)
|
export function VideoEmbed({embed}: Props) {
|
||||||
const isActive = embed.playlist === activeSource && activeViewId === viewId
|
const gate = useGate()
|
||||||
|
|
||||||
const [key, setKey] = useState(0)
|
const [key, setKey] = useState(0)
|
||||||
|
|
||||||
const renderError = useCallback(
|
const renderError = useCallback(
|
||||||
(error: unknown) => (
|
(error: unknown) => (
|
||||||
<VideoError error={error} retry={() => setKey(key + 1)} />
|
<VideoError error={error} retry={() => setKey(key + 1)} />
|
||||||
),
|
),
|
||||||
[key],
|
[key],
|
||||||
)
|
)
|
||||||
const gate = useGate()
|
|
||||||
|
|
||||||
const onChangeStatus = (isVisible: boolean) => {
|
|
||||||
if (isVisible) {
|
|
||||||
setActiveSource(embed.playlist, viewId)
|
|
||||||
if (!player.playing) {
|
|
||||||
player.play()
|
|
||||||
}
|
|
||||||
} else if (!isFullscreen) {
|
|
||||||
player.muted = true
|
|
||||||
if (player.playing) {
|
|
||||||
player.pause()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!gate('video_view_on_posts')) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
let aspectRatio = 16 / 9
|
let aspectRatio = 16 / 9
|
||||||
|
|
||||||
if (embed.aspectRatio) {
|
if (embed.aspectRatio) {
|
||||||
const {width, height} = embed.aspectRatio
|
const {width, height} = embed.aspectRatio
|
||||||
aspectRatio = width / height
|
aspectRatio = width / height
|
||||||
aspectRatio = clamp(aspectRatio, 1 / 1, 3 / 1)
|
aspectRatio = clamp(aspectRatio, 1 / 1, 3 / 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!gate('video_view_on_posts')) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
|
@ -71,39 +56,138 @@ export function VideoEmbed({embed}: {embed: AppBskyEmbedVideo.View}) {
|
||||||
a.my_xs,
|
a.my_xs,
|
||||||
]}>
|
]}>
|
||||||
<ErrorBoundary renderError={renderError} key={key}>
|
<ErrorBoundary renderError={renderError} key={key}>
|
||||||
<VisibilityView enabled={true} onChangeStatus={onChangeStatus}>
|
<InnerWrapper embed={embed} />
|
||||||
{isActive ? (
|
|
||||||
<VideoEmbedInnerNative
|
|
||||||
embed={embed}
|
|
||||||
isFullscreen={isFullscreen}
|
|
||||||
setIsFullscreen={setIsFullscreen}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Image
|
|
||||||
source={{uri: embed.thumbnail}}
|
|
||||||
alt={embed.alt}
|
|
||||||
style={a.flex_1}
|
|
||||||
contentFit="cover"
|
|
||||||
accessibilityIgnoresInvertColors
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
style={[a.absolute, a.inset_0]}
|
|
||||||
onPress={() => {
|
|
||||||
setActiveSource(embed.playlist, viewId)
|
|
||||||
}}
|
|
||||||
label={_(msg`Play video`)}
|
|
||||||
color="secondary">
|
|
||||||
<PlayButtonIcon />
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</VisibilityView>
|
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function InnerWrapper({embed}: Props) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const {activeSource, activeViewId, setActiveSource, player} =
|
||||||
|
useActiveVideoNative()
|
||||||
|
const viewId = useId()
|
||||||
|
|
||||||
|
const [playerStatus, setPlayerStatus] = useState<VideoPlayerStatus>('loading')
|
||||||
|
const [isMuted, setIsMuted] = useState(player.muted)
|
||||||
|
const [isFullscreen, setIsFullscreen] = React.useState(false)
|
||||||
|
const [timeRemaining, setTimeRemaining] = React.useState(0)
|
||||||
|
const isActive = embed.playlist === activeSource && activeViewId === viewId
|
||||||
|
const isLoading =
|
||||||
|
isActive &&
|
||||||
|
(playerStatus === 'waitingToPlayAtSpecifiedRate' ||
|
||||||
|
playerStatus === 'loading')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isActive) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||||
|
const volumeSub = player.addListener('volumeChange', ({isMuted}) => {
|
||||||
|
setIsMuted(isMuted)
|
||||||
|
})
|
||||||
|
const timeSub = player.addListener(
|
||||||
|
'timeRemainingChange',
|
||||||
|
secondsRemaining => {
|
||||||
|
setTimeRemaining(secondsRemaining)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
const statusSub = player.addListener(
|
||||||
|
'statusChange',
|
||||||
|
(status, _oldStatus, error) => {
|
||||||
|
setPlayerStatus(status)
|
||||||
|
if (status === 'error') {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return () => {
|
||||||
|
volumeSub.remove()
|
||||||
|
timeSub.remove()
|
||||||
|
statusSub.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [player, isActive])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isActive && playerStatus !== 'loading') {
|
||||||
|
setPlayerStatus('loading')
|
||||||
|
}
|
||||||
|
}, [isActive, playerStatus])
|
||||||
|
|
||||||
|
const onChangeStatus = (isVisible: boolean) => {
|
||||||
|
if (isFullscreen) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isVisible) {
|
||||||
|
setActiveSource(embed.playlist, viewId)
|
||||||
|
if (!player.playing) {
|
||||||
|
player.play()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
player.muted = true
|
||||||
|
if (player.playing) {
|
||||||
|
player.pause()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VisibilityView enabled={true} onChangeStatus={onChangeStatus}>
|
||||||
|
{isActive ? (
|
||||||
|
<VideoEmbedInnerNative
|
||||||
|
embed={embed}
|
||||||
|
timeRemaining={timeRemaining}
|
||||||
|
isMuted={isMuted}
|
||||||
|
isFullscreen={isFullscreen}
|
||||||
|
setIsFullscreen={setIsFullscreen}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{!isActive || isLoading ? (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<Image
|
||||||
|
source={{uri: embed.thumbnail}}
|
||||||
|
alt={embed.alt}
|
||||||
|
style={a.flex_1}
|
||||||
|
contentFit="cover"
|
||||||
|
accessibilityIgnoresInvertColors
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
style={[a.absolute, a.inset_0]}
|
||||||
|
onPress={() => {
|
||||||
|
setActiveSource(embed.playlist, viewId)
|
||||||
|
}}
|
||||||
|
label={_(msg`Play video`)}
|
||||||
|
color="secondary">
|
||||||
|
{isLoading ? (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.rounded_full,
|
||||||
|
a.p_xs,
|
||||||
|
a.absolute,
|
||||||
|
{top: 'auto', left: 'auto'},
|
||||||
|
{backgroundColor: 'rgba(0,0,0,0.5)'},
|
||||||
|
]}>
|
||||||
|
<Loader size="2xl" style={{color: 'white'}} />
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<PlayButtonIcon />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
</VisibilityView>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function VideoError({retry}: {error: unknown; retry: () => void}) {
|
function VideoError({retry}: {error: unknown; retry: () => void}) {
|
||||||
return (
|
return (
|
||||||
<VideoFallback.Container>
|
<VideoFallback.Container>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, {useCallback, useEffect, useRef, useState} from 'react'
|
import React, {useCallback, useRef} from 'react'
|
||||||
import {Pressable, View} from 'react-native'
|
import {Pressable, View} from 'react-native'
|
||||||
import Animated, {FadeInDown} from 'react-native-reanimated'
|
import Animated, {FadeInDown} from 'react-native-reanimated'
|
||||||
import {VideoPlayer, VideoView} from 'expo-video'
|
import {VideoPlayer, VideoView} from 'expo-video'
|
||||||
|
@ -22,10 +22,14 @@ export function VideoEmbedInnerNative({
|
||||||
embed,
|
embed,
|
||||||
isFullscreen,
|
isFullscreen,
|
||||||
setIsFullscreen,
|
setIsFullscreen,
|
||||||
|
isMuted,
|
||||||
|
timeRemaining,
|
||||||
}: {
|
}: {
|
||||||
embed: AppBskyEmbedVideo.View
|
embed: AppBskyEmbedVideo.View
|
||||||
isFullscreen: boolean
|
isFullscreen: boolean
|
||||||
setIsFullscreen: (isFullscreen: boolean) => void
|
setIsFullscreen: (isFullscreen: boolean) => void
|
||||||
|
timeRemaining: number
|
||||||
|
isMuted: boolean
|
||||||
}) {
|
}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {player} = useActiveVideoNative()
|
const {player} = useActiveVideoNative()
|
||||||
|
@ -73,7 +77,12 @@ export function VideoEmbedInnerNative({
|
||||||
}
|
}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
/>
|
/>
|
||||||
<VideoControls player={player} enterFullscreen={enterFullscreen} />
|
<VideoControls
|
||||||
|
player={player}
|
||||||
|
enterFullscreen={enterFullscreen}
|
||||||
|
isMuted={isMuted}
|
||||||
|
timeRemaining={timeRemaining}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -81,40 +90,16 @@ export function VideoEmbedInnerNative({
|
||||||
function VideoControls({
|
function VideoControls({
|
||||||
player,
|
player,
|
||||||
enterFullscreen,
|
enterFullscreen,
|
||||||
|
timeRemaining,
|
||||||
|
isMuted,
|
||||||
}: {
|
}: {
|
||||||
player: VideoPlayer
|
player: VideoPlayer
|
||||||
enterFullscreen: () => void
|
enterFullscreen: () => void
|
||||||
|
timeRemaining: number
|
||||||
|
isMuted: boolean
|
||||||
}) {
|
}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const [isMuted, setIsMuted] = useState(player.muted)
|
|
||||||
const [timeRemaining, setTimeRemaining] = React.useState(0)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
|
||||||
const volumeSub = player.addListener('volumeChange', ({isMuted}) => {
|
|
||||||
setIsMuted(isMuted)
|
|
||||||
})
|
|
||||||
const timeSub = player.addListener(
|
|
||||||
'timeRemainingChange',
|
|
||||||
secondsRemaining => {
|
|
||||||
setTimeRemaining(secondsRemaining)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
const statusSub = player.addListener(
|
|
||||||
'statusChange',
|
|
||||||
(status, _oldStatus, error) => {
|
|
||||||
if (status === 'error') {
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
return () => {
|
|
||||||
volumeSub.remove()
|
|
||||||
timeSub.remove()
|
|
||||||
statusSub.remove()
|
|
||||||
}
|
|
||||||
}, [player])
|
|
||||||
|
|
||||||
const onPressFullscreen = useCallback(() => {
|
const onPressFullscreen = useCallback(() => {
|
||||||
switch (player.status) {
|
switch (player.status) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue