Skip to content

Commit

Permalink
fix(pip): add basic picture in picture implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
abdallahmehiz committed Jul 17, 2024
1 parent cbf166c commit ee94943
Show file tree
Hide file tree
Showing 11 changed files with 260 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,6 @@ class PlayerPreferences(

val defaultSpeed = preferenceStore.getFloat("default_speed", 1f)
val savePositionOnQuit = preferenceStore.getBoolean("save_position", true)

val automaticallyEnterPip = preferenceStore.getBoolean("automatic_pip")
}
72 changes: 72 additions & 0 deletions app/src/main/java/live/mehiz/mpvkt/ui/player/PipActions.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package live.mehiz.mpvkt.ui.player

import android.app.PendingIntent
import android.app.RemoteAction
import android.content.Context
import android.content.Intent
import android.graphics.drawable.Icon
import android.os.Build.VERSION_CODES.O
import androidx.annotation.DrawableRes
import androidx.annotation.RequiresApi
import live.mehiz.mpvkt.R

@RequiresApi(O)
fun createPipActions(
context: Context,
isPaused: Boolean,
): ArrayList<RemoteAction> = arrayListOf(
createPipAction(
context,
"fast rewind",
R.drawable.baseline_fast_rewind_24,
PIP_FR,
),
if (isPaused) {
createPipAction(
context,
"play",
R.drawable.baseline_play_arrow_24,
PIP_PLAY,
)
} else {
createPipAction(
context,
"pause",
R.drawable.baseline_pause_24,
PIP_PAUSE,
)
},
createPipAction(
context,
"fast forward",
R.drawable.baseline_fast_forward_24,
PIP_FF
)
)

@RequiresApi(O)
fun createPipAction(
context: Context,
title: String,
@DrawableRes icon: Int,
actionCode: Int,
): RemoteAction {
return RemoteAction(
Icon.createWithResource(context, icon),
title,
title,
PendingIntent.getBroadcast(
context,
actionCode,
Intent(PIP_INTENTS_FILTER).putExtra(PIP_INTENT_ACTION, actionCode).setPackage(context.packageName),
PendingIntent.FLAG_IMMUTABLE,
),
)
}

const val PIP_INTENTS_FILTER = "pip_control"
const val PIP_INTENT_ACTION = "media_control"
const val PIP_PAUSE = 1
const val PIP_PLAY = 2
const val PIP_FF = 3
const val PIP_FR = 4
139 changes: 127 additions & 12 deletions app/src/main/java/live/mehiz/mpvkt/ui/player/PlayerActivity.kt
Original file line number Diff line number Diff line change
@@ -1,17 +1,29 @@
package live.mehiz.mpvkt.ui.player

import android.app.PictureInPictureParams
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.ActivityInfo
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.media.AudioManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.ParcelFileDescriptor
import android.util.Log
import android.util.Rational
import android.view.KeyEvent
import android.view.WindowManager
import androidx.activity.enableEdgeToEdge
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toAndroidRect
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.core.view.WindowCompat
import androidx.documentfile.provider.DocumentFile
import androidx.media.AudioAttributesCompat
Expand Down Expand Up @@ -56,6 +68,16 @@ class PlayerActivity : AppCompatActivity() {
private var audioFocusRequest: AudioFocusRequestCompat? = null
private var restoreAudioFocus: () -> Unit = {}

private var pipRect: android.graphics.Rect? = null
private val isPipSupported by lazy {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
false
} else {
packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
}
}
private var pipReceiver: BroadcastReceiver? = null

override fun onCreate(savedInstanceState: Bundle?) {
if (playerPreferences.drawOverDisplayCutout.get()) enableEdgeToEdge()
super.onCreate(savedInstanceState)
Expand All @@ -78,25 +100,58 @@ class PlayerActivity : AppCompatActivity() {

binding.controls.setContent {
MpvKtTheme {
controls.Content()
controls.Content(
modifier = Modifier.onGloballyPositioned {
pipRect = it.boundsInWindow().toAndroidRect()
},
)
}
}
}

override fun onDestroy() {
super.onDestroy()
audioFocusRequest?.let {
AudioManagerCompat.abandonAudioFocusRequest(audioManager, it)
}
audioFocusRequest = null
MPVLib.destroy()
super.onDestroy()
}

override fun finish() {
endPlayback(EndPlaybackReason.ExternalAction)
super.finish()
}

override fun onPause() {
super.onPause()
CoroutineScope(Dispatchers.IO).launch {
saveVideoPlaybackState()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
if (isInPictureInPictureMode) return
}
viewModel.pause()
}

override fun onUserLeaveHint() {
if (!isPipSupported) {
super.onUserLeaveHint()
return
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && player.paused == false) {
enterPictureInPictureMode()
}
super.onUserLeaveHint()
}

override fun onStart() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
setPictureInPictureParams(createPipParams())
}
super.onStart()
}

private fun setupMPV() {
Utils.copyAssets(this)
copyMPVConfigFiles()
Expand Down Expand Up @@ -178,7 +233,8 @@ class PlayerActivity : AppCompatActivity() {
private val audioFocusChangeListener = AudioManager.OnAudioFocusChangeListener {
when (it) {
AudioManager.AUDIOFOCUS_LOSS,
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT,
-> {
val oldRestore = restoreAudioFocus
val wasPlayerPaused = player.paused ?: false
viewModel.pause()
Expand Down Expand Up @@ -207,7 +263,10 @@ class PlayerActivity : AppCompatActivity() {
}

private fun setupIntents(intent: Intent) {
intent.getStringExtra("title")?.ifBlank { viewModel.mediaTitle.update { it } }
intent.getStringExtra("title")?.let {
viewModel.mediaTitle.update { _ -> it }
MPVLib.setPropertyString("force-media-title", it)
}
player.timePos = intent.getIntExtra("position", 0) / 1000
}

Expand Down Expand Up @@ -327,6 +386,10 @@ class PlayerActivity : AppCompatActivity() {
}
}

@Suppress("EmptyFunctionBlock", "UnusedParameter")
internal fun efEvent(err: String?) {
}

private suspend fun saveVideoPlaybackState() {
mpvKtDatabase.videoDataDao().upsert(
PlaybackStateEntity(
Expand All @@ -349,28 +412,80 @@ class PlayerActivity : AppCompatActivity() {
}
}

override fun finish() {
endPlayback(EndPlaybackReason.ExternalAction)
}

private fun endPlayback(reason: EndPlaybackReason) {
CoroutineScope(Dispatchers.IO).launch {
saveVideoPlaybackState()
}
if (!intent.getBooleanExtra("return_result", false)) {
super.finish()
return
}
val returnIntent = Intent()
returnIntent.putExtra("end_by", reason.value)
player.timePos?.let { returnIntent.putExtra("position", it * 1000) }
player.duration?.let { returnIntent.putExtra("duration", it * 1000) }
setResult(RESULT_OK, returnIntent)
super.finish()
}

@Suppress("EmptyFunctionBlock", "UnusedParameter")
internal fun efEvent(err: String?) {
@RequiresApi(Build.VERSION_CODES.O)
fun createPipParams(): PictureInPictureParams {
val builder = PictureInPictureParams.Builder()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
builder.setTitle(viewModel.mediaTitle.value)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val autoEnter = playerPreferences.automaticallyEnterPip.get()
builder.setAutoEnterEnabled(player.paused == false && autoEnter)
builder.setSeamlessResizeEnabled(player.paused == false && autoEnter)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder.setActions(createPipActions(this, player.paused ?: true))
}
builder.setSourceRectHint(pipRect)
player.videoH?.let {
val height = it
val width = it * player.getVideoOutAspect()!!
val rational = Rational(height, width.toInt()).toFloat()
if (rational in 0.41..2.40) builder.setAspectRatio(Rational(width.toInt(), height))
}
return builder.build()
}

override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) {
if (!isInPictureInPictureMode) {
pipReceiver?.let {
unregisterReceiver(pipReceiver)
pipReceiver = null
}
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
return
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
setPictureInPictureParams(createPipParams())
}
viewModel.hideControls()
viewModel.hideSeekBar()
viewModel.isBrightnessSliderShown.update { false }
viewModel.isVolumeSliderShown.update { false }
pipReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent == null || intent.action != PIP_INTENTS_FILTER) return
when (intent.getIntExtra(PIP_INTENT_ACTION, 0)) {
PIP_PAUSE -> viewModel.pause()
PIP_PLAY -> viewModel.unpause()
PIP_FF -> viewModel.seekBy(playerPreferences.doubleTapToSeekDuration.get())
PIP_FR -> viewModel.seekBy(-playerPreferences.doubleTapToSeekDuration.get())
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
setPictureInPictureParams(createPipParams())
}
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(pipReceiver, IntentFilter(PIP_INTENTS_FILTER), RECEIVER_NOT_EXPORTED)
} else {
registerReceiver(pipReceiver, IntentFilter(PIP_INTENTS_FILTER))
}
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
}

private fun setOrientation() {
Expand Down
11 changes: 11 additions & 0 deletions app/src/main/java/live/mehiz/mpvkt/ui/player/PlayerViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package live.mehiz.mpvkt.ui.player

import android.media.AudioManager
import android.net.Uri
import android.os.Build
import android.provider.Settings
import android.util.DisplayMetrics
import androidx.core.view.WindowInsetsCompat
Expand Down Expand Up @@ -237,6 +238,11 @@ class PlayerViewModel(
fun pause() {
activity.player.paused = true
_paused.update { true }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
runCatching {
activity.setPictureInPictureParams(activity.createPipParams())
}
}
}

fun unpause() {
Expand Down Expand Up @@ -349,6 +355,11 @@ class PlayerViewModel(
MPVLib.setPropertyDouble("video-aspect-override", ratio)
playerPreferences.videoAspect.set(aspect)
playerUpdate.update { PlayerUpdates.AspectRatio }
runCatching {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
activity.setPictureInPictureParams(activity.createPipParams())
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package live.mehiz.mpvkt.ui.player.controls

import android.os.Build
import androidx.compose.foundation.layout.Row
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AspectRatio
import androidx.compose.material.icons.filled.PictureInPictureAlt
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import live.mehiz.mpvkt.preferences.PlayerPreferences
import live.mehiz.mpvkt.preferences.preference.collectAsState
import live.mehiz.mpvkt.ui.player.PlayerActivity
import live.mehiz.mpvkt.ui.player.PlayerViewModel
import live.mehiz.mpvkt.ui.player.VideoAspect
import live.mehiz.mpvkt.ui.player.controls.components.ControlsButton
Expand All @@ -16,11 +20,24 @@ import org.koin.compose.koinInject
@Composable
fun BottomRightPlayerControls(
viewModel: PlayerViewModel,
modifier: Modifier = Modifier
modifier: Modifier = Modifier,
) {
val playerPreferences = koinInject<PlayerPreferences>()
val aspect by playerPreferences.videoAspect.collectAsState()
Row(modifier) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val activity = LocalContext.current as PlayerActivity
ControlsButton(
Icons.Default.PictureInPictureAlt,
onClick = {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
activity.enterPictureInPictureMode(activity.createPipParams())
} else {
activity.enterPictureInPictureMode()
}
},
)
}
ControlsButton(
Icons.Default.AspectRatio,
onClick = {
Expand Down
Loading

0 comments on commit ee94943

Please sign in to comment.