Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

[Video] Add loading state to player (#5149)

authored by

Hailey and committed by
GitHub
25566984 76f493c2

+250 -116
+81 -30
patches/expo-video+1.2.4.patch
··· 5 5 @@ -41,6 +41,11 @@ sealed class PlayerEvent { 6 6 override val name = "playToEnd" 7 7 } 8 - 8 + 9 9 + data class PlayerTimeRemainingChanged(val timeRemaining: Double): PlayerEvent() { 10 10 + override val name = "timeRemainingChange" 11 11 + override val arguments = arrayOf(timeRemaining) ··· 32 32 setTimeBarInteractive(requireLinearPlayback) 33 33 + setShowSubtitleButton(true) 34 34 } 35 - 35 + 36 36 @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) 37 37 @@ -27,7 +28,8 @@ internal fun PlayerView.setTimeBarInteractive(interactive: Boolean) { 38 - 38 + 39 39 @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) 40 40 internal fun PlayerView.setFullscreenButtonVisibility(visible: Boolean) { 41 41 - val fullscreenButton = findViewById<android.widget.ImageButton>(androidx.media3.ui.R.id.exo_fullscreen) ··· 144 144 + "onEnterFullscreen", 145 145 + "onExitFullscreen" 146 146 ) 147 - 147 + 148 148 Prop("player") { view: VideoView, player: VideoPlayer -> 149 149 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 150 150 index 58f00af..5ad8237 100644 ··· 152 152 +++ b/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoPlayer.kt 153 153 @@ -1,5 +1,6 @@ 154 154 package expo.modules.video 155 - 155 + 156 156 +import ProgressTracker 157 157 import android.content.Context 158 158 import android.view.SurfaceView ··· 162 162 .setLooper(context.mainLooper) 163 163 .build() 164 164 + var progressTracker: ProgressTracker? = null 165 - 165 + 166 166 val serviceConnection = PlaybackServiceConnection(WeakReference(player)) 167 - 167 + 168 168 var playing by IgnoreSameSet(false) { new, old -> 169 169 sendEvent(PlayerEvent.IsPlayingChanged(new, old)) 170 170 + addOrRemoveProgressTracker() 171 171 } 172 - 172 + 173 173 var uncommittedSource: VideoSource? = source 174 174 @@ -141,6 +144,9 @@ class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSou 175 175 } 176 - 176 + 177 177 override fun close() { 178 178 + this.progressTracker?.remove() 179 179 + this.progressTracker = null ··· 184 184 @@ -228,7 +234,7 @@ class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSou 185 185 listeners.removeAll { it.get() == videoPlayerListener } 186 186 } 187 - 187 + 188 188 - private fun sendEvent(event: PlayerEvent) { 189 189 + fun sendEvent(event: PlayerEvent) { 190 190 // Emits to the native listeners ··· 224 224 val onPictureInPictureStop by EventDispatcher<Unit>() 225 225 + val onEnterFullscreen by EventDispatcher() 226 226 + val onExitFullscreen by EventDispatcher() 227 - 227 + 228 228 var willEnterPiP: Boolean = false 229 229 var isInFullscreen: Boolean = false 230 230 @@ -154,6 +156,7 @@ class VideoView(context: Context, appContext: AppContext) : ExpoView(context, ap ··· 234 234 + onEnterFullscreen(mapOf()) 235 235 isInFullscreen = true 236 236 } 237 - 237 + 238 238 @@ -162,6 +165,7 @@ class VideoView(context: Context, appContext: AppContext) : ExpoView(context, ap 239 239 val fullScreenButton: ImageButton = playerView.findViewById(androidx.media3.ui.R.id.exo_fullscreen) 240 240 fullScreenButton.setImageResource(androidx.media3.ui.R.drawable.exo_icon_fullscreen_enter) ··· 242 242 + this.onExitFullscreen(mapOf()) 243 243 isInFullscreen = false 244 244 } 245 - 245 + 246 246 diff --git a/node_modules/expo-video/build/VideoPlayer.types.d.ts b/node_modules/expo-video/build/VideoPlayer.types.d.ts 247 - index a09fcfe..5eac9e5 100644 247 + index a09fcfe..46cbae7 100644 248 248 --- a/node_modules/expo-video/build/VideoPlayer.types.d.ts 249 249 +++ b/node_modules/expo-video/build/VideoPlayer.types.d.ts 250 250 @@ -128,6 +128,8 @@ export type VideoPlayerEvents = { ··· 256 256 }; 257 257 /** 258 258 * Describes the current status of the player. 259 + @@ -136,7 +138,7 @@ export type VideoPlayerEvents = { 260 + * - `readyToPlay`: The player has loaded enough data to start playing or to continue playback. 261 + * - `error`: The player has encountered an error while loading or playing the video. 262 + */ 263 + -export type VideoPlayerStatus = 'idle' | 'loading' | 'readyToPlay' | 'error'; 264 + +export type VideoPlayerStatus = 'idle' | 'loading' | 'readyToPlay' | 'error' | 'waitingToPlayAtSpecifiedRate'; 265 + export type VideoSource = string | { 266 + /** 267 + * The URI of the video. 259 268 diff --git a/node_modules/expo-video/build/VideoView.types.d.ts b/node_modules/expo-video/build/VideoView.types.d.ts 260 269 index cb9ca6d..ed8bb7e 100644 261 270 --- a/node_modules/expo-video/build/VideoView.types.d.ts ··· 270 279 } 271 280 //# sourceMappingURL=VideoView.types.d.ts.map 272 281 \ No newline at end of file 282 + diff --git a/node_modules/expo-video/ios/Enums/PlayerStatus.swift b/node_modules/expo-video/ios/Enums/PlayerStatus.swift 283 + index 6af69ca..189fbbe 100644 284 + --- a/node_modules/expo-video/ios/Enums/PlayerStatus.swift 285 + +++ b/node_modules/expo-video/ios/Enums/PlayerStatus.swift 286 + @@ -6,5 +6,8 @@ internal enum PlayerStatus: String, Enumerable { 287 + case idle 288 + case loading 289 + case readyToPlay 290 + + case waitingToPlayAtSpecifiedRate 291 + + case unlikeToKeepUp 292 + + case playbackBufferEmpty 293 + case error 294 + } 273 295 diff --git a/node_modules/expo-video/ios/VideoManager.swift b/node_modules/expo-video/ios/VideoManager.swift 274 - index 094a8b0..3f00525 100644 296 + index 094a8b0..16e7081 100644 275 297 --- a/node_modules/expo-video/ios/VideoManager.swift 276 298 +++ b/node_modules/expo-video/ios/VideoManager.swift 277 299 @@ -12,6 +12,7 @@ class VideoManager { ··· 409 431 + "onEnterFullscreen", 410 432 + "onExitFullscreen" 411 433 ) 412 - 434 + 413 435 Prop("player") { (view, player: VideoPlayer?) in 414 436 diff --git a/node_modules/expo-video/ios/VideoPlayer.swift b/node_modules/expo-video/ios/VideoPlayer.swift 415 437 index 3315b88..733ab1f 100644 ··· 418 440 @@ -185,6 +185,10 @@ internal final class VideoPlayer: SharedRef<AVPlayer>, Hashable, VideoPlayerObse 419 441 safeEmit(event: "sourceChange", arguments: newVideoPlayerItem?.videoSource, oldVideoPlayerItem?.videoSource) 420 442 } 421 - 443 + 422 444 + func onPlayerTimeRemainingChanged(player: AVPlayer, timeRemaining: Double) { 423 445 + safeEmit(event: "timeRemainingChange", arguments: timeRemaining) 424 446 + } ··· 427 449 if self.appContext != nil { 428 450 self.emit(event: event, arguments: repeat each arguments) 429 451 diff --git a/node_modules/expo-video/ios/VideoPlayerObserver.swift b/node_modules/expo-video/ios/VideoPlayerObserver.swift 430 - index d289e26..ea4d96f 100644 452 + index d289e26..7de8cbf 100644 431 453 --- a/node_modules/expo-video/ios/VideoPlayerObserver.swift 432 454 +++ b/node_modules/expo-video/ios/VideoPlayerObserver.swift 433 455 @@ -21,6 +21,7 @@ protocol VideoPlayerObserverDelegate: AnyObject { ··· 436 458 func onPlayerItemStatusChanged(player: AVPlayer, oldStatus: AVPlayerItem.Status?, newStatus: AVPlayerItem.Status) 437 459 + func onPlayerTimeRemainingChanged(player: AVPlayer, timeRemaining: Double) 438 460 } 439 - 461 + 440 462 // Default implementations for the delegate 441 463 @@ -33,6 +34,7 @@ extension VideoPlayerObserverDelegate { 442 464 func onItemChanged(player: AVPlayer, oldVideoPlayerItem: VideoPlayerItem?, newVideoPlayerItem: VideoPlayerItem?) {} ··· 444 466 func onPlayerItemStatusChanged(player: AVPlayer, oldStatus: AVPlayerItem.Status?, newStatus: AVPlayerItem.Status) {} 445 467 + func onPlayerTimeRemainingChanged(player: AVPlayer, timeRemaining: Double) {} 446 468 } 447 - 469 + 448 470 // Wrapper used to store WeakReferences to the observer delegate 449 471 @@ -91,6 +93,7 @@ class VideoPlayerObserver { 450 472 private var playerVolumeObserver: NSKeyValueObservation? 451 473 private var playerCurrentItemObserver: NSKeyValueObservation? 452 474 private var playerIsMutedObserver: NSKeyValueObservation? 453 475 + private var playerPeriodicTimeObserver: Any? 454 - 476 + 455 477 // Current player item observers 456 478 private var playbackBufferEmptyObserver: NSKeyValueObservation? 457 479 @@ -152,6 +155,9 @@ class VideoPlayerObserver { ··· 462 484 + player?.removeTimeObserver(playerPeriodicTimeObserver) 463 485 + } 464 486 } 465 - 487 + 466 488 private func initializeCurrentPlayerItemObservers(player: AVPlayer, playerItem: AVPlayerItem) { 467 - @@ -270,6 +276,7 @@ class VideoPlayerObserver { 468 - 489 + @@ -265,23 +271,24 @@ class VideoPlayerObserver { 490 + if player.timeControlStatus != .waitingToPlayAtSpecifiedRate && player.status == .readyToPlay && currentItem?.isPlaybackBufferEmpty != true { 491 + status = .readyToPlay 492 + } else if player.timeControlStatus == .waitingToPlayAtSpecifiedRate { 493 + - status = .loading 494 + + status = .waitingToPlayAtSpecifiedRate 495 + } 496 + 469 497 if isPlaying != (player.timeControlStatus == .playing) { 470 498 isPlaying = player.timeControlStatus == .playing 471 499 + addPeriodicTimeObserverIfNeeded() 472 500 } 473 501 } 474 - 502 + 503 + private func onIsBufferEmptyChanged(_ playerItem: AVPlayerItem, _ change: NSKeyValueObservedChange<Bool>) { 504 + if playerItem.isPlaybackBufferEmpty { 505 + - status = .loading 506 + + status = .playbackBufferEmpty 507 + } 508 + } 509 + 510 + private func onPlayerLikelyToKeepUpChanged(_ playerItem: AVPlayerItem, _ change: NSKeyValueObservedChange<Bool>) { 511 + if !playerItem.isPlaybackLikelyToKeepUp && playerItem.isPlaybackBufferEmpty { 512 + - status = .loading 513 + + status = .unlikeToKeepUp 514 + } else if playerItem.isPlaybackLikelyToKeepUp { 515 + status = .readyToPlay 516 + } 475 517 @@ -310,4 +317,28 @@ class VideoPlayerObserver { 476 518 } 477 519 } ··· 506 548 --- a/node_modules/expo-video/ios/VideoView.swift 507 549 +++ b/node_modules/expo-video/ios/VideoView.swift 508 550 @@ -41,6 +41,8 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate { 509 - 551 + 510 552 let onPictureInPictureStart = EventDispatcher() 511 553 let onPictureInPictureStop = EventDispatcher() 512 554 + let onEnterFullscreen = EventDispatcher() 513 555 + let onExitFullscreen = EventDispatcher() 514 - 556 + 515 557 public override var bounds: CGRect { 516 558 didSet { 517 559 @@ -163,6 +165,7 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate { ··· 521 563 + onEnterFullscreen() 522 564 isFullscreen = true 523 565 } 524 - 566 + 525 567 @@ -179,6 +182,7 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate { 526 568 if wasPlaying { 527 569 self.player?.pointer.play() ··· 531 573 } 532 574 } 533 575 diff --git a/node_modules/expo-video/src/VideoPlayer.types.ts b/node_modules/expo-video/src/VideoPlayer.types.ts 534 - index aaf4b63..f438196 100644 576 + index aaf4b63..5ff6b7a 100644 535 577 --- a/node_modules/expo-video/src/VideoPlayer.types.ts 536 578 +++ b/node_modules/expo-video/src/VideoPlayer.types.ts 537 579 @@ -151,6 +151,8 @@ export type VideoPlayerEvents = { ··· 541 583 + 542 584 + timeRemainingChange(timeRemaining: number): void; 543 585 }; 544 - 586 + 545 587 /** 588 + @@ -160,7 +162,7 @@ export type VideoPlayerEvents = { 589 + * - `readyToPlay`: The player has loaded enough data to start playing or to continue playback. 590 + * - `error`: The player has encountered an error while loading or playing the video. 591 + */ 592 + -export type VideoPlayerStatus = 'idle' | 'loading' | 'readyToPlay' | 'error'; 593 + +export type VideoPlayerStatus = 'idle' | 'loading' | 'readyToPlay' | 'error' | 'waitingToPlayAtSpecifiedRate'; 594 + 595 + export type VideoSource = 596 + | string 546 597 diff --git a/node_modules/expo-video/src/VideoView.types.ts b/node_modules/expo-video/src/VideoView.types.ts 547 598 index 29fe5db..e1fbf59 100644 548 599 --- a/node_modules/expo-video/src/VideoView.types.ts
+13
patches/expo-video+1.2.4.patch.md
··· 3 3 ## `expo-video` Patch 4 4 5 5 ### `onEnterFullScreen`/`onExitFullScreen` 6 + 6 7 Adds two props to `VideoView`: `onEnterFullscreen` and `onExitFullscreen` which do exactly what they say on 7 8 the tin. 8 9 ··· 16 17 Instead of handling the pausing/playing of videos in React, we'll handle them here. There's some logic that we do not 17 18 need (around PIP mode) that we can remove, and just pause any playing players on background and then resume them on 18 19 foreground. 20 + 21 + ### Additional `statusChange` Events 22 + 23 + `expo-video` uses the `loading` status for a variety of cases where the video is not actually "loading". We're making 24 + those status events more specific here, so that we can determine if a video is truly loading or not. These statuses are: 25 + 26 + - `waitingToPlayAtSpecifiedRate` 27 + - `unlikelyToKeepUp` 28 + - `playbackBufferEmpty` 29 + 30 + It's unlikely we will ever need to pay attention to these statuses, so they are not being include in the TypeScript 31 + types.
+1
src/components/icons/common.tsx
··· 19 19 md: 20, 20 20 lg: 24, 21 21 xl: 28, 22 + '2xl': 32, 22 23 } 23 24 24 25 export function useCommonSVGProps(props: Props) {
+140 -56
src/view/com/util/post-embeds/VideoEmbed.tsx
··· 1 - import React, {useCallback, useId, useState} from 'react' 1 + import React, {useCallback, useEffect, useId, useState} from 'react' 2 2 import {View} from 'react-native' 3 3 import {Image} from 'expo-image' 4 + import {VideoPlayerStatus} from 'expo-video' 4 5 import {AppBskyEmbedVideo} from '@atproto/api' 5 6 import {msg, Trans} from '@lingui/macro' 6 7 import {useLingui} from '@lingui/react' ··· 10 11 import {VideoEmbedInnerNative} from '#/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative' 11 12 import {atoms as a} from '#/alf' 12 13 import {Button} from '#/components/Button' 14 + import {Loader} from '#/components/Loader' 13 15 import {PlayButtonIcon} from '#/components/video/PlayButtonIcon' 14 16 import {VisibilityView} from '../../../../../modules/expo-bluesky-swiss-army' 15 17 import {ErrorBoundary} from '../ErrorBoundary' 16 18 import {useActiveVideoNative} from './ActiveVideoNativeContext' 17 19 import * as VideoFallback from './VideoEmbedInner/VideoFallback' 18 20 19 - export function VideoEmbed({embed}: {embed: AppBskyEmbedVideo.View}) { 20 - const {_} = useLingui() 21 - const {activeSource, activeViewId, setActiveSource, player} = 22 - useActiveVideoNative() 23 - const viewId = useId() 21 + interface Props { 22 + embed: AppBskyEmbedVideo.View 23 + } 24 24 25 - const [isFullscreen, setIsFullscreen] = React.useState(false) 26 - const isActive = embed.playlist === activeSource && activeViewId === viewId 25 + export function VideoEmbed({embed}: Props) { 26 + const gate = useGate() 27 27 28 28 const [key, setKey] = useState(0) 29 + 29 30 const renderError = useCallback( 30 31 (error: unknown) => ( 31 32 <VideoError error={error} retry={() => setKey(key + 1)} /> 32 33 ), 33 34 [key], 34 35 ) 35 - const gate = useGate() 36 - 37 - const onChangeStatus = (isVisible: boolean) => { 38 - if (isVisible) { 39 - setActiveSource(embed.playlist, viewId) 40 - if (!player.playing) { 41 - player.play() 42 - } 43 - } else if (!isFullscreen) { 44 - player.muted = true 45 - if (player.playing) { 46 - player.pause() 47 - } 48 - } 49 - } 50 - 51 - if (!gate('video_view_on_posts')) { 52 - return null 53 - } 54 36 55 37 let aspectRatio = 16 / 9 56 - 57 38 if (embed.aspectRatio) { 58 39 const {width, height} = embed.aspectRatio 59 40 aspectRatio = width / height 60 41 aspectRatio = clamp(aspectRatio, 1 / 1, 3 / 1) 61 42 } 62 43 44 + if (!gate('video_view_on_posts')) { 45 + return null 46 + } 47 + 63 48 return ( 64 49 <View 65 50 style={[ ··· 71 56 a.my_xs, 72 57 ]}> 73 58 <ErrorBoundary renderError={renderError} key={key}> 74 - <VisibilityView enabled={true} onChangeStatus={onChangeStatus}> 75 - {isActive ? ( 76 - <VideoEmbedInnerNative 77 - embed={embed} 78 - isFullscreen={isFullscreen} 79 - setIsFullscreen={setIsFullscreen} 80 - /> 81 - ) : ( 82 - <> 83 - <Image 84 - source={{uri: embed.thumbnail}} 85 - alt={embed.alt} 86 - style={a.flex_1} 87 - contentFit="cover" 88 - accessibilityIgnoresInvertColors 89 - /> 90 - <Button 91 - style={[a.absolute, a.inset_0]} 92 - onPress={() => { 93 - setActiveSource(embed.playlist, viewId) 94 - }} 95 - label={_(msg`Play video`)} 96 - color="secondary"> 97 - <PlayButtonIcon /> 98 - </Button> 99 - </> 100 - )} 101 - </VisibilityView> 59 + <InnerWrapper embed={embed} /> 102 60 </ErrorBoundary> 103 61 </View> 62 + ) 63 + } 64 + 65 + function InnerWrapper({embed}: Props) { 66 + const {_} = useLingui() 67 + const {activeSource, activeViewId, setActiveSource, player} = 68 + useActiveVideoNative() 69 + const viewId = useId() 70 + 71 + const [playerStatus, setPlayerStatus] = useState<VideoPlayerStatus>('loading') 72 + const [isMuted, setIsMuted] = useState(player.muted) 73 + const [isFullscreen, setIsFullscreen] = React.useState(false) 74 + const [timeRemaining, setTimeRemaining] = React.useState(0) 75 + const isActive = embed.playlist === activeSource && activeViewId === viewId 76 + const isLoading = 77 + isActive && 78 + (playerStatus === 'waitingToPlayAtSpecifiedRate' || 79 + playerStatus === 'loading') 80 + 81 + useEffect(() => { 82 + if (isActive) { 83 + // eslint-disable-next-line @typescript-eslint/no-shadow 84 + const volumeSub = player.addListener('volumeChange', ({isMuted}) => { 85 + setIsMuted(isMuted) 86 + }) 87 + const timeSub = player.addListener( 88 + 'timeRemainingChange', 89 + secondsRemaining => { 90 + setTimeRemaining(secondsRemaining) 91 + }, 92 + ) 93 + const statusSub = player.addListener( 94 + 'statusChange', 95 + (status, _oldStatus, error) => { 96 + setPlayerStatus(status) 97 + if (status === 'error') { 98 + throw error 99 + } 100 + }, 101 + ) 102 + return () => { 103 + volumeSub.remove() 104 + timeSub.remove() 105 + statusSub.remove() 106 + } 107 + } 108 + }, [player, isActive]) 109 + 110 + useEffect(() => { 111 + if (!isActive && playerStatus !== 'loading') { 112 + setPlayerStatus('loading') 113 + } 114 + }, [isActive, playerStatus]) 115 + 116 + const onChangeStatus = (isVisible: boolean) => { 117 + if (isFullscreen) { 118 + return 119 + } 120 + 121 + if (isVisible) { 122 + setActiveSource(embed.playlist, viewId) 123 + if (!player.playing) { 124 + player.play() 125 + } 126 + } else { 127 + player.muted = true 128 + if (player.playing) { 129 + player.pause() 130 + } 131 + } 132 + } 133 + 134 + return ( 135 + <VisibilityView enabled={true} onChangeStatus={onChangeStatus}> 136 + {isActive ? ( 137 + <VideoEmbedInnerNative 138 + embed={embed} 139 + timeRemaining={timeRemaining} 140 + isMuted={isMuted} 141 + isFullscreen={isFullscreen} 142 + setIsFullscreen={setIsFullscreen} 143 + /> 144 + ) : null} 145 + {!isActive || isLoading ? ( 146 + <View 147 + style={[ 148 + { 149 + position: 'absolute', 150 + top: 0, 151 + bottom: 0, 152 + left: 0, 153 + right: 0, 154 + }, 155 + ]}> 156 + <Image 157 + source={{uri: embed.thumbnail}} 158 + alt={embed.alt} 159 + style={a.flex_1} 160 + contentFit="cover" 161 + accessibilityIgnoresInvertColors 162 + /> 163 + <Button 164 + style={[a.absolute, a.inset_0]} 165 + onPress={() => { 166 + setActiveSource(embed.playlist, viewId) 167 + }} 168 + label={_(msg`Play video`)} 169 + color="secondary"> 170 + {isLoading ? ( 171 + <View 172 + style={[ 173 + a.rounded_full, 174 + a.p_xs, 175 + a.absolute, 176 + {top: 'auto', left: 'auto'}, 177 + {backgroundColor: 'rgba(0,0,0,0.5)'}, 178 + ]}> 179 + <Loader size="2xl" style={{color: 'white'}} /> 180 + </View> 181 + ) : ( 182 + <PlayButtonIcon /> 183 + )} 184 + </Button> 185 + </View> 186 + ) : null} 187 + </VisibilityView> 104 188 ) 105 189 } 106 190
+15 -30
src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx
··· 1 - import React, {useCallback, useEffect, useRef, useState} from 'react' 1 + import React, {useCallback, useRef} from 'react' 2 2 import {Pressable, View} from 'react-native' 3 3 import Animated, {FadeInDown} from 'react-native-reanimated' 4 4 import {VideoPlayer, VideoView} from 'expo-video' ··· 22 22 embed, 23 23 isFullscreen, 24 24 setIsFullscreen, 25 + isMuted, 26 + timeRemaining, 25 27 }: { 26 28 embed: AppBskyEmbedVideo.View 27 29 isFullscreen: boolean 28 30 setIsFullscreen: (isFullscreen: boolean) => void 31 + timeRemaining: number 32 + isMuted: boolean 29 33 }) { 30 34 const {_} = useLingui() 31 35 const {player} = useActiveVideoNative() ··· 73 77 } 74 78 accessibilityHint="" 75 79 /> 76 - <VideoControls player={player} enterFullscreen={enterFullscreen} /> 80 + <VideoControls 81 + player={player} 82 + enterFullscreen={enterFullscreen} 83 + isMuted={isMuted} 84 + timeRemaining={timeRemaining} 85 + /> 77 86 </View> 78 87 ) 79 88 } ··· 81 90 function VideoControls({ 82 91 player, 83 92 enterFullscreen, 93 + timeRemaining, 94 + isMuted, 84 95 }: { 85 96 player: VideoPlayer 86 97 enterFullscreen: () => void 98 + timeRemaining: number 99 + isMuted: boolean 87 100 }) { 88 101 const {_} = useLingui() 89 102 const t = useTheme() 90 - const [isMuted, setIsMuted] = useState(player.muted) 91 - const [timeRemaining, setTimeRemaining] = React.useState(0) 92 - 93 - useEffect(() => { 94 - // eslint-disable-next-line @typescript-eslint/no-shadow 95 - const volumeSub = player.addListener('volumeChange', ({isMuted}) => { 96 - setIsMuted(isMuted) 97 - }) 98 - const timeSub = player.addListener( 99 - 'timeRemainingChange', 100 - secondsRemaining => { 101 - setTimeRemaining(secondsRemaining) 102 - }, 103 - ) 104 - const statusSub = player.addListener( 105 - 'statusChange', 106 - (status, _oldStatus, error) => { 107 - if (status === 'error') { 108 - throw error 109 - } 110 - }, 111 - ) 112 - return () => { 113 - volumeSub.remove() 114 - timeSub.remove() 115 - statusSub.remove() 116 - } 117 - }, [player]) 118 103 119 104 const onPressFullscreen = useCallback(() => { 120 105 switch (player.status) {