Skip to content

Commit

Permalink
Merge pull request #62 from damontecres/feat/exo-player
Browse files Browse the repository at this point in the history
Use ExoPlayer
  • Loading branch information
damontecres authored Jan 24, 2024
2 parents 890d9ec + 6f7a028 commit 212a755
Show file tree
Hide file tree
Showing 9 changed files with 328 additions and 11 deletions.
7 changes: 7 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ tasks.create("generateStrings", Exec::class.java) {

tasks.preBuild.dependsOn("generateStrings")

val mediaVersion = "1.2.1"

dependencies {
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.leanback:leanback:1.1.0-rc02")
Expand All @@ -137,6 +139,11 @@ dependencies {
implementation("com.apollographql.apollo3:apollo-runtime:3.8.2")
implementation("androidx.preference:preference-ktx:1.2.1")

implementation("androidx.media3:media3-exoplayer:$mediaVersion")
implementation("androidx.media3:media3-ui:$mediaVersion")
implementation("androidx.media3:media3-exoplayer-dash:$mediaVersion")
implementation("androidx.media3:media3-exoplayer-hls:$mediaVersion")

testImplementation("androidx.test:core-ktx:1.5.0")
testImplementation("junit:junit:4.13.2")
testImplementation("org.mockito:mockito-core:5.9.0")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,41 @@ package com.github.damontecres.stashapp
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.annotation.OptIn
import androidx.fragment.app.Fragment
import androidx.media3.common.util.UnstableApi
import androidx.preference.PreferenceManager

/** Loads [PlaybackVideoFragment]. */
class PlaybackActivity : SecureFragmentActivity() {
private val fragment: PlaybackVideoFragment = PlaybackVideoFragment()
private lateinit var fragment: Fragment

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
val useExo =
PreferenceManager.getDefaultSharedPreferences(this).getBoolean("playerChoice", true)
fragment =
if (useExo) {
PlaybackExoFragment()
} else {
PlaybackVideoFragment()
}
supportFragmentManager.beginTransaction()
.replace(android.R.id.content, fragment)
.commit()
}
}

@OptIn(UnstableApi::class)
@Deprecated("Deprecated in Java")
override fun onBackPressed() {
// TODO: deprecated, so use https://stackoverflow.com/a/72634975/608317 eventually
returnPosition()
super.onBackPressed()
if (!(fragment as StashVideoPlayer).hideControlsIfVisible()) {
returnPosition()
super.onBackPressed()
}
}

override fun onStop() {
Expand All @@ -34,8 +50,25 @@ class PlaybackActivity : SecureFragmentActivity() {
*/
private fun returnPosition() {
val intent = Intent()
intent.putExtra("position", fragment.currentVideoPosition)
val position = (fragment as StashVideoPlayer).currentVideoPosition
Log.d(TAG, "Video playback ending, currentVideoPosition=$position")
intent.putExtra("position", position)
setResult(Activity.RESULT_OK, intent)
finish()
}

interface StashVideoPlayer {
val currentVideoPosition: Long

/**
* Hide the controls if needed
*
* @return true if the controls needed to be hidden
*/
fun hideControlsIfVisible(): Boolean
}

companion object {
const val TAG = "PlaybackActivity"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
package com.github.damontecres.stashapp

import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.annotation.OptIn
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.media3.common.MediaItem
import androidx.media3.common.MimeTypes
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.common.util.Util
import androidx.media3.datasource.DataSource
import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.ui.PlayerView
import androidx.preference.PreferenceManager
import com.github.damontecres.stashapp.data.Scene
import com.github.damontecres.stashapp.util.Constants

@OptIn(UnstableApi::class)
class PlaybackExoFragment :
Fragment(R.layout.video_playback),
PlaybackActivity.StashVideoPlayer {
private var player: ExoPlayer? = null
private lateinit var scene: Scene
private lateinit var videoView: PlayerView

private var playbackPosition = -1L

override val currentVideoPosition get() = player?.currentPosition ?: playbackPosition

override fun hideControlsIfVisible(): Boolean {
if (videoView.isControllerFullyVisible) {
videoView.hideController()
return true
}
return false
}

private fun releasePlayer() {
player?.let { exoPlayer ->
playbackPosition = exoPlayer.currentPosition
exoPlayer.release()
}
player = null
}

@OptIn(UnstableApi::class)
private fun initializePlayer(position: Long) {
val apiKey =
PreferenceManager.getDefaultSharedPreferences(requireContext())
.getString("stashApiKey", "")
val skipForward =
PreferenceManager.getDefaultSharedPreferences(requireContext())
.getInt("skip_forward_time", 30)
val skipBack =
PreferenceManager.getDefaultSharedPreferences(requireContext())
.getInt("skip_back_time", 10)

val dataSourceFactory =
DataSource.Factory {
val dataSource = DefaultHttpDataSource.Factory().createDataSource()
if (!apiKey.isNullOrBlank()) {
dataSource.setRequestProperty(Constants.STASH_API_HEADER, apiKey)
dataSource.setRequestProperty(Constants.STASH_API_HEADER.lowercase(), apiKey)
}
dataSource
}

player =
ExoPlayer.Builder(requireContext())
.setMediaSourceFactory(
DefaultMediaSourceFactory(requireContext()).setDataSourceFactory(
dataSourceFactory,
),
)
.setSeekBackIncrementMs(skipBack * 1000L)
.setSeekForwardIncrementMs(skipForward * 1000L)
.build()
.also { exoPlayer ->
videoView.player = exoPlayer
}.also { exoPlayer ->
var mediaItem: MediaItem? = null
var streamUrl = scene.streams.get("Direct stream")
if (streamUrl != null && scene.videoCodec != "av1") {
mediaItem = MediaItem.fromUri(streamUrl)
} else {
val streamChoice =
PreferenceManager.getDefaultSharedPreferences(requireContext())
.getString("stream_choice", "MP4")
streamUrl = scene.streams.get(streamChoice)
val mimeType =
if (streamChoice == "DASH") {
MimeTypes.APPLICATION_MPD
} else if (streamChoice == "HLS") {
MimeTypes.APPLICATION_M3U8
} else if (streamChoice == "MP4") {
MimeTypes.VIDEO_MP4
} else {
MimeTypes.VIDEO_WEBM
}

mediaItem =
MediaItem.Builder()
.setUri(streamUrl)
.setMimeType(mimeType)
.build()
}

exoPlayer.setMediaItem(mediaItem)
exoPlayer.playWhenReady = true
exoPlayer.prepare()
videoView.hideController()
exoPlayer.addListener(
object : Player.Listener {
private var initialSeek = true

override fun onAvailableCommandsChanged(availableCommands: Player.Commands) {
if (initialSeek && position > 0 && Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM in availableCommands) {
exoPlayer.seekTo(position)
initialSeek = false
}
}
},
)
}
}

@OptIn(UnstableApi::class)
override fun onViewCreated(
view: View,
savedInstanceState: Bundle?,
) {
super.onViewCreated(view, savedInstanceState)

scene = requireActivity().intent.getParcelableExtra(DetailsActivity.MOVIE) as Scene?
?: throw RuntimeException()

val position = requireActivity().intent.getLongExtra(VideoDetailsFragment.POSITION_ARG, -1)
Log.d(TAG, "scene=${scene.id}, ${VideoDetailsFragment.POSITION_ARG}=$position")

videoView = view.findViewById(R.id.video_view)
videoView.requestFocus()
videoView.controllerShowTimeoutMs = 2000
videoView.hideController()

val mFocusedZoom =
requireContext().resources.getFraction(
androidx.leanback.R.fraction.lb_focus_zoom_factor_large,
1,
1,
)
val onFocusChangeListener =
View.OnFocusChangeListener { v, hasFocus ->
val zoom = if (hasFocus) mFocusedZoom else 1f
v.animate().scaleX(zoom).scaleY(zoom).setDuration(150.toLong()).start()

if (hasFocus) {
v.setBackgroundColor(
ContextCompat.getColor(
requireContext(),
R.color.selected_background,
),
)
} else {
v.setBackgroundColor(
ContextCompat.getColor(
requireContext(),
android.R.color.transparent,
),
)
}
}

val buttons =
listOf(
androidx.media3.ui.R.id.exo_rew_with_amount,
androidx.media3.ui.R.id.exo_ffwd_with_amount,
androidx.media3.ui.R.id.exo_settings,
androidx.media3.ui.R.id.exo_prev,
androidx.media3.ui.R.id.exo_play_pause,
androidx.media3.ui.R.id.exo_next,
)
buttons.forEach {
view.findViewById<View>(it)?.onFocusChangeListener = onFocusChangeListener
}

// val timeBar: DefaultTimeBar =
// view.findViewById(androidx.media3.ui.R.id.exo_progress)
}

@OptIn(UnstableApi::class)
override fun onStart() {
super.onStart()
if (Util.SDK_INT > 23) {
val position =
requireActivity().intent.getLongExtra(VideoDetailsFragment.POSITION_ARG, -1)
initializePlayer(position)
}
}

@OptIn(UnstableApi::class)
override fun onResume() {
super.onResume()
// hideSystemUi()
if ((Util.SDK_INT <= 23 || player == null)) {
val position =
requireActivity().intent.getLongExtra(VideoDetailsFragment.POSITION_ARG, -1)
initializePlayer(position)
}
}

@OptIn(UnstableApi::class)
override fun onPause() {
super.onPause()
if (Util.SDK_INT <= 23) {
releasePlayer()
}
}

@OptIn(UnstableApi::class)
override fun onStop() {
super.onStop()
if (Util.SDK_INT > 23) {
releasePlayer()
}
}

companion object {
const val TAG = "PlaybackExoFragment"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,15 @@ import kotlin.time.DurationUnit
import kotlin.time.toDuration

/** Handles video playback with media controls. */
class PlaybackVideoFragment : VideoSupportFragment() {
class PlaybackVideoFragment : VideoSupportFragment(), PlaybackActivity.StashVideoPlayer {
private lateinit var mTransportControlGlue: BasicTransportControlsGlue
private lateinit var playerAdapter: BasicMediaPlayerAdapter

val currentVideoPosition get() = playerAdapter.currentPosition
override val currentVideoPosition get() = playerAdapter.currentPosition

override fun hideControlsIfVisible(): Boolean {
return false
}

override fun onViewCreated(
view: View,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import androidx.preference.Preference
import androidx.preference.PreferenceDialogFragmentCompat
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreference
import com.github.damontecres.stashapp.util.MutationEngine
import com.github.damontecres.stashapp.util.testStashConnection
import kotlinx.coroutines.CoroutineExceptionHandler
Expand Down Expand Up @@ -129,6 +130,18 @@ class SettingsFragment : LeanbackSettingsFragmentCompat() {
}
true
}

val playerChoice = findPreference<SwitchPreference>("playerChoice")
playerChoice?.summaryProvider =
object : Preference.SummaryProvider<SwitchPreference> {
override fun provideSummary(preference: SwitchPreference): CharSequence {
return if (preference.isChecked) {
"Using ExoPlayer (recommended)"
} else {
"Using default player"
}
}
}
}

companion object {
Expand Down
Loading

0 comments on commit 212a755

Please sign in to comment.