Skip to content

Commit

Permalink
feat(*): improve full screen usage for view based player
Browse files Browse the repository at this point in the history
  • Loading branch information
ThibaultBee committed Nov 6, 2023
1 parent 2d6e0c2 commit b8524cc
Show file tree
Hide file tree
Showing 11 changed files with 231 additions and 72 deletions.
24 changes: 18 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,11 @@ analytics of [your viewers usage](https://api.video/product/video-analytics/).

```xml

<video.api.player.ApiVideoExoPlayerView
<video.api.player.ApiVideoExoPlayerView
android:id="@+id/playerView"
android:layout_width="wrap_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:show_controls="true"
app:show_controls="true"
app:show_subtitles="true" />
```

Expand All @@ -101,7 +101,7 @@ val playerControllerListener = object : ApiVideoPlayerController.Listener {
```kotlin
val playerView = findViewById<ApiVideoExoPlayerView>(R.id.playerView)

val player = ApiVideoPlayerController(
val playerController = ApiVideoPlayerController(
applicationContext,
VideoOptions(
"YOUR_VIDEO_ID",
Expand All @@ -114,8 +114,20 @@ val player = ApiVideoPlayerController(

4. Fullscreen video

If you requires a fullscreen video. You will have to implement
the `ApiVideoPlayerController.ViewListener` interface.
If you require a fullscreen video. You will have to implement
the `ApiVideoExoPlayerView.FullScreenListener` interface.
To help you, you can use the `ApiVideoPlayerFullScreenController` that will handle the fullscreen.
As it implements the `ApiVideoExoPlayerView.FullScreenListener` interface, you can pass it to
your `ApiVideoExoPlayerView` instance.

```kotlin
playerView.fullScreenListener = ApiVideoExoPlayerView(
supportFragmentManager,
playerView,
playerController
)
```

Check out for the implementation in the [Sample application](#sample-application).

### Supported player views
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,14 @@ import android.os.Bundle
import android.util.Log
import android.util.Size
import android.view.View
import android.view.ViewGroup
import android.widget.PopupMenu
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.preference.PreferenceManager
import com.android.volley.ClientError
import com.google.android.material.snackbar.Snackbar
import video.api.player.ApiVideoPlayerController
import video.api.player.example.databinding.ActivityMainBinding
import video.api.player.models.ApiVideoPlayerFullScreenController
import video.api.player.models.VideoOptions
import video.api.player.models.VideoType
import video.api.player.views.ApiVideoExoPlayerView
Expand Down Expand Up @@ -51,33 +48,6 @@ class MainActivity : AppCompatActivity() {
}
}

private val fullScreenListener = object : ApiVideoExoPlayerView.FullScreenListener {
override fun onFullScreenModeChanged(isFullScreen: Boolean) {
/**
* For fullscreen video, hides every views and forces orientation in landscape.
*/
if (isFullScreen) {
supportActionBar?.hide()
hideSystemUI()
binding.fab.hide()
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
binding.playerView.layoutParams.apply {
width = ViewGroup.LayoutParams.MATCH_PARENT
height = ViewGroup.LayoutParams.MATCH_PARENT
}
} else {
supportActionBar?.show()
showSystemUI()
binding.fab.show()
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR
binding.playerView.layoutParams.apply {
width = ViewGroup.LayoutParams.WRAP_CONTENT
height = ViewGroup.LayoutParams.WRAP_CONTENT
}
}
}
}

private val playerControllerListener = object : ApiVideoPlayerController.Listener {
override fun onError(error: Exception) {
val message = when {
Expand Down Expand Up @@ -125,8 +95,23 @@ class MainActivity : AppCompatActivity() {
}
}

private val fullScreenController: ApiVideoPlayerFullScreenController by lazy {
ApiVideoPlayerFullScreenController(
supportFragmentManager,
binding.playerView,
playerController,
object : ApiVideoExoPlayerView.FullScreenListener {
override fun onFullScreenModeChanged(isFullScreen: Boolean) {
requestedOrientation = if (isFullScreen) {
ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
} else {
ActivityInfo.SCREEN_ORIENTATION_SENSOR
}
}
}
)
}
private val playerController: ApiVideoPlayerController by lazy {
binding.playerView.fullScreenListener = fullScreenListener
ApiVideoPlayerController(
applicationContext,
null,
Expand All @@ -140,23 +125,6 @@ class MainActivity : AppCompatActivity() {
Snackbar.make(binding.root, message, Snackbar.LENGTH_LONG).show()
}

private fun hideSystemUI() {
WindowCompat.setDecorFitsSystemWindows(window, false)
WindowInsetsControllerCompat(window, window.decorView).let { controller ->
controller.hide(WindowInsetsCompat.Type.systemBars())
controller.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
}

private fun showSystemUI() {
WindowCompat.setDecorFitsSystemWindows(window, true)
WindowInsetsControllerCompat(
window,
window.decorView
).show(WindowInsetsCompat.Type.systemBars())
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

Expand Down Expand Up @@ -187,8 +155,9 @@ class MainActivity : AppCompatActivity() {
}
binding.unmute.setOnClickListener { playerController.isMuted = false }

binding.playerView.fullScreenListener = fullScreenController
binding.showFullScreenButton.setOnClickListener {
binding.playerView.fullScreenListener = fullScreenListener
binding.playerView.fullScreenListener = fullScreenController
}
binding.hideFullScreenButton.setOnClickListener {
binding.playerView.fullScreenListener = null
Expand Down
2 changes: 2 additions & 0 deletions player/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ dependencies {
implementation "androidx.media3:media3-exoplayer:${exoPlayerVersion}"
implementation "androidx.media3:media3-exoplayer-hls:${exoPlayerVersion}"
implementation "androidx.media:media:1.6.0"
implementation "androidx.fragment:fragment-ktx:1.6.2"
implementation "com.google.android.material:material:1.10.0"

testImplementation 'org.robolectric:robolectric:4.10.3'
testImplementation 'org.robolectric:shadows-httpclient:4.5.1'
Expand Down
16 changes: 13 additions & 3 deletions player/src/main/java/video/api/player/ApiVideoPlayerController.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package video.api.player

import android.annotation.SuppressLint
import android.content.Context
import android.os.Handler
import android.os.Looper
Expand Down Expand Up @@ -597,9 +598,7 @@ constructor(
*
* @param view the player view. An [ApiVideoExoPlayerView] for example.
*/
fun setPlayerView(view: IExoPlayerBasedPlayerView) {
view.playerView.player = exoplayer
}
fun setPlayerView(view: IExoPlayerBasedPlayerView) = setPlayerView(view.playerView)

/**
* Sets the player view
Expand Down Expand Up @@ -637,6 +636,17 @@ constructor(
exoplayer.setVideoSurface(surface)
}

@SuppressLint("UnsafeOptInUsageError")
fun switchTargetView(oldPlayerView: PlayerView, newPlayerView: PlayerView) {
PlayerView.switchTargetView(exoplayer, oldPlayerView, newPlayerView)
}

@SuppressLint("UnsafeOptInUsageError")
fun switchTargetView(
oldPlayerView: IExoPlayerBasedPlayerView,
newPlayerView: IExoPlayerBasedPlayerView
) = switchTargetView(oldPlayerView.playerView, newPlayerView.playerView)

companion object {
private const val TAG = "ApiVideoPlayer"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package video.api.player.extensions

import android.view.Window
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat

/**
* Hides the system UI: status bar, navigation bar and system bars.
*/
fun Window.hideSystemUI() {
WindowCompat.setDecorFitsSystemWindows(this, false)
WindowInsetsControllerCompat(this, this.decorView).let { controller ->
controller.hide(WindowInsetsCompat.Type.systemBars())
controller.hide(WindowInsetsCompat.Type.navigationBars())
controller.hide(WindowInsetsCompat.Type.statusBars())
controller.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package video.api.player.models

import android.util.Log
import android.widget.ImageButton
import androidx.fragment.app.FragmentManager
import video.api.player.ApiVideoPlayerController
import video.api.player.R
import video.api.player.views.ApiVideoExoPlayerView
import video.api.player.views.FullScreenDialogFragment

/**
* A class that handles the player full screen.
*
* Internally, it creates another player view in a full screen dialog fragment.
*
* @param fragmentManager The fragment manager.
* @param originalPlayerView The player view.
* @param playerController The player controller.
* @param fullScreenListener The full screen listener if you want to lock the orientation in full screen.
*/
class ApiVideoPlayerFullScreenController(
private val fragmentManager: FragmentManager,
private val originalPlayerView: ApiVideoExoPlayerView,
private val playerController: ApiVideoPlayerController,
private val fullScreenListener: ApiVideoExoPlayerView.FullScreenListener? = null
) : ApiVideoExoPlayerView.FullScreenListener {
/**
* Full screen listener for the full screen player view.
*/
private val internalFullScreenListener = object : ApiVideoExoPlayerView.FullScreenListener {
override fun onFullScreenModeChanged(isFullScreen: Boolean) {
if (dialogFragment.isVisible) {
fullScreenPlayerView.playerView.findViewById<ImageButton>(androidx.media3.ui.R.id.exo_fullscreen)
.setImageResource(R.drawable.exo_styled_controls_fullscreen_exit)
playerController.switchTargetView(fullScreenPlayerView, originalPlayerView)
dialogFragment.dismiss()
fullScreenListener?.onFullScreenModeChanged(false)
} else {
Log.e(TAG, "onFullScreenModeChanged: not expected when dialog is already visible")
}
}
}
private val fullScreenPlayerView: ApiVideoExoPlayerView = originalPlayerView.duplicate().apply {
this.fullScreenListener = this@ApiVideoPlayerFullScreenController.internalFullScreenListener
this.playerView.findViewById<ImageButton>(androidx.media3.ui.R.id.exo_fullscreen)
.setImageResource(R.drawable.exo_styled_controls_fullscreen_exit)
}
private val dialogFragment: FullScreenDialogFragment =
FullScreenDialogFragment(fullScreenPlayerView)

/**
* Original view full screen listener.
*/
override fun onFullScreenModeChanged(isFullScreen: Boolean) {
if (!dialogFragment.isVisible) {
originalPlayerView.playerView.findViewById<ImageButton>(androidx.media3.ui.R.id.exo_fullscreen)
.setImageResource(R.drawable.exo_styled_controls_fullscreen_enter)
playerController.switchTargetView(originalPlayerView, fullScreenPlayerView)
dialogFragment.show(fragmentManager, TAG)
fullScreenListener?.onFullScreenModeChanged(true)
} else {
Log.e(TAG, "onFullScreenModeChanged: not expected")
}
}

companion object {
private const val TAG = "FullScreenDialogCtrl"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import androidx.media3.ui.PlayerView
import video.api.player.R
import video.api.player.databinding.ExoPlayerLayoutBinding
import video.api.player.interfaces.IExoPlayerBasedPlayerView
import video.api.player.models.ApiVideoPlayerFullScreenController

/**
* The api.video player view class based on an ExoPlayer [PlayerView].
Expand All @@ -23,9 +24,7 @@ import video.api.player.interfaces.IExoPlayerBasedPlayerView
* @param defStyleAttr an attribute in the current theme that contains a reference to a style resource that supplies default values for the view. Can be 0 to not look for defaults.
*/
class ApiVideoExoPlayerView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr), IExoPlayerBasedPlayerView {
private val binding = ExoPlayerLayoutBinding.inflate(LayoutInflater.from(context), this)

Expand All @@ -35,10 +34,11 @@ class ApiVideoExoPlayerView @JvmOverloads constructor(
/**
* Sets or gets the full screen listener.
* If set to null, the full screen button is hidden.
*
* To simplify full screen management, you can use [ApiVideoPlayerFullScreenController].
*/
var fullScreenListener: FullScreenListener? = null
@SuppressLint("UnsafeOptInUsageError")
set(value) {
@SuppressLint("UnsafeOptInUsageError") set(value) {
if (value != null) {
playerView.setFullscreenButtonClickListener {
value.onFullScreenModeChanged(it)
Expand All @@ -62,8 +62,7 @@ class ApiVideoExoPlayerView @JvmOverloads constructor(
* Shows or hides the subtitles
*/
var showSubtitles: Boolean = true
@SuppressLint("UnsafeOptInUsageError")
set(value) {
@SuppressLint("UnsafeOptInUsageError") set(value) {
if (value) {
playerView.subtitleView?.visibility = VISIBLE
playerView.setShowSubtitleButton(true)
Expand All @@ -78,10 +77,8 @@ class ApiVideoExoPlayerView @JvmOverloads constructor(
* Sets or gets how the video is fitted in its parent view
*/
var viewFit: ViewFit
@SuppressLint("UnsafeOptInUsageError")
get() = ViewFit.fromValue(playerView.resizeMode)
@SuppressLint("UnsafeOptInUsageError")
set(value) {
@SuppressLint("UnsafeOptInUsageError") get() = ViewFit.fromValue(playerView.resizeMode)
@SuppressLint("UnsafeOptInUsageError") set(value) {
playerView.resizeMode = value.value
}

Expand All @@ -95,6 +92,15 @@ class ApiVideoExoPlayerView @JvmOverloads constructor(
}
}

fun duplicate(): ApiVideoExoPlayerView {
val view = ApiVideoExoPlayerView(context)
view.showControls = showControls
view.showSubtitles = showSubtitles
view.viewFit = viewFit
view.fullScreenListener = fullScreenListener
return view
}

interface FullScreenListener {
/**
* Called when the full screen button has been clicked
Expand Down
Loading

0 comments on commit b8524cc

Please sign in to comment.