mirror of
https://github.com/zoriya/react-native-video.git
synced 2026-08-16 02:56:22 +00:00
fix: player initialization bugs (#4775)
This commit is contained in:
+3
-1
@@ -121,7 +121,9 @@ const VideoDemo = () => {
|
||||
|
||||
const player = useVideoPlayer(
|
||||
getVideoSource(defaultSettings.videoType),
|
||||
(_player) => {}
|
||||
(_player) => {
|
||||
player.seekTo(1);
|
||||
}
|
||||
);
|
||||
|
||||
useEvent(player, 'onEnd', handlePlayerEnd);
|
||||
|
||||
+4
-4
@@ -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()
|
||||
|
||||
+15
-4
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -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) {
|
||||
|
||||
+27
-3
@@ -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
|
||||
|
||||
+5
-3
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -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,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user