Skip to content

Commit 2e88f36

Browse files
fix: player initialization bugs (#4775)
1 parent 8ee914f commit 2e88f36

File tree

9 files changed

+99
-17
lines changed

9 files changed

+99
-17
lines changed

example/src/App.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,9 @@ const VideoDemo = () => {
121121

122122
const player = useVideoPlayer(
123123
getVideoSource(defaultSettings.videoType),
124-
(_player) => {}
124+
(_player) => {
125+
player.seekTo(1);
126+
}
125127
);
126128

127129
useEvent(player, 'onEnd', handlePlayerEnd);

packages/react-native-video/android/src/main/java/com/twg/video/core/AudioFocusManager.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ class AudioFocusManager() {
8888

8989
private fun determineRequiredMixMode(): MixAudioMode? {
9090
val activePlayers = players.filter { player ->
91-
player.player?.isPlaying == true && player.player?.volume != 0f
91+
player.player.isPlaying && player.player.volume != 0f
9292
}
9393

9494
if (activePlayers.isEmpty()) {
@@ -195,7 +195,7 @@ class AudioFocusManager() {
195195
private fun pauseActivePlayers() {
196196
Threading.runOnMainThread {
197197
players.forEach { player ->
198-
player.player?.let { mediaPlayer ->
198+
player.player.let { mediaPlayer ->
199199
if (mediaPlayer.volume != 0f && mediaPlayer.isPlaying) {
200200
mediaPlayer.pause()
201201
}
@@ -207,7 +207,7 @@ class AudioFocusManager() {
207207
private fun duckActivePlayers() {
208208
Threading.runOnMainThread {
209209
players.forEach { player ->
210-
player.player?.let { mediaPlayer ->
210+
player.player.let { mediaPlayer ->
211211
// We need to duck the volume to 50%. After the audio focus is regained,
212212
// we will restore the volume to the user's volume.
213213
mediaPlayer.volume = mediaPlayer.volume * 0.5f
@@ -220,7 +220,7 @@ class AudioFocusManager() {
220220
Threading.runOnMainThread {
221221
// Resume players that were paused due to audio focus loss
222222
players.forEach { player ->
223-
player.player?.let { mediaPlayer ->
223+
player.player.let { mediaPlayer ->
224224
// Restore full volume if it was ducked
225225
if (mediaPlayer.volume != 0f && mediaPlayer.volume.toDouble() != player.userVolume) {
226226
mediaPlayer.volume = player.userVolume.toFloat()

packages/react-native-video/android/src/main/java/com/twg/video/core/fragments/FullscreenVideoFragment.kt

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,18 +83,29 @@ class FullscreenVideoFragment(private val videoView: VideoView) : Fragment() {
8383
}
8484
}
8585

86+
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
87+
super.onPictureInPictureModeChanged(isInPictureInPictureMode)
88+
89+
if (isInPictureInPictureMode) {
90+
videoView.playerView.useController = false
91+
videoView.playerView.controllerAutoShow = false
92+
} else {
93+
videoView.playerView.useController = videoView.useController
94+
videoView.playerView.controllerAutoShow = true
95+
}
96+
}
97+
8698
override fun onConfigurationChanged(newConfig: Configuration) {
8799
super.onConfigurationChanged(newConfig)
88100

89-
// Handle PiP mode changes
90-
val isInPictureInPictureMode =
91-
requireActivity().isInPictureInPictureMode
101+
val isInPictureInPictureMode = requireActivity().isInPictureInPictureMode
92102

93103
if (isInPictureInPictureMode) {
94-
// Disable controls in PiP mode - media session creates its own controls for PiP
95104
videoView.playerView.useController = false
105+
videoView.playerView.controllerAutoShow = false
96106
} else {
97107
videoView.playerView.useController = videoView.useController
108+
videoView.playerView.controllerAutoShow = true
98109
}
99110
}
100111

packages/react-native-video/android/src/main/java/com/twg/video/core/fragments/PictureInPictureHelperFragment.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ class PictureInPictureHelperFragment(private val videoView: VideoView) : Fragmen
5454
if (currentPipVideo == videoView) {
5555
// Disable controls immediately when entering PiP - media session creates its own controls for PiP
5656
videoView.playerView.useController = false
57+
videoView.playerView.controllerAutoShow = false
5758

5859
// If we're currently in fullscreen, exit it first to prevent parent conflicts
5960
if (videoView.isInFullscreen) {

packages/react-native-video/android/src/main/java/com/twg/video/core/utils/PictureInPictureUtils.kt

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ object PictureInPictureUtils {
3232
}
3333

3434
return builder
35-
.setAspectRatio(calculateAspectRatio(videoView.playerView))
35+
.setAspectRatio(calculateAspectRatio(videoView))
3636
.setSourceRectHint(calculateSourceRectHint(videoView.playerView))
3737
.build()
3838
}
@@ -50,14 +50,38 @@ object PictureInPictureUtils {
5050
return defaultParams.build()
5151
}
5252

53-
fun calculateAspectRatio(view: View): Rational {
53+
fun calculateAspectRatio(videoView: VideoView): Rational {
5454
// AspectRatio for PIP must be between 2.39:1 and 1:2.39
5555
// see: https://developer.android.com/reference/android/app/PictureInPictureParams.Builder#setAspectRatio(android.util.Rational)
5656

5757
val maximumAspectRatio = Rational(239, 100)
5858
val minimumAspectRatio = Rational(100, 239)
5959

60-
val currentAspectRatio = Rational(view.width, view.height)
60+
val player = videoView.hybridPlayer?.player
61+
val videoFormat = player?.videoFormat
62+
63+
var width: Int
64+
var height: Int
65+
66+
if (videoFormat != null && videoFormat.width > 0 && videoFormat.height > 0) {
67+
width = videoFormat.width
68+
height = videoFormat.height
69+
70+
val rotationDegrees = videoFormat.rotationDegrees
71+
if (rotationDegrees == 90 || rotationDegrees == 270) {
72+
val temp = width
73+
width = height
74+
height = temp
75+
}
76+
77+
Log.d(TAG, "Using video format dimensions for PiP: ${width}x${height} (rotation: ${rotationDegrees}°)")
78+
} else {
79+
width = videoView.playerView.width
80+
height = videoView.playerView.height
81+
Log.d(TAG, "Using view dimensions for PiP: ${width}x${height}")
82+
}
83+
84+
val currentAspectRatio = Rational(width, height)
6185

6286
return when {
6387
currentAspectRatio > maximumAspectRatio -> maximumAspectRatio

packages/react-native-video/android/src/main/java/com/twg/video/hybrids/videoplayer/HybridVideoPlayer.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,11 @@ class HybridVideoPlayer() : HybridVideoPlayerSpec() {
6060
throw LibraryError.ApplicationContextNotFound
6161
}
6262

63-
lateinit var player: ExoPlayer
63+
var player: ExoPlayer = runOnMainThreadSync {
64+
// Build Temporary player that will be replaced when source is loaded
65+
return@runOnMainThreadSync ExoPlayer.Builder(context).build()
66+
}
67+
6468
var loadedWithSource = false
6569
private var currentPlayerView: WeakReference<PlayerView>? = null
6670

@@ -277,8 +281,6 @@ class HybridVideoPlayer() : HybridVideoPlayerSpec() {
277281
if (source.config.initializeOnCreation == true) {
278282
initializePlayer()
279283
player.prepare()
280-
} else {
281-
player = ExoPlayer.Builder(context).build()
282284
}
283285
}
284286

packages/react-native-video/android/src/main/java/com/twg/video/hybrids/videoviewviewmanager/HybridVideoViewViewManager.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,9 @@ class HybridVideoViewViewManager(nitroId: Int): HybridVideoViewViewManagerSpec()
5151
override var autoEnterPictureInPicture: Boolean
5252
get() = videoView.get()?.autoEnterPictureInPicture == true
5353
set(value) {
54-
videoView.get()?.autoEnterPictureInPicture = value
54+
Threading.runOnMainThread {
55+
videoView.get()?.autoEnterPictureInPicture = value
56+
}
5557
}
5658

5759
override var pictureInPicture: Boolean

packages/react-native-video/android/src/main/java/com/twg/video/view/VideoView.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,17 @@ class VideoView @JvmOverloads constructor(
143143
var isInPictureInPicture: Boolean = false
144144
set(value) {
145145
field = value
146+
147+
if (value) {
148+
playerView.useController = false
149+
playerView.controllerAutoShow = false
150+
playerView.controllerHideOnTouch = true
151+
} else {
152+
playerView.useController = useController
153+
playerView.controllerAutoShow = true
154+
playerView.controllerHideOnTouch = true
155+
}
156+
146157
events.onPictureInPictureChange?.let { it(value) }
147158
}
148159
private var rootContentViews: List<View> = listOf()
@@ -436,6 +447,7 @@ class VideoView @JvmOverloads constructor(
436447
// Disable controls before entering PiP - media session creates its own controls for PiP
437448
runOnMainThread {
438449
playerView.useController = false
450+
playerView.controllerAutoShow = false
439451
}
440452

441453
val success = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -475,6 +487,7 @@ class VideoView @JvmOverloads constructor(
475487

476488
// Restore controls when exiting PiP - they were disabled because media session handles PiP controls
477489
playerView.useController = useController
490+
playerView.controllerAutoShow = true
478491

479492
if (movedToRootForPiP) {
480493
restoreRootContentViews()
@@ -503,6 +516,7 @@ class VideoView @JvmOverloads constructor(
503516

504517
// Restore controls when exiting PiP - they were disabled because media session handles PiP controls
505518
playerView.useController = useController
519+
playerView.controllerAutoShow = true
506520

507521
if (movedToRootForPiP) {
508522
restoreRootContentViews()

packages/react-native-video/src/core/hooks/useVideoPlayer.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useRef } from 'react';
12
import type { VideoPlayerSource } from '../../spec/nitro/VideoPlayerSource.nitro';
23
import type { NoAutocomplete } from '../types/Utils';
34
import type { VideoConfig, VideoSource } from '../types/VideoConfig';
@@ -19,6 +20,9 @@ const sourceEqual = <T extends VideoConfig | VideoSource | VideoPlayerSource>(
1920
/**
2021
* Creates a `VideoPlayer` instance and manages its lifecycle.
2122
*
23+
* if `initializeOnCreation` is true (default), the `setup` function will be called when the player is started loading source.
24+
* if `initializeOnCreation` is false, the `setup` function will be called when the player is created. changes made to player made before initializing will be overwritten when initializing.
25+
*
2226
* @param source - The source of the video to play
2327
* @param setup - A function to setup the player
2428
* @returns The `VideoPlayer` instance
@@ -27,15 +31,37 @@ export const useVideoPlayer = (
2731
source: VideoConfig | VideoSource | NoAutocomplete<VideoPlayerSource>,
2832
setup?: (player: VideoPlayer) => void
2933
) => {
34+
const setupCalled = useRef(false);
35+
3036
return useManagedInstance(
3137
{
3238
factory: () => {
3339
const player = new VideoPlayer(source);
34-
setup?.(player);
40+
41+
if (player.source.config.initializeOnCreation) {
42+
// if source is small video, it can happen that onLoadStart is called before we set event from JS
43+
// Thats why we adding event listener and calling setup once if player is loading or ready to play
44+
// That way we ensure that setup is always called
45+
46+
const callSetupOnce = () => {
47+
if (!setupCalled.current) {
48+
setupCalled.current = true;
49+
console.log('calling setup');
50+
setup?.(player);
51+
}
52+
};
53+
54+
player.addEventListener('onLoadStart', callSetupOnce);
55+
player.addEventListener('onStatusChange', callSetupOnce);
56+
} else {
57+
setup?.(player);
58+
}
59+
3560
return player;
3661
},
3762
cleanup: (player) => {
3863
player.__destroy();
64+
setupCalled.current = false;
3965
},
4066
dependenciesEqualFn: sourceEqual,
4167
},

0 commit comments

Comments
 (0)