Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,9 @@ const VideoDemo = () => {

const player = useVideoPlayer(
getVideoSource(defaultSettings.videoType),
(_player) => {}
(_player) => {
player.seekTo(1);
}
);

useEvent(player, 'onEnd', handlePlayerEnd);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down Expand Up @@ -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()
}
Expand All @@ -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
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ object PictureInPictureUtils {
}

return builder
.setAspectRatio(calculateAspectRatio(videoView.playerView))
.setAspectRatio(calculateAspectRatio(videoView))
.setSourceRectHint(calculateSourceRectHint(videoView.playerView))
.build()
}
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -277,8 +281,6 @@ class HybridVideoPlayer() : HybridVideoPlayerSpec() {
if (source.config.initializeOnCreation == true) {
initializePlayer()
player.prepare()
} else {
player = ExoPlayer.Builder(context).build()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down
28 changes: 27 additions & 1 deletion packages/react-native-video/src/core/hooks/useVideoPlayer.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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
Expand All @@ -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,
},
Expand Down
Loading