fix: player initialization bugs (#4775)

This commit is contained in:
Krzysztof Moch
2025-11-16 18:27:59 +01:00
committed by GitHub
parent 8ee914f1da
commit 2e88f36a5a
9 changed files with 99 additions and 17 deletions
+3 -1
View File
@@ -121,7 +121,9 @@ const VideoDemo = () => {
const player = useVideoPlayer(
getVideoSource(defaultSettings.videoType),
(_player) => {}
(_player) => {
player.seekTo(1);
}
);
useEvent(player, 'onEnd', handlePlayerEnd);
@@ -88,7 +88,7 @@ class AudioFocusManager() {
private fun determineRequiredMixMode(): MixAudioMode? {
val activePlayers = players.filter { player ->
player.player?.isPlaying == true && player.player?.volume != 0f
player.player.isPlaying && player.player.volume != 0f
}
if (activePlayers.isEmpty()) {
@@ -195,7 +195,7 @@ class AudioFocusManager() {
private fun pauseActivePlayers() {
Threading.runOnMainThread {
players.forEach { player ->
player.player?.let { mediaPlayer ->
player.player.let { mediaPlayer ->
if (mediaPlayer.volume != 0f && mediaPlayer.isPlaying) {
mediaPlayer.pause()
}
@@ -207,7 +207,7 @@ class AudioFocusManager() {
private fun duckActivePlayers() {
Threading.runOnMainThread {
players.forEach { player ->
player.player?.let { mediaPlayer ->
player.player.let { mediaPlayer ->
// We need to duck the volume to 50%. After the audio focus is regained,
// we will restore the volume to the user's volume.
mediaPlayer.volume = mediaPlayer.volume * 0.5f
@@ -220,7 +220,7 @@ class AudioFocusManager() {
Threading.runOnMainThread {
// Resume players that were paused due to audio focus loss
players.forEach { player ->
player.player?.let { mediaPlayer ->
player.player.let { mediaPlayer ->
// Restore full volume if it was ducked
if (mediaPlayer.volume != 0f && mediaPlayer.volume.toDouble() != player.userVolume) {
mediaPlayer.volume = player.userVolume.toFloat()
@@ -83,18 +83,29 @@ class FullscreenVideoFragment(private val videoView: VideoView) : Fragment() {
}
}
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode)
if (isInPictureInPictureMode) {
videoView.playerView.useController = false
videoView.playerView.controllerAutoShow = false
} else {
videoView.playerView.useController = videoView.useController
videoView.playerView.controllerAutoShow = true
}
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
// Handle PiP mode changes
val isInPictureInPictureMode =
requireActivity().isInPictureInPictureMode
val isInPictureInPictureMode = requireActivity().isInPictureInPictureMode
if (isInPictureInPictureMode) {
// Disable controls in PiP mode - media session creates its own controls for PiP
videoView.playerView.useController = false
videoView.playerView.controllerAutoShow = false
} else {
videoView.playerView.useController = videoView.useController
videoView.playerView.controllerAutoShow = true
}
}
@@ -54,6 +54,7 @@ class PictureInPictureHelperFragment(private val videoView: VideoView) : Fragmen
if (currentPipVideo == videoView) {
// Disable controls immediately when entering PiP - media session creates its own controls for PiP
videoView.playerView.useController = false
videoView.playerView.controllerAutoShow = false
// If we're currently in fullscreen, exit it first to prevent parent conflicts
if (videoView.isInFullscreen) {
@@ -32,7 +32,7 @@ object PictureInPictureUtils {
}
return builder
.setAspectRatio(calculateAspectRatio(videoView.playerView))
.setAspectRatio(calculateAspectRatio(videoView))
.setSourceRectHint(calculateSourceRectHint(videoView.playerView))
.build()
}
@@ -50,14 +50,38 @@ object PictureInPictureUtils {
return defaultParams.build()
}
fun calculateAspectRatio(view: View): Rational {
fun calculateAspectRatio(videoView: VideoView): Rational {
// AspectRatio for PIP must be between 2.39:1 and 1:2.39
// see: https://developer.android.com/reference/android/app/PictureInPictureParams.Builder#setAspectRatio(android.util.Rational)
val maximumAspectRatio = Rational(239, 100)
val minimumAspectRatio = Rational(100, 239)
val currentAspectRatio = Rational(view.width, view.height)
val player = videoView.hybridPlayer?.player
val videoFormat = player?.videoFormat
var width: Int
var height: Int
if (videoFormat != null && videoFormat.width > 0 && videoFormat.height > 0) {
width = videoFormat.width
height = videoFormat.height
val rotationDegrees = videoFormat.rotationDegrees
if (rotationDegrees == 90 || rotationDegrees == 270) {
val temp = width
width = height
height = temp
}
Log.d(TAG, "Using video format dimensions for PiP: ${width}x${height} (rotation: ${rotationDegrees}°)")
} else {
width = videoView.playerView.width
height = videoView.playerView.height
Log.d(TAG, "Using view dimensions for PiP: ${width}x${height}")
}
val currentAspectRatio = Rational(width, height)
return when {
currentAspectRatio > maximumAspectRatio -> maximumAspectRatio
@@ -60,7 +60,11 @@ class HybridVideoPlayer() : HybridVideoPlayerSpec() {
throw LibraryError.ApplicationContextNotFound
}
lateinit var player: ExoPlayer
var player: ExoPlayer = runOnMainThreadSync {
// Build Temporary player that will be replaced when source is loaded
return@runOnMainThreadSync ExoPlayer.Builder(context).build()
}
var loadedWithSource = false
private var currentPlayerView: WeakReference<PlayerView>? = null
@@ -277,8 +281,6 @@ class HybridVideoPlayer() : HybridVideoPlayerSpec() {
if (source.config.initializeOnCreation == true) {
initializePlayer()
player.prepare()
} else {
player = ExoPlayer.Builder(context).build()
}
}
@@ -51,7 +51,9 @@ class HybridVideoViewViewManager(nitroId: Int): HybridVideoViewViewManagerSpec()
override var autoEnterPictureInPicture: Boolean
get() = videoView.get()?.autoEnterPictureInPicture == true
set(value) {
videoView.get()?.autoEnterPictureInPicture = value
Threading.runOnMainThread {
videoView.get()?.autoEnterPictureInPicture = value
}
}
override var pictureInPicture: Boolean
@@ -143,6 +143,17 @@ class VideoView @JvmOverloads constructor(
var isInPictureInPicture: Boolean = false
set(value) {
field = value
if (value) {
playerView.useController = false
playerView.controllerAutoShow = false
playerView.controllerHideOnTouch = true
} else {
playerView.useController = useController
playerView.controllerAutoShow = true
playerView.controllerHideOnTouch = true
}
events.onPictureInPictureChange?.let { it(value) }
}
private var rootContentViews: List<View> = listOf()
@@ -436,6 +447,7 @@ class VideoView @JvmOverloads constructor(
// Disable controls before entering PiP - media session creates its own controls for PiP
runOnMainThread {
playerView.useController = false
playerView.controllerAutoShow = false
}
val success = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -475,6 +487,7 @@ class VideoView @JvmOverloads constructor(
// Restore controls when exiting PiP - they were disabled because media session handles PiP controls
playerView.useController = useController
playerView.controllerAutoShow = true
if (movedToRootForPiP) {
restoreRootContentViews()
@@ -503,6 +516,7 @@ class VideoView @JvmOverloads constructor(
// Restore controls when exiting PiP - they were disabled because media session handles PiP controls
playerView.useController = useController
playerView.controllerAutoShow = true
if (movedToRootForPiP) {
restoreRootContentViews()
@@ -1,3 +1,4 @@
import { useRef } from 'react';
import type { VideoPlayerSource } from '../../spec/nitro/VideoPlayerSource.nitro';
import type { NoAutocomplete } from '../types/Utils';
import type { VideoConfig, VideoSource } from '../types/VideoConfig';
@@ -19,6 +20,9 @@ const sourceEqual = <T extends VideoConfig | VideoSource | VideoPlayerSource>(
/**
* Creates a `VideoPlayer` instance and manages its lifecycle.
*
* if `initializeOnCreation` is true (default), the `setup` function will be called when the player is started loading source.
* 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.
*
* @param source - The source of the video to play
* @param setup - A function to setup the player
* @returns The `VideoPlayer` instance
@@ -27,15 +31,37 @@ export const useVideoPlayer = (
source: VideoConfig | VideoSource | NoAutocomplete<VideoPlayerSource>,
setup?: (player: VideoPlayer) => void
) => {
const setupCalled = useRef(false);
return useManagedInstance(
{
factory: () => {
const player = new VideoPlayer(source);
setup?.(player);
if (player.source.config.initializeOnCreation) {
// if source is small video, it can happen that onLoadStart is called before we set event from JS
// Thats why we adding event listener and calling setup once if player is loading or ready to play
// That way we ensure that setup is always called
const callSetupOnce = () => {
if (!setupCalled.current) {
setupCalled.current = true;
console.log('calling setup');
setup?.(player);
}
};
player.addEventListener('onLoadStart', callSetupOnce);
player.addEventListener('onStatusChange', callSetupOnce);
} else {
setup?.(player);
}
return player;
},
cleanup: (player) => {
player.__destroy();
setupCalled.current = false;
},
dependenciesEqualFn: sourceEqual,
},