Skip to content
Open

fix pip #17983

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
Original file line number Diff line number Diff line change
Expand Up @@ -9,59 +9,65 @@ package com.nextcloud.client.player.ui

import android.app.Activity
import android.app.PictureInPictureParams
import android.graphics.Rect
import android.os.Build
import android.util.Rational
import android.view.View
import com.nextcloud.client.player.media3.PlaybackModel
import com.nextcloud.client.player.model.state.VideoSize
import com.nextcloud.client.player.util.PlayerUtil.isPictureInPictureAllowed
import com.owncloud.android.lib.common.utils.Log_OC

private const val ASPECT_RATIO_WIDTH = 16
private const val ASPECT_RATIO_HEIGHT = 9
private const val MIN_ASPECT_RATIO = 0.42f
private const val MAX_ASPECT_RATIO = 2.39f
private const val TAG = "VideoPictureInPicture"
private val MIN_ASPECT_RATIO = Rational(41841, 100000)
private val MAX_ASPECT_RATIO = Rational(239, 100)

class VideoPictureInPicture(private val activity: Activity, private val playbackModel: PlaybackModel) {
class VideoPictureInPicture(private val activity: Activity) {

private val defaultAspectRatio = Rational(ASPECT_RATIO_WIDTH, ASPECT_RATIO_HEIGHT)
private var aspectRatio: Rational? = null
private var isAutoEnterEnabled = false

val isAllowed: Boolean get() = activity.isPictureInPictureAllowed()

fun enter(sourceView: View): Boolean = isAllowed && activity.enterPictureInPictureMode(createParams(sourceView))

private fun createParams(sourceView: View): PictureInPictureParams {
val aspectRatio = getAspectRatio(playbackModel.state?.currentItemState?.videoSize)
return PictureInPictureParams.Builder().let {
it.setAspectRatio(aspectRatio)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
it.setAutoEnterEnabled(false)
}
it.setSourceRectHint(getSourceRectHint(sourceView, aspectRatio))
it.build()
}
fun enter(): Boolean = isAllowed && activity.enterPictureInPictureMode(createParams())

fun enterOnUserLeave() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S || !isAutoEnterEnabled) return
enter()
}

fun update(videoSize: VideoSize?, isPlaying: Boolean) {
val newAspectRatio = videoSize?.toAspectRatio() ?: aspectRatio
if (newAspectRatio == aspectRatio && isPlaying == isAutoEnterEnabled) return

aspectRatio = newAspectRatio
isAutoEnterEnabled = isPlaying
applyParams()
}

private fun getAspectRatio(videoSize: VideoSize?): Rational {
if (videoSize == null) return defaultAspectRatio
fun disableAutoEnter() {
if (!isAutoEnterEnabled) return

val ratio = videoSize.width.toFloat() / videoSize.height.toFloat()
return if (ratio < MIN_ASPECT_RATIO || ratio > MAX_ASPECT_RATIO) {
defaultAspectRatio
} else {
Rational(videoSize.width, videoSize.height)
isAutoEnterEnabled = false
applyParams()
}

private fun applyParams() {
if (!isAllowed) return

try {
activity.setPictureInPictureParams(createParams())
} catch (e: IllegalStateException) {
Log_OC.e(TAG, "setPictureInPictureParams failed", e)
}
}

private fun getSourceRectHint(sourceView: View, aspectRatio: Rational): Rect {
val sourceRect = Rect()
sourceView.getGlobalVisibleRect(sourceRect)
val heightHint = (sourceRect.width() / aspectRatio.toFloat()).toInt()
return Rect(
sourceRect.left,
sourceRect.top + (sourceRect.height() - heightHint) / 2,
sourceRect.right,
sourceRect.top + (sourceRect.height() + heightHint) / 2
)
private fun createParams(): PictureInPictureParams = PictureInPictureParams.Builder().apply {
aspectRatio?.let(::setAspectRatio)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
setAutoEnterEnabled(isAutoEnterEnabled)
}
}.build()

private fun VideoSize.toAspectRatio(): Rational? {
if (width <= 0 || height <= 0) return null
return Rational(width, height).coerceIn(MIN_ASPECT_RATIO, MAX_ASPECT_RATIO)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ class PreviewMediaActivity :
private var isDownloadWorkStarted = false
private var screenState = PreviewImageActivityState.Idle

private val pictureInPicture by lazy { VideoPictureInPicture(this, playbackModel) }
val pictureInPicture by lazy { VideoPictureInPicture(this) }
private var wasActionBarVisibleBeforePictureInPicture = true
private var keepPlaybackOnFinish = false

Expand Down Expand Up @@ -563,7 +563,12 @@ class PreviewMediaActivity :
viewPager?.setCurrentItem(position, true)
}

fun enterPictureInPicture(): Boolean = viewPager?.let { pictureInPicture.enter(it) } == true
fun enterPictureInPicture(): Boolean = pictureInPicture.enter()

override fun onUserLeaveHint() {
super.onUserLeaveHint()
pictureInPicture.enterOnUserLeave()
}

override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ import android.view.View
import android.view.ViewGroup
import androidx.activity.OnBackPressedCallback
import androidx.activity.addCallback
import androidx.activity.trackPipAnimationHintView
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import androidx.core.view.MenuProvider
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.nextcloud.client.account.UserAccountManager
import com.nextcloud.client.player.media3.PlaybackModel
import com.nextcloud.client.player.media3.PlaybackQueueLoader
Expand Down Expand Up @@ -127,6 +129,7 @@ class PreviewPlaybackFragment :
binding = PreviewPlaybackFragmentBinding.inflate(inflater, container, false)
loadThumbnail()
registerPictureInPictureOnBack()
trackPictureInPictureSourceRect()
binding.playerControlView.navigator = activity as? MediaNavigator
binding.playerControlView.addPaddingForNavBar()
binding.playerControlView.isVisible = previewActivity?.isActionBarVisible != false
Expand Down Expand Up @@ -257,6 +260,16 @@ class PreviewPlaybackFragment :
}
}

private fun trackPictureInPictureSourceRect() {
if (!MimeTypeUtil.isVideo(file)) return

viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
requireActivity().trackPipAnimationHintView(binding.surfaceView)
}
}
}

override fun onStart() {
super.onStart()
playbackModel.addListener(this)
Expand All @@ -274,6 +287,9 @@ class PreviewPlaybackFragment :
override fun onPause() {
pictureInPictureCallback?.isEnabled = false
binding.playerControlView.onStop()
if (!isInPictureInPictureMode()) {
previewActivity?.pictureInPicture?.disableAutoEnter()
}
if (!isInPictureInPictureMode() && isCurrentItem(playbackModel.state)) {
playbackModel.pause()
playbackModel.setVideoSurfaceView(null)
Expand Down Expand Up @@ -339,6 +355,7 @@ class PreviewPlaybackFragment :
if (isCurrentItem(state)) {
wasCurrentItem = true
showVideo(state?.currentItemState?.videoSize)
updatePictureInPicture(state)

// Keep the screen on while playing. This works because it's called at every playback update and when
// the surfaceview is not visible (scrolled away, app in background...)
Expand Down Expand Up @@ -368,6 +385,14 @@ class PreviewPlaybackFragment :

private fun isInPictureInPictureMode(): Boolean = activity?.isInPictureInPictureMode == true

private fun updatePictureInPicture(state: PlaybackState?) {
if (!MimeTypeUtil.isVideo(file)) return
if (!isResumed && !isInPictureInPictureMode()) return

val itemState = state?.currentItemState
previewActivity?.pictureInPicture?.update(itemState?.videoSize, itemState?.playerState == PlayerState.PLAYING)
}

private fun toggleFullScreen() {
val previewActivity = previewActivity ?: return
binding.playerControlView.setVisibilityWithAnimation(!previewActivity.isActionBarVisible)
Expand Down
Loading