LibreTube/app/src/main/java/com/github/libretube/helpers/PlayerHelper.kt

477 lines
15 KiB
Kotlin
Raw Normal View History

package com.github.libretube.helpers
2022-07-17 01:01:15 +05:30
2023-01-09 21:38:05 +05:30
import android.app.Activity
import android.app.PendingIntent
2022-07-17 01:01:15 +05:30
import android.content.Context
2023-01-09 21:38:05 +05:30
import android.content.Intent
import android.content.pm.ActivityInfo
import android.net.Uri
2023-06-19 16:26:53 +05:30
import android.text.format.DateUtils
import android.util.Base64
2022-07-17 01:01:15 +05:30
import android.view.accessibility.CaptioningManager
import android.widget.Toast
2023-01-09 21:38:05 +05:30
import androidx.annotation.StringRes
import androidx.core.app.PendingIntentCompat
2023-02-03 17:09:08 +05:30
import androidx.core.app.RemoteActionCompat
2023-03-31 06:09:38 +05:30
import androidx.core.content.getSystemService
2023-02-03 17:09:08 +05:30
import androidx.core.graphics.drawable.IconCompat
import androidx.core.net.toUri
2023-05-15 20:51:49 +05:30
import androidx.media3.common.AudioAttributes
import androidx.media3.common.C
import androidx.media3.common.PlaybackParameters
import androidx.media3.exoplayer.DefaultLoadControl
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.LoadControl
import androidx.media3.ui.CaptionStyleCompat
2023-01-09 21:38:05 +05:30
import com.github.libretube.R
2023-06-19 16:26:53 +05:30
import com.github.libretube.api.obj.ChapterSegment
import com.github.libretube.api.obj.Segment
import com.github.libretube.api.obj.Streams
2022-09-20 23:30:51 +05:30
import com.github.libretube.constants.PreferenceKeys
2023-01-09 21:38:05 +05:30
import com.github.libretube.enums.PlayerEvent
import com.github.libretube.enums.SbSkipOptions
2023-06-19 16:26:53 +05:30
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlin.math.absoluteValue
import kotlin.math.roundToInt
2022-07-17 01:01:15 +05:30
object PlayerHelper {
2023-01-09 21:38:05 +05:30
private const val ACTION_MEDIA_CONTROL = "media_control"
const val CONTROL_TYPE = "control_type"
private val SPONSOR_CATEGORIES: Array<String> =
arrayOf(
"intro",
"selfpromo",
"interaction",
"sponsor",
"outro",
"filler",
"music_offtopic",
"preview")
2023-01-09 21:38:05 +05:30
2023-01-08 20:36:29 +05:30
/**
* Create a base64 encoded DASH stream manifest
2023-01-08 20:36:29 +05:30
*/
fun createDashSource(streams: Streams, context: Context, audioOnly: Boolean = false): Uri {
val manifest = DashHelper.createManifest(
streams,
DisplayHelper.supportsHdr(context),
audioOnly
2023-01-08 20:36:29 +05:30
)
// encode to base64
val encoded = Base64.encodeToString(manifest.toByteArray(), Base64.DEFAULT)
return "data:application/dash+xml;charset=utf-8;base64,$encoded".toUri()
2022-07-17 01:01:15 +05:30
}
2023-01-08 20:36:29 +05:30
/**
* Get the system's default captions style
2023-01-08 20:36:29 +05:30
*/
2023-05-15 20:51:49 +05:30
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
2022-07-17 01:01:15 +05:30
fun getCaptionStyle(context: Context): CaptionStyleCompat {
2023-03-31 06:09:38 +05:30
val captioningManager = context.getSystemService<CaptioningManager>()!!
2022-07-17 01:01:15 +05:30
return if (!captioningManager.isEnabled) {
// system captions are disabled, using android default captions style
CaptionStyleCompat.DEFAULT
} else {
// system captions are enabled
CaptionStyleCompat.createFromCaptionStyle(captioningManager.userStyle)
}
}
2022-08-02 14:47:15 +05:30
/**
* get the categories for sponsorBlock
*/
fun getSponsorBlockCategories(): MutableMap<String, SbSkipOptions> {
val categories: MutableMap<String, SbSkipOptions> = mutableMapOf()
for (cat in SPONSOR_CATEGORIES){
val state = PreferenceHelper.getString(cat + "_category_key", "off").uppercase()
if (SbSkipOptions.valueOf(state) != SbSkipOptions.OFF){
categories[cat] = SbSkipOptions.valueOf(state)
}
2022-08-02 14:47:15 +05:30
}
return categories
}
2023-03-24 20:32:56 +05:30
fun getOrientation(videoWidth: Int, videoHeight: Int): Int {
val fullscreenOrientationPref = PreferenceHelper.getString(
PreferenceKeys.FULLSCREEN_ORIENTATION,
"ratio",
)
return when (fullscreenOrientationPref) {
"ratio" -> {
// probably a youtube shorts video
2023-03-24 20:32:56 +05:30
if (videoHeight > videoWidth) {
ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
} // a video with normal aspect ratio
else {
ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
}
}
"auto" -> ActivityInfo.SCREEN_ORIENTATION_SENSOR
"landscape" -> ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
"portrait" -> ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
else -> ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
}
}
2022-10-07 23:00:59 +05:30
val autoRotationEnabled: Boolean
get() = PreferenceHelper.getBoolean(
PreferenceKeys.AUTO_FULLSCREEN,
false,
2022-10-07 23:00:59 +05:30
)
val relatedStreamsEnabled: Boolean
get() = PreferenceHelper.getBoolean(
PreferenceKeys.RELATED_STREAMS,
true,
2022-10-07 23:00:59 +05:30
)
val pausePlayerOnScreenOffEnabled: Boolean
get() = PreferenceHelper.getBoolean(
PreferenceKeys.PAUSE_ON_SCREEN_OFF,
false,
2022-10-07 23:00:59 +05:30
)
private val watchPositionsPref: String
get() = PreferenceHelper.getString(
PreferenceKeys.WATCH_POSITIONS,
"always",
2022-10-07 23:00:59 +05:30
)
val watchPositionsVideo: Boolean
get() = watchPositionsPref in listOf("always", "videos")
val watchPositionsAudio: Boolean
get() = watchPositionsPref == "always"
2022-10-07 23:00:59 +05:30
val watchHistoryEnabled: Boolean
get() = PreferenceHelper.getBoolean(
PreferenceKeys.WATCH_HISTORY_TOGGLE,
true,
2022-10-07 23:00:59 +05:30
)
val useSystemCaptionStyle: Boolean
get() = PreferenceHelper.getBoolean(
PreferenceKeys.SYSTEM_CAPTION_STYLE,
true,
2022-10-07 23:00:59 +05:30
)
private val bufferingGoal: Int
2022-10-07 23:00:59 +05:30
get() = PreferenceHelper.getString(
PreferenceKeys.BUFFERING_GOAL,
"50",
2022-10-07 23:00:59 +05:30
).toInt() * 1000
val sponsorBlockEnabled: Boolean
get() = PreferenceHelper.getBoolean(
"sb_enabled_key",
true,
2022-10-07 23:00:59 +05:30
)
private val sponsorBlockNotifications: Boolean
2022-10-07 23:00:59 +05:30
get() = PreferenceHelper.getBoolean(
"sb_notifications_key",
true,
2022-10-07 23:00:59 +05:30
)
2022-12-22 15:47:34 +05:30
val defaultSubtitleCode: String?
2022-10-07 23:00:59 +05:30
get() {
val code = PreferenceHelper.getString(
PreferenceKeys.DEFAULT_SUBTITLE,
"",
2022-10-07 23:00:59 +05:30
)
2022-12-22 15:47:34 +05:30
if (code == "") return null
2022-10-07 23:00:59 +05:30
if (code.contains("-")) {
return code.split("-")[0]
}
return code
}
val skipButtonsEnabled: Boolean
get() = PreferenceHelper.getBoolean(
PreferenceKeys.SKIP_BUTTONS,
false,
2022-10-07 23:00:59 +05:30
)
2022-10-19 23:23:18 +05:30
val pipEnabled: Boolean
get() = PreferenceHelper.getBoolean(
PreferenceKeys.PICTURE_IN_PICTURE,
true,
2022-10-19 23:23:18 +05:30
)
2022-10-07 23:00:59 +05:30
val autoPlayEnabled: Boolean
get() = PreferenceHelper.getBoolean(
PreferenceKeys.AUTO_PLAY,
true,
)
val autoPlayCountdown: Boolean
get() = PreferenceHelper.getBoolean(
PreferenceKeys.AUTOPLAY_COUNTDOWN,
false,
)
val seekIncrement: Long
get() = PreferenceHelper.getString(
PreferenceKeys.SEEK_INCREMENT,
"10.0",
).toFloat()
.roundToInt()
.toLong() * 1000
private val playbackSpeed: Float
get() = PreferenceHelper.getString(
PreferenceKeys.PLAYBACK_SPEED,
"1",
).replace("F", "").toFloat()
private val backgroundSpeed: Float
get() = when (PreferenceHelper.getBoolean(PreferenceKeys.CUSTOM_PLAYBACK_SPEED, false)) {
true -> PreferenceHelper.getString(PreferenceKeys.BACKGROUND_PLAYBACK_SPEED, "1").toFloat()
else -> playbackSpeed
}
val resizeModePref: String
get() = PreferenceHelper.getString(
PreferenceKeys.PLAYER_RESIZE_MODE,
"fit",
)
2022-11-12 23:34:40 +05:30
val alternativeVideoLayout: Boolean
get() = PreferenceHelper.getBoolean(
PreferenceKeys.ALTERNATIVE_PLAYER_LAYOUT,
false,
2022-11-12 23:34:40 +05:30
)
val autoInsertRelatedVideos: Boolean
get() = PreferenceHelper.getBoolean(
PreferenceKeys.QUEUE_AUTO_INSERT_RELATED,
true,
)
val swipeGestureEnabled: Boolean
get() = PreferenceHelper.getBoolean(
PreferenceKeys.PLAYER_SWIPE_CONTROLS,
true,
)
val fullscreenGesturesEnabled: Boolean
get() = PreferenceHelper.getBoolean(
PreferenceKeys.FULLSCREEN_GESTURES,
false,
)
val pinchGestureEnabled: Boolean
get() = PreferenceHelper.getBoolean(
PreferenceKeys.PLAYER_PINCH_CONTROL,
true,
)
val captionsTextSize: Float
get() = PreferenceHelper.getString(
PreferenceKeys.CAPTIONS_SIZE,
"18",
).toFloat()
2022-11-27 23:11:34 +05:30
val doubleTapToSeek: Boolean
get() = PreferenceHelper.getBoolean(
PreferenceKeys.DOUBLE_TAP_TO_SEEK,
true,
2022-11-27 23:11:34 +05:30
)
2022-12-27 23:18:09 +05:30
val pauseOnQuit: Boolean
get() = PreferenceHelper.getBoolean(
PreferenceKeys.PAUSE_ON_QUIT,
false,
2022-12-27 23:18:09 +05:30
)
private val alternativePiPControls: Boolean
get() = PreferenceHelper.getBoolean(
PreferenceKeys.ALTERNATIVE_PIP_CONTROLS,
false,
)
2023-01-21 16:37:28 +05:30
private val skipSilence: Boolean
get() = PreferenceHelper.getBoolean(
PreferenceKeys.SKIP_SILENCE,
false,
2023-01-21 16:37:28 +05:30
)
2023-02-08 14:11:59 +05:30
val enabledVideoCodecs: String
get() = PreferenceHelper.getString(
PreferenceKeys.ENABLED_VIDEO_CODECS,
"all",
2023-02-08 14:11:59 +05:30
)
2022-10-07 23:00:59 +05:30
fun getDefaultResolution(context: Context): String {
val prefKey = if (NetworkHelper.isNetworkMetered(context)) {
PreferenceKeys.DEFAULT_RESOLUTION_MOBILE
2022-10-07 23:00:59 +05:30
} else {
PreferenceKeys.DEFAULT_RESOLUTION
2022-10-07 23:00:59 +05:30
}
return PreferenceHelper.getString(prefKey, "")
2022-10-07 23:00:59 +05:30
}
2023-01-09 21:38:05 +05:30
fun getIntentActon(context: Context): String {
return context.packageName + "." + ACTION_MEDIA_CONTROL
}
private fun getPendingIntent(activity: Activity, code: Int): PendingIntent {
val intent = Intent(getIntentActon(activity)).putExtra(CONTROL_TYPE, code)
return PendingIntentCompat.getBroadcast(activity, code, intent, 0, false)
2023-01-09 21:38:05 +05:30
}
private fun getRemoteAction(
activity: Activity,
id: Int,
@StringRes title: Int,
event: PlayerEvent,
2023-02-03 17:09:08 +05:30
): RemoteActionCompat {
2023-01-09 21:38:05 +05:30
val text = activity.getString(title)
2023-02-03 17:09:08 +05:30
return RemoteActionCompat(
IconCompat.createWithResource(activity, id),
2023-01-09 21:38:05 +05:30
text,
text,
getPendingIntent(activity, event.value),
2023-01-09 21:38:05 +05:30
)
}
/**
* Create controls to use in the PiP window
*/
2023-02-03 17:09:08 +05:30
fun getPiPModeActions(activity: Activity, isPlaying: Boolean): List<RemoteActionCompat> {
2023-01-15 17:56:16 +05:30
val audioModeAction = getRemoteAction(
activity,
R.drawable.ic_headphones,
R.string.background_mode,
PlayerEvent.Background,
2023-01-09 21:38:05 +05:30
)
2023-01-15 17:56:16 +05:30
val rewindAction = getRemoteAction(
activity,
R.drawable.ic_rewind,
R.string.rewind,
PlayerEvent.Rewind,
2023-01-09 21:38:05 +05:30
)
2023-01-15 17:56:16 +05:30
val playPauseAction = getRemoteAction(
activity,
if (isPlaying) R.drawable.ic_pause else R.drawable.ic_play,
R.string.pause,
if (isPlaying) PlayerEvent.Pause else PlayerEvent.Play,
2023-01-15 17:56:16 +05:30
)
val skipNextAction = getRemoteAction(
activity,
R.drawable.ic_next,
R.string.play_next,
PlayerEvent.Next,
2023-01-09 21:38:05 +05:30
)
2023-01-15 17:56:16 +05:30
val forwardAction = getRemoteAction(
activity,
R.drawable.ic_forward,
R.string.forward,
PlayerEvent.Forward,
2023-01-15 17:56:16 +05:30
)
2023-02-03 17:09:08 +05:30
return if (alternativePiPControls) {
listOf(audioModeAction, playPauseAction, skipNextAction)
2023-01-15 17:56:16 +05:30
} else {
2023-02-03 17:09:08 +05:30
listOf(rewindAction, playPauseAction, forwardAction)
2023-01-15 17:56:16 +05:30
}
2023-01-09 21:38:05 +05:30
}
/**
* Get the audio attributes to use for the player
*/
fun getAudioAttributes(): AudioAttributes {
return AudioAttributes.Builder()
.setUsage(C.USAGE_MEDIA)
.setContentType(C.AUDIO_CONTENT_TYPE_MOVIE)
.build()
}
/**
* Get the load controls for the player (buffering, etc)
*/
2023-05-15 20:51:49 +05:30
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
fun getLoadControl(): LoadControl {
return DefaultLoadControl.Builder()
// cache the last three minutes
.setBackBuffer(1000 * 60 * 3, true)
.setBufferDurationsMs(
1000 * 10, // exo default is 50s
bufferingGoal,
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS,
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS,
)
.build()
}
2023-01-21 16:37:28 +05:30
/**
* Load playback parameters such as speed and skip silence
*/
2023-05-15 20:51:49 +05:30
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
fun ExoPlayer.loadPlaybackParams(isBackgroundMode: Boolean = false): ExoPlayer {
2023-01-21 16:37:28 +05:30
skipSilenceEnabled = skipSilence
val speed = if (isBackgroundMode) backgroundSpeed else playbackSpeed
playbackParameters = PlaybackParameters(speed, 1.0f)
2023-01-21 16:37:28 +05:30
return this
}
/**
* Check for SponsorBlock segments matching the current player position
* @param context A main dispatcher context
* @param segments List of the SponsorBlock segments
* @param skipManually Whether the event gets handled by the function caller
* @return If segment found and [skipManually] is true, the end position of the segment in ms, otherwise null
*/
fun ExoPlayer.checkForSegments(
context: Context,
segments: List<Segment>,
sponsorBlockConfig: MutableMap<String, SbSkipOptions>,
): Long? {
for (segment in segments) {
val segmentStart = (segment.segment[0] * 1000f).toLong()
val segmentEnd = (segment.segment[1] * 1000f).toLong()
// avoid seeking to the same segment multiple times, e.g. when the SB segment is at the end of the video
if ((duration - currentPosition).absoluteValue < 500) continue
if (currentPosition in segmentStart until segmentEnd) {
2023-06-19 16:26:53 +05:30
if (sponsorBlockConfig[segment.category] == SbSkipOptions.AUTOMATIC) {
if (sponsorBlockNotifications) {
runCatching {
Toast.makeText(context, R.string.segment_skipped, Toast.LENGTH_SHORT)
.show()
}
}
seekTo(segmentEnd)
} else {
return segmentEnd
}
}
}
return null
}
2023-06-19 16:26:53 +05:30
/**
* Show a dialog with the chapters provided, even if the list is empty
*/
fun showChaptersDialog(context: Context, chapters: List<ChapterSegment>, player: ExoPlayer) {
val titles = chapters.map { chapter ->
"(${DateUtils.formatElapsedTime(chapter.start)}) ${chapter.title}"
}
MaterialAlertDialogBuilder(context)
.setTitle(R.string.chapters)
.setItems(titles.toTypedArray()) { _, index ->
player.seekTo(chapters[index].start * 1000)
}
.show()
}
2022-07-17 02:19:32 +05:30
}