Implement pip

This commit is contained in:
2026-04-24 13:17:13 +02:00
parent 7d40e42085
commit d888fbfab2
3 changed files with 130 additions and 7 deletions
@@ -268,12 +268,7 @@ class OmniPlayerService : MediaSessionService() {
lateinit var player: Player
lateinit var mediaSession: MediaSession
init {
Log.e("omni", "service inited")
}
override fun onCreate() {
Log.e("omni", "service created")
super.onCreate()
player = OmniPlayer.notificationPlayer ?: throw Error("No player available")
val launchIntent = packageManager.getLaunchIntentForPackage(packageName)?.apply {
@@ -1,15 +1,42 @@
package dev.zoriya.omni
import android.app.PictureInPictureParams
import android.os.Build
import android.util.Log
import android.util.Rational
import android.view.SurfaceHolder
import android.view.SurfaceView
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.core.view.isVisible
import com.facebook.react.bridge.LifecycleEventListener
import com.facebook.react.uimanager.ThemedReactContext
import com.margelo.nitro.omni.HybridOmniPlayerSpec
import com.margelo.nitro.omni.HybridOmniViewSpec
class OmniView(val context: ThemedReactContext) : HybridOmniViewSpec(), SurfaceHolder.Callback {
override val view = FrameLayout(context)
class OmniView(val context: ThemedReactContext) :
HybridOmniViewSpec(),
SurfaceHolder.Callback,
LifecycleEventListener {
override val view = object : FrameLayout(context) {
// React Native manages layout via Yoga and swallows requestLayout() calls from native
// views. When children are added/removed (e.g. during PIP transitions), addView triggers
// requestLayout() but it never results in a measure+layout pass. Override to force one.
// see: https://github.com/facebook/react-native/blob/d19afc73f5048f81656d0b4424232ce6d69a6368/ReactAndroid/src/main/java/com/facebook/react/views/toolbar/ReactToolbar.java#L166
private val layoutRunnable = Runnable {
measure(
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
)
layout(left, top, right, bottom)
}
override fun requestLayout() {
super.requestLayout()
post(layoutRunnable)
}
}
private val surfaceView = SurfaceView(context).apply {
layoutParams = FrameLayout.LayoutParams(
@@ -21,11 +48,18 @@ class OmniView(val context: ThemedReactContext) : HybridOmniViewSpec(), SurfaceH
}
private var surfaceReady = false
private var boundPlayer: OmniPlayer? = null
private var rootContent: ViewGroup? = null
private var rootContentViews: List<View> = emptyList()
private var movedSurfaceToRootForPip = false
override lateinit var player: HybridOmniPlayerSpec
override var autoplay: Boolean? = true
override var autoPip: Boolean? = true
init {
context.addLifecycleEventListener(this)
}
override fun afterUpdate() {
Log.e("omniView", "After update called")
if (!::player.isInitialized) {
@@ -51,6 +85,9 @@ class OmniView(val context: ThemedReactContext) : HybridOmniViewSpec(), SurfaceH
}
override fun onDropView() {
restoreUiAfterPip()
context.removeLifecycleEventListener(this)
if (!::player.isInitialized) return
val omniPlayer = player as? OmniPlayer ?: return
@@ -58,6 +95,95 @@ class OmniView(val context: ThemedReactContext) : HybridOmniViewSpec(), SurfaceH
boundPlayer = null
}
override fun onHostResume() {
Log.e("omni", "resume")
restoreUiAfterPip()
}
override fun onHostPause() {
Log.e("omni", "pause")
if (autoPip != true) {
return
}
val activity = context.currentActivity ?: return
if (activity.isFinishing || activity.isDestroyed || activity.isInPictureInPictureMode) {
return
}
val omniPlayer = boundPlayer ?: return
if (!omniPlayer.isPlaying) {
return
}
val aspectRatio = if (surfaceView.width > 0 && surfaceView.height > 0) {
Rational(surfaceView.width, surfaceView.height)
} else {
Rational(16, 9)
}
val params = PictureInPictureParams.Builder()
.setAspectRatio(aspectRatio)
.build()
isolateUiForPip(activity)
val enteredPip = activity.enterPictureInPictureMode(params)
if (!enteredPip) {
restoreUiAfterPip()
}
}
override fun onHostDestroy() {
restoreUiAfterPip()
}
private fun isolateUiForPip(activity: android.app.Activity) {
if (movedSurfaceToRootForPip) {
return
}
val root = activity.window.decorView.findViewById<ViewGroup>(android.R.id.content) ?: return
rootContent = root
rootContentViews = (0 until root.childCount)
.map { root.getChildAt(it) }
.filter { child -> child.isVisible }
rootContentViews.forEach { child ->
child.visibility = View.GONE
}
view.removeView(surfaceView)
root.addView(
surfaceView,
FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
)
)
movedSurfaceToRootForPip = true
}
private fun restoreUiAfterPip() {
if (!movedSurfaceToRootForPip) {
return
}
val root = rootContent ?: return
root.removeView(surfaceView)
rootContent = null
rootContentViews.forEach { child ->
child.visibility = View.VISIBLE
}
rootContentViews = emptyList()
view.addView(surfaceView, FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
))
movedSurfaceToRootForPip = false
}
override fun surfaceCreated(holder: SurfaceHolder) {
surfaceReady = true
boundPlayer?.setSurface(holder)
@@ -16,6 +16,8 @@
android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
android:launchMode="singleTask"
android:resizeableActivity="true"
android:supportsPictureInPicture="true"
android:windowSoftInputMode="adjustResize"
android:exported="true">
<intent-filter>