diff --git a/app/(auth)/player/direct-player.tsx b/app/(auth)/player/direct-player.tsx index 8f722302..64c089c7 100644 --- a/app/(auth)/player/direct-player.tsx +++ b/app/(auth)/player/direct-player.tsx @@ -327,29 +327,6 @@ export default function page() { : 0; }, [item]); - const [appState, setAppState] = useState(AppState.currentState); - - useEffect(() => { - const handleAppStateChange = (nextAppState: AppStateStatus) => { - // Handle app going to the background - if (nextAppState.match(/inactive|background/)) { - _setShowControls(false); - } - setAppState(nextAppState); - }; - - // Use AppState.addEventListener and return a cleanup function - const subscription = AppState.addEventListener( - "change", - handleAppStateChange - ); - - return () => { - // Cleanup the event listener when the component is unmounted - subscription.remove(); - }; - }, [appState, isPipStarted, isPlaying]); - // Preselection of audio and subtitle tracks. if (!settings) return null; let initOptions = [`--sub-text-scale=${settings.subtitleSize}`]; @@ -456,7 +433,7 @@ export default function page() { }} /> - {videoRef.current && ( + {videoRef.current && !isPipStarted && ( = mutableListOf() +// override fun onCreate(activity: Activity?, savedInstanceState: Bundle?) { +// listeners.forEach { +// it.onCreate(activity, savedInstanceState) +// } +// } +// +// override fun onResume(activity: Activity?) { +// listeners.forEach { +// it.onResume(activity) +// } +// } +// +// override fun onPause(activity: Activity?) { +// listeners.forEach { +// it.onPause(activity) +// } +// } +// +// override fun onUserLeaveHint(activity: Activity?) { +// listeners.forEach { +// it.onUserLeaveHint(activity) +// } +// } +// +// override fun onDestroy(activity: Activity?) { +// listeners.forEach { +// it.onDestroy(activity) +// } +// } +} \ No newline at end of file diff --git a/modules/vlc-player/android/src/main/java/expo/modules/vlcplayer/VlcPlayerModule.kt b/modules/vlc-player/android/src/main/java/expo/modules/vlcplayer/VlcPlayerModule.kt index 54b40399..91d48fc5 100644 --- a/modules/vlc-player/android/src/main/java/expo/modules/vlcplayer/VlcPlayerModule.kt +++ b/modules/vlc-player/android/src/main/java/expo/modules/vlcplayer/VlcPlayerModule.kt @@ -1,5 +1,6 @@ package expo.modules.vlcplayer +import androidx.core.os.bundleOf import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition @@ -7,6 +8,18 @@ class VlcPlayerModule : Module() { override fun definition() = ModuleDefinition { Name("VlcPlayer") + OnActivityEntersForeground { + VLCManager.listeners.forEach { + it.onResume(appContext.currentActivity) + } + } + + OnActivityEntersBackground { + VLCManager.listeners.forEach { + it.onPause(appContext.currentActivity) + } + } + View(VlcPlayerView::class) { Prop("source") { view: VlcPlayerView, source: Map -> view.setSource(source) diff --git a/modules/vlc-player/android/src/main/java/expo/modules/vlcplayer/VlcPlayerView.kt b/modules/vlc-player/android/src/main/java/expo/modules/vlcplayer/VlcPlayerView.kt index 9c40f809..97d1d7aa 100644 --- a/modules/vlc-player/android/src/main/java/expo/modules/vlcplayer/VlcPlayerView.kt +++ b/modules/vlc-player/android/src/main/java/expo/modules/vlcplayer/VlcPlayerView.kt @@ -1,6 +1,7 @@ package expo.modules.vlcplayer import android.R +import android.app.Activity import android.app.PendingIntent import android.app.PendingIntent.FLAG_IMMUTABLE import android.app.PendingIntent.FLAG_UPDATE_CURRENT @@ -14,13 +15,20 @@ import android.content.IntentFilter import android.graphics.drawable.Icon import android.net.Uri import android.os.Build +import android.os.Bundle import android.os.Handler import android.os.Looper import android.util.Log +import android.view.View import androidx.annotation.RequiresApi -import androidx.core.app.ComponentActivity -import androidx.core.content.ContextCompat +import androidx.core.app.PictureInPictureModeChangedInfo +import androidx.core.view.isVisible +import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleObserver +import androidx.lifecycle.OnLifecycleEvent +import expo.modules.core.interfaces.ReactActivityLifecycleListener +import expo.modules.core.logging.LogHandlers +import expo.modules.core.logging.Logger import expo.modules.kotlin.AppContext import expo.modules.kotlin.viewevent.EventDispatcher import expo.modules.kotlin.views.ExpoView @@ -31,7 +39,8 @@ import org.videolan.libvlc.interfaces.IMedia import org.videolan.libvlc.util.VLCVideoLayout -class VlcPlayerView(context: Context, appContext: AppContext) : ExpoView(context, appContext), LifecycleObserver, MediaPlayer.EventListener { +class VlcPlayerView(context: Context, appContext: AppContext) : ExpoView(context, appContext), LifecycleObserver, MediaPlayer.EventListener, ReactActivityLifecycleListener { + private val log = Logger(listOf(LogHandlers.createOSLogHandler(this::class.simpleName!!))) private val PIP_PLAY_PAUSE_ACTION = "PIP_PLAY_PAUSE_ACTION" private val PIP_REWIND_ACTION = "PIP_REWIND_ACTION" private val PIP_FORWARD_ACTION = "PIP_FORWARD_ACTION" @@ -43,6 +52,7 @@ class VlcPlayerView(context: Context, appContext: AppContext) : ExpoView(context private var lastReportedState: Int? = null private var lastReportedIsPlaying: Boolean? = null private var media : Media? = null + private var timeLeft: Long? = null private val onVideoProgress by EventDispatcher() private val onVideoStateChange by EventDispatcher() @@ -64,53 +74,87 @@ class VlcPlayerView(context: Context, appContext: AppContext) : ExpoView(context } private val currentActivity get() = context.findActivity() private val actions: MutableList = mutableListOf() - - private val actionReceiver: BroadcastReceiver = object : BroadcastReceiver() { + private val remoteActionFilter = IntentFilter() + private val playPauseIntent: Intent = Intent(PIP_PLAY_PAUSE_ACTION).setPackage(context.packageName) + private val forwardIntent: Intent = Intent(PIP_FORWARD_ACTION).setPackage(context.packageName) + private val rewindIntent: Intent = Intent(PIP_REWIND_ACTION).setPackage(context.packageName) + private var actionReceiver: BroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { when (intent?.action) { - PIP_PLAY_PAUSE_ACTION -> if (isPaused) play() else pause() + PIP_PLAY_PAUSE_ACTION -> { + if (isPaused) play() else pause() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + setupPipActions() + currentActivity.setPictureInPictureParams(getPipParams()!!) + } + } PIP_FORWARD_ACTION -> seekTo((mediaPlayer?.time?.toInt() ?: 0) + 15_000) PIP_REWIND_ACTION -> seekTo((mediaPlayer?.time?.toInt() ?: 0) - 15_000) } } } - init { - setupView() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - setupPipActions() - currentActivity.apply { - setPictureInPictureParams(getPipParams()!!) - addOnPictureInPictureModeChangedListener { info -> - onPipStarted(mapOf( - "pipStarted" to info.isInPictureInPictureMode - )) - } + private var pipChangeListener: (PictureInPictureModeChangedInfo) -> Unit = { info -> + if (!info.isInPictureInPictureMode && mediaPlayer?.isPlaying == true) { + log.debug("Exiting PiP") + timeLeft = mediaPlayer?.time + pause() + + // Setting the media after reattaching the view allows for a fast video view render + if (mediaPlayer?.vlcVout?.areViewsAttached() == false) { + mediaPlayer?.attachViews(videoLayout, null, false, false) + mediaPlayer?.media = media + mediaPlayer?.play() + timeLeft?.let { mediaPlayer?.time = it } + mediaPlayer?.pause() + } } + onPipStarted(mapOf( + "pipStarted" to info.isInPictureInPictureMode + )) + } + + init { + VLCManager.listeners.add(this) + setupView() + setupPiP() } private fun setupView() { - Log.d("VlcPlayerView", "Setting up view") + log.debug("Setting up view") setBackgroundColor(android.graphics.Color.WHITE) videoLayout = VLCVideoLayout(context).apply { layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT) } + videoLayout.keepScreenOn = true addView(videoLayout) - Log.d("VlcPlayerView", "View setup complete") + log.debug("View setup complete") + } + + private fun setupPiP() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + remoteActionFilter.addAction(PIP_PLAY_PAUSE_ACTION) + remoteActionFilter.addAction(PIP_FORWARD_ACTION) + remoteActionFilter.addAction(PIP_REWIND_ACTION) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + currentActivity.registerReceiver( + actionReceiver, + remoteActionFilter, + Context.RECEIVER_NOT_EXPORTED + ) + } + setupPipActions() + currentActivity.apply { + setPictureInPictureParams(getPipParams()!!) + addOnPictureInPictureModeChangedListener(pipChangeListener) + } + } } @RequiresApi(Build.VERSION_CODES.O) private fun setupPipActions() { - val remoteActionFilter = IntentFilter() - val playPauseIntent: Intent = Intent(PIP_PLAY_PAUSE_ACTION).setPackage(context.packageName) - val forwardIntent: Intent = Intent(PIP_FORWARD_ACTION).setPackage(context.packageName) - val rewindIntent: Intent = Intent(PIP_REWIND_ACTION).setPackage(context.packageName) - - remoteActionFilter.addAction(PIP_PLAY_PAUSE_ACTION) - remoteActionFilter.addAction(PIP_FORWARD_ACTION) - remoteActionFilter.addAction(PIP_REWIND_ACTION) - + actions.clear() actions.addAll( listOf( RemoteAction( @@ -125,12 +169,13 @@ class VlcPlayerView(context: Context, appContext: AppContext) : ExpoView(context ) ), RemoteAction( - Icon.createWithResource(context, R.drawable.ic_media_play), + if (isPaused) Icon.createWithResource(context, R.drawable.ic_media_play) + else Icon.createWithResource(context, R.drawable.ic_media_pause), "Play", "Play Video", PendingIntent.getBroadcast( context, - 0, + if (isPaused) 0 else 1, playPauseIntent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE ) @@ -148,13 +193,6 @@ class VlcPlayerView(context: Context, appContext: AppContext) : ExpoView(context ) ) ) - - ContextCompat.registerReceiver( - context, - actionReceiver, - remoteActionFilter, - ContextCompat.RECEIVER_NOT_EXPORTED - ) } private fun getPipParams(): PictureInPictureParams? { @@ -171,7 +209,9 @@ class VlcPlayerView(context: Context, appContext: AppContext) : ExpoView(context } fun setSource(source: Map) { + log.debug("setting source $source") if (hasSource) { + log.debug("Source already set. Resuming") mediaPlayer?.attachViews(videoLayout, null, false, false) play() return @@ -196,12 +236,12 @@ class VlcPlayerView(context: Context, appContext: AppContext) : ExpoView(context mediaPlayer?.attachViews(videoLayout, null, false, false) mediaPlayer?.setEventListener(this) - Log.d("VlcPlayerView", "Loading network file: $uri") + log.debug("Loading network file: $uri") media = Media(libVLC, Uri.parse(uri)) mediaPlayer?.media = media - Log.d("VlcPlayerView", "Debug: Media options: $mediaOptions") + log.debug("Debug: Media options: $mediaOptions") // media.addOptions(mediaOptions) // Apply subtitle options @@ -218,7 +258,7 @@ class VlcPlayerView(context: Context, appContext: AppContext) : ExpoView(context hasSource = true if (autoplay) { - Log.d("VlcPlayerView", "Playing...") + log.debug("Playing...") play() } } @@ -268,9 +308,7 @@ class VlcPlayerView(context: Context, appContext: AppContext) : ExpoView(context } fun getAudioTracks(): List>? { - - println("getAudioTracks") - println(mediaPlayer?.getAudioTracks()) + log.debug("getAudioTracks ${mediaPlayer?.audioTracks}") val trackDescriptions = mediaPlayer?.audioTracks ?: return null return trackDescriptions.map { trackDescription -> @@ -294,19 +332,32 @@ class VlcPlayerView(context: Context, appContext: AppContext) : ExpoView(context } // Debug statement to print the result - Log.d("VlcPlayerView", "Subtitle Tracks: $subtitleTracks") + log.debug("Subtitle Tracks: $subtitleTracks") return subtitleTracks } fun setSubtitleURL(subtitleURL: String, name: String) { - println("Setting subtitle URL: $subtitleURL, name: $name") + log.debug("Setting subtitle URL: $subtitleURL, name: $name") mediaPlayer?.addSlave(IMedia.Slave.Type.Subtitle, Uri.parse(subtitleURL), true) } override fun onDetachedFromWindow() { - println("onDetachedFromWindow") + log.debug("onDetachedFromWindow") super.onDetachedFromWindow() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + currentActivity.setPictureInPictureParams( + PictureInPictureParams.Builder() + .setAutoEnterEnabled(false) + .build() + ) + } + + currentActivity.unregisterReceiver(actionReceiver) + currentActivity.removeOnPictureInPictureModeChangedListener(pipChangeListener) + VLCManager.listeners.clear() + mediaPlayer?.stop() handler.removeCallbacks(updateProgressRunnable) // Stop updating progress @@ -319,6 +370,7 @@ class VlcPlayerView(context: Context, appContext: AppContext) : ExpoView(context } override fun onEvent(event: MediaPlayer.Event) { + keepScreenOn = event.type == MediaPlayer.Event.Playing || event.type == MediaPlayer.Event.Buffering when (event.type) { MediaPlayer.Event.Playing, MediaPlayer.Event.Paused, @@ -340,35 +392,27 @@ class VlcPlayerView(context: Context, appContext: AppContext) : ExpoView(context "target" to "null", // Replace with actual target if needed "currentTime" to player.time.toInt(), "duration" to (player.media?.duration?.toInt() ?: 0), - "error" to false + "error" to false, + "isPlaying" to (currentState == MediaPlayer.Event.Playing), + "isBuffering" to (!player.isPlaying && currentState == MediaPlayer.Event.Buffering) ) + // Todo: make enum - string to prevent this when statement from becoming exhaustive when (currentState) { - MediaPlayer.Event.Playing -> { - stateInfo["isPlaying"] = true - stateInfo["isBuffering"] = false + MediaPlayer.Event.Playing -> stateInfo["state"] = "Playing" - } - MediaPlayer.Event.Paused -> { - stateInfo["isPlaying"] = false + MediaPlayer.Event.Paused -> stateInfo["state"] = "Paused" - } - MediaPlayer.Event.Buffering -> { - stateInfo["isBuffering"] = true + MediaPlayer.Event.Buffering -> stateInfo["state"] = "Buffering" - } MediaPlayer.Event.EncounteredError -> { - Log.e("VlcPlayerView", "player.state ~ error") stateInfo["state"] = "Error" onVideoLoadEnd(stateInfo); } - MediaPlayer.Event.Opening -> { - Log.d("VlcPlayerView", "player.state ~ opening") + MediaPlayer.Event.Opening -> stateInfo["state"] = "Opening" - } } - if (lastReportedState != currentState || lastReportedIsPlaying != player.isPlaying) { lastReportedState = currentState lastReportedIsPlaying = player.isPlaying @@ -400,6 +444,16 @@ class VlcPlayerView(context: Context, appContext: AppContext) : ExpoView(context )); } } + + override fun onPause(activity: Activity?) { + log.debug("Pausing activity...") + } + + + override fun onResume(activity: Activity?) { + log.debug("Resuming activity...") + if (isPaused) play() + } } internal fun Context.findActivity(): androidx.activity.ComponentActivity {