diff --git a/android/src/main/java/com/amazonaws/ivs/reactnative/player/AmazonIvsView.kt b/android/src/main/java/com/amazonaws/ivs/reactnative/player/AmazonIvsView.kt index f1030f8..565c543 100644 --- a/android/src/main/java/com/amazonaws/ivs/reactnative/player/AmazonIvsView.kt +++ b/android/src/main/java/com/amazonaws/ivs/reactnative/player/AmazonIvsView.kt @@ -1,5 +1,9 @@ package com.amazonaws.ivs.reactnative.player - +import android.content.BroadcastReceiver +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.content.IntentFilter import android.content.pm.PackageManager import android.net.Uri import android.widget.FrameLayout @@ -17,20 +21,29 @@ import kotlin.concurrent.timerTask import android.app.PictureInPictureParams import android.app.Activity import androidx.annotation.RequiresApi - +import android.graphics.Rect +import android.view.View +import android.util.Rational +import android.app.RemoteAction +import android.graphics.drawable.Icon +private const val EXTRA_ACTION = "EXTRA_ACTION" +private const val ACTION_MEDIA_CONTROL = "pip_media_control" +private const val ACTION_PLAY = 0 +private const val ACTION_PAUSE = ACTION_PLAY + 1 class AmazonIvsView(private val context: ThemedReactContext) : FrameLayout(context), LifecycleEventListener { + private var playerView: PlayerView? = null private var player: Player? = null private var streamUri: Uri? = null private val playerListener: Player.Listener? - + private var enabledBroadcast: Boolean = false var playerObserver: Timer? = null private var lastLiveLatency: Long? = null private var lastBitrate: Long? = null private var lastDuration: Long? = null private var finishedLoading: Boolean = false - + private val broadcastReceiver: BroadcastReceiver = buildBroadcastReceiver() enum class Events(private val mName: String) { STATE_CHANGED("onPlayerStateChange"), DURATION_CHANGED("onDurationChange"), @@ -104,6 +117,7 @@ class AmazonIvsView(private val context: ThemedReactContext) : FrameLayout(conte playerObserver?.schedule(timerTask { intervalHandler() }, 0, 1000) + enableBroadcastReceiver() } override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { @@ -121,11 +135,37 @@ class AmazonIvsView(private val context: ThemedReactContext) : FrameLayout(conte finishedLoading = false player.load(uri) - reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, Events.LOAD_START.toString(), Arguments.createMap()) } } + fun enableBroadcastReceiver(){ + val activity: Activity? = context.currentActivity + if (enabledBroadcast) { + return + } + + activity?.registerReceiver( + broadcastReceiver, + IntentFilter(ACTION_MEDIA_CONTROL) + ) + enabledBroadcast = true + } + + fun disableBroadcastReceiver(){ + val activity: Activity? = context.currentActivity + if (!enabledBroadcast) { + return + } + + try { + activity?.unregisterReceiver(broadcastReceiver) + } catch(ignore: IllegalArgumentException) { + enabledBroadcast = false; + } + + } + fun setMuted(muted: Boolean) { player?.isMuted = muted } @@ -303,7 +343,7 @@ class AmazonIvsView(private val context: ThemedReactContext) : FrameLayout(conte fun onPlayerStateChange(state: Player.State) { val reactContext = context as ReactContext - + updatePipParams() when (state) { Player.State.PLAYING -> { if (!finishedLoading) { @@ -429,6 +469,99 @@ class AmazonIvsView(private val context: ThemedReactContext) : FrameLayout(conte } } + private fun getVideoAspectRatio(): Rational { + val width = 16 + val height = 9 + return Rational(playerView?.getWidth() ?: 16,playerView?.getHeight() ?: 9) + } + + private fun getVisibleRectForView(view: View): Rect? { + val visibleRect = Rect() + if (view.getGlobalVisibleRect(visibleRect)) { + return visibleRect + } + return null + } + + private fun buildBroadcastReceiver(): BroadcastReceiver { + val reactContext = context as ReactContext + return object : BroadcastReceiver() { + @RequiresApi(Build.VERSION_CODES.O) + override fun onReceive(context: Context?, intent: Intent?) { + intent?.getIntExtra(EXTRA_ACTION, -1)?.let { action -> + when (action) { + ACTION_PLAY -> play() + ACTION_PAUSE -> pause() + } + reactContext.currentActivity?.setPictureInPictureParams(getPipParams()) + } + } + } + } + + + + @RequiresApi(Build.VERSION_CODES.O) + fun buildPipActions( + playing: Boolean, + ): List { + return mutableListOf().apply { + add( + if (playing) { + buildRemoteAction( + ACTION_PAUSE, + R.drawable.ic_pause, + "Pause", + "Pause Video" + ) + } else { + buildRemoteAction( + ACTION_PLAY, + R.drawable.ic_play, + "Play", + "Play Video" + ) + } + ) + } + } + + + + @RequiresApi(Build.VERSION_CODES.O) + fun getPipParams(): PictureInPictureParams { + + + val visibleRect = getVisibleRectForView(this) + val aspectRatio = getVideoAspectRatio() + val initialWidth = ((playerView?.getMeasuredWidth() ?: 0) * 0.75).toInt() + val initialHeight = ((playerView?.getMeasuredHeight() ?: 0) * 0.75).toInt() + val initialBonds = Rect(0, 0, initialWidth, initialHeight) + + return PictureInPictureParams.Builder() + .setSourceRectHint(initialBonds) + .setActions( + buildPipActions(player?.getState() === Player.State.PLAYING) + ) + .build() + } + + @RequiresApi(Build.VERSION_CODES.O) + private fun buildRemoteAction( + requestId: Int, + iconId: Int, + title: String, + desc: String, + ): RemoteAction { + val reactContext = context as ReactContext + val requestCode = requestId + val intent = Intent(ACTION_MEDIA_CONTROL).putExtra(EXTRA_ACTION, requestCode) + val pendingIntent = + PendingIntent.getBroadcast(reactContext, requestCode, intent, PendingIntent.FLAG_IMMUTABLE) + val icon: Icon = Icon.createWithResource(reactContext, iconId) + return RemoteAction(icon, title, desc, pendingIntent) + } + fun togglePip(){ @@ -438,8 +571,7 @@ class AmazonIvsView(private val context: ThemedReactContext) : FrameLayout(conte PackageManager.FEATURE_PICTURE_IN_PICTURE)) { val activity: Activity? = context.currentActivity if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val params = PictureInPictureParams.Builder() - activity?.enterPictureInPictureMode(params.build()); + activity?.enterPictureInPictureMode(getPipParams()); } else { activity?.enterPictureInPictureMode(); } @@ -447,6 +579,13 @@ class AmazonIvsView(private val context: ThemedReactContext) : FrameLayout(conte } } + private fun updatePipParams() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val activity: Activity? = context.currentActivity + activity?.setPictureInPictureParams(getPipParams()) + } + } + override fun onHostResume() { } @@ -458,10 +597,10 @@ class AmazonIvsView(private val context: ThemedReactContext) : FrameLayout(conte } fun cleanup() { + disableBroadcastReceiver() player?.removeListener(playerListener!!) player?.release() player = null - playerObserver?.cancel() playerObserver = null } diff --git a/android/src/main/res/drawable/ic_pause.xml b/android/src/main/res/drawable/ic_pause.xml new file mode 100644 index 0000000..2865518 --- /dev/null +++ b/android/src/main/res/drawable/ic_pause.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/android/src/main/res/drawable/ic_play.xml b/android/src/main/res/drawable/ic_play.xml new file mode 100644 index 0000000..62d965e --- /dev/null +++ b/android/src/main/res/drawable/ic_play.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/example/src/screens/AdvancedExample.tsx b/example/src/screens/AdvancedExample.tsx index ee3f3a2..677592d 100644 --- a/example/src/screens/AdvancedExample.tsx +++ b/example/src/screens/AdvancedExample.tsx @@ -90,6 +90,9 @@ export default function AdvancedExample() { if (state === PlayerState.Playing || state === PlayerState.Idle) { setBuffering(false); } + if (state === PlayerState.Idle) { + setPaused(true); + } }} onProgress={(newPosition) => { if (!lockPosition) {