package com.github.libretube.helpers import android.app.Activity import android.app.PendingIntent import android.app.RemoteAction import android.content.Context import android.content.Intent import android.content.pm.ActivityInfo import android.graphics.drawable.Icon import android.os.Build import android.view.accessibility.CaptioningManager import androidx.annotation.RequiresApi import androidx.annotation.StringRes import com.github.libretube.R import com.github.libretube.api.obj.PipedStream import com.github.libretube.constants.PreferenceKeys import com.github.libretube.enums.AudioQuality import com.github.libretube.enums.PlayerEvent import com.google.android.exoplayer2.C import com.google.android.exoplayer2.DefaultLoadControl import com.google.android.exoplayer2.ExoPlayer import com.google.android.exoplayer2.LoadControl import com.google.android.exoplayer2.PlaybackParameters import com.google.android.exoplayer2.audio.AudioAttributes import com.google.android.exoplayer2.ui.CaptionStyleCompat import com.google.android.exoplayer2.video.VideoSize import kotlin.math.roundToInt object PlayerHelper { private const val ACTION_MEDIA_CONTROL = "media_control" const val CONTROL_TYPE = "control_type" /** * Get the audio source following the users preferences */ fun getAudioSource(context: Context, audios: List): String { val audioFormat = PreferenceHelper.getString(PreferenceKeys.PLAYER_AUDIO_FORMAT, "all") val audioPrefKey = if (NetworkHelper.isNetworkMetered(context)) { PreferenceKeys.PLAYER_AUDIO_QUALITY_MOBILE } else { PreferenceKeys.PLAYER_AUDIO_QUALITY } val audioQuality = PreferenceHelper.getString(audioPrefKey, "best") val filteredAudios = audios.filter { val audioMimeType = "audio/$audioFormat" it.mimeType != audioMimeType || audioFormat == "all" } return getBitRate( filteredAudios, if (audioQuality == "best") AudioQuality.BEST else AudioQuality.WORST ) } /** * Get the best or worst bitrate from a list of audio streams * @param audios list of the audio streams * @param quality Whether to use the best or worst quality available * @return Url of the audio source */ private fun getBitRate(audios: List, quality: AudioQuality): String { val filteredAudios = audios.filter { it.bitrate != null }.sortedBy { it.bitrate } return when (quality) { AudioQuality.BEST -> filteredAudios.last() AudioQuality.WORST -> filteredAudios.first() }.url!! } // get the system default caption style fun getCaptionStyle(context: Context): CaptionStyleCompat { val captioningManager = context.getSystemService(Context.CAPTIONING_SERVICE) as CaptioningManager return if (!captioningManager.isEnabled) { // system captions are disabled, using android default captions style CaptionStyleCompat.DEFAULT } else { // system captions are enabled CaptionStyleCompat.createFromCaptionStyle(captioningManager.userStyle) } } /** * get the categories for sponsorBlock */ fun getSponsorBlockCategories(): ArrayList { val categories: ArrayList = arrayListOf() if (PreferenceHelper.getBoolean( "intro_category_key", false ) ) { categories.add("intro") } if (PreferenceHelper.getBoolean( "selfpromo_category_key", false ) ) { categories.add("selfpromo") } if (PreferenceHelper.getBoolean( "interaction_category_key", false ) ) { categories.add("interaction") } if (PreferenceHelper.getBoolean( "sponsors_category_key", true ) ) { categories.add("sponsor") } if (PreferenceHelper.getBoolean( "outro_category_key", false ) ) { categories.add("outro") } if (PreferenceHelper.getBoolean( "filler_category_key", false ) ) { categories.add("filler") } if (PreferenceHelper.getBoolean( "music_offtopic_category_key", false ) ) { categories.add("music_offtopic") } if (PreferenceHelper.getBoolean( "preview_category_key", false ) ) { categories.add("preview") } return categories } fun getOrientation(videoSize: VideoSize): Int { val fullscreenOrientationPref = PreferenceHelper.getString( PreferenceKeys.FULLSCREEN_ORIENTATION, "ratio" ) return when (fullscreenOrientationPref) { "ratio" -> { // probably a youtube shorts video if (videoSize.height > videoSize.width) { 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 } } val autoRotationEnabled: Boolean get() = PreferenceHelper.getBoolean( PreferenceKeys.AUTO_FULLSCREEN, false ) val relatedStreamsEnabled: Boolean get() = PreferenceHelper.getBoolean( PreferenceKeys.RELATED_STREAMS, true ) val pausePlayerOnScreenOffEnabled: Boolean get() = PreferenceHelper.getBoolean( PreferenceKeys.PAUSE_ON_SCREEN_OFF, false ) private val watchPositionsPref: String get() = PreferenceHelper.getString( PreferenceKeys.WATCH_POSITIONS, "always" ) val watchPositionsVideo: Boolean get() = watchPositionsPref in listOf("always", "videos") val watchPositionsAudio: Boolean get() = watchPositionsPref == "always" val watchHistoryEnabled: Boolean get() = PreferenceHelper.getBoolean( PreferenceKeys.WATCH_HISTORY_TOGGLE, true ) val useSystemCaptionStyle: Boolean get() = PreferenceHelper.getBoolean( PreferenceKeys.SYSTEM_CAPTION_STYLE, true ) val videoFormatPreference: String get() = PreferenceHelper.getString( PreferenceKeys.PLAYER_VIDEO_FORMAT, "webm" ) private val bufferingGoal: Int get() = PreferenceHelper.getString( PreferenceKeys.BUFFERING_GOAL, "50" ).toInt() * 1000 val sponsorBlockEnabled: Boolean get() = PreferenceHelper.getBoolean( "sb_enabled_key", true ) val sponsorBlockNotifications: Boolean get() = PreferenceHelper.getBoolean( "sb_notifications_key", true ) val defaultSubtitleCode: String? get() { val code = PreferenceHelper.getString( PreferenceKeys.DEFAULT_SUBTITLE, "" ) if (code == "") return null if (code.contains("-")) { return code.split("-")[0] } return code } val skipButtonsEnabled: Boolean get() = PreferenceHelper.getBoolean( PreferenceKeys.SKIP_BUTTONS, false ) val pipEnabled: Boolean get() = PreferenceHelper.getBoolean( PreferenceKeys.PICTURE_IN_PICTURE, true ) val skipSegmentsManually: Boolean get() = PreferenceHelper.getBoolean( PreferenceKeys.SB_SKIP_MANUALLY, false ) val autoPlayEnabled: Boolean get() = PreferenceHelper.getBoolean( PreferenceKeys.AUTO_PLAY, true ) val seekIncrement: Long get() = PreferenceHelper.getString( PreferenceKeys.SEEK_INCREMENT, "10.0" ).toFloat() .roundToInt() .toLong() * 1000 private val playbackSpeed: String get() = PreferenceHelper.getString( PreferenceKeys.PLAYBACK_SPEED, "1" ).replace("F", "") val resizeModePref: String get() = PreferenceHelper.getString( PreferenceKeys.PLAYER_RESIZE_MODE, "fit" ) val alternativeVideoLayout: Boolean get() = PreferenceHelper.getBoolean( PreferenceKeys.ALTERNATIVE_PLAYER_LAYOUT, false ) val autoInsertRelatedVideos: Boolean get() = PreferenceHelper.getBoolean( PreferenceKeys.QUEUE_AUTO_INSERT_RELATED, true ) val swipeGestureEnabled: Boolean get() = PreferenceHelper.getBoolean( PreferenceKeys.PLAYER_SWIPE_CONTROLS, true ) val pinchGestureEnabled: Boolean get() = PreferenceHelper.getBoolean( PreferenceKeys.PLAYER_PINCH_CONTROL, true ) val captionsTextSize: Float get() = PreferenceHelper.getString( PreferenceKeys.CAPTIONS_SIZE, "18" ).toFloat() val doubleTapToSeek: Boolean get() = PreferenceHelper.getBoolean( PreferenceKeys.DOUBLE_TAP_TO_SEEK, true ) val pauseOnQuit: Boolean get() = PreferenceHelper.getBoolean( PreferenceKeys.PAUSE_ON_QUIT, false ) private val alternativePiPControls: Boolean get() = PreferenceHelper.getBoolean( PreferenceKeys.ALTERNATIVE_PIP_CONTROLS, false ) private val skipSilence: Boolean get() = PreferenceHelper.getBoolean( PreferenceKeys.SKIP_SILENCE, false ) val enabledVideoCodecs: String get() = PreferenceHelper.getString( PreferenceKeys.ENABLED_VIDEO_CODECS, "all" ) fun getDefaultResolution(context: Context): String { val prefKey = if (NetworkHelper.isNetworkMetered(context)) { PreferenceKeys.DEFAULT_RESOLUTION_MOBILE } else { PreferenceKeys.DEFAULT_RESOLUTION } return PreferenceHelper.getString(prefKey, "") } fun getIntentActon(context: Context): String { return context.packageName + "." + ACTION_MEDIA_CONTROL } @RequiresApi(Build.VERSION_CODES.O) private fun getPendingIntent(activity: Activity, code: Int): PendingIntent { return PendingIntent.getBroadcast( activity, code, Intent(getIntentActon(activity)).putExtra(CONTROL_TYPE, code), PendingIntent.FLAG_IMMUTABLE ) } @RequiresApi(Build.VERSION_CODES.O) private fun getRemoteAction( activity: Activity, id: Int, @StringRes title: Int, event: PlayerEvent ): RemoteAction { val text = activity.getString(title) return RemoteAction( Icon.createWithResource(activity, id), text, text, getPendingIntent(activity, event.value) ) } /** * Create controls to use in the PiP window */ @RequiresApi(Build.VERSION_CODES.O) fun getPiPModeActions(activity: Activity, isPlaying: Boolean, isOfflinePlayer: Boolean = false): ArrayList { val audioModeAction = getRemoteAction( activity, R.drawable.ic_headphones, R.string.background_mode, PlayerEvent.Background ) val rewindAction = getRemoteAction( activity, R.drawable.ic_rewind, R.string.rewind, PlayerEvent.Rewind ) 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 ) val skipNextAction = getRemoteAction( activity, R.drawable.ic_next, R.string.play_next, PlayerEvent.Next ) val forwardAction = getRemoteAction( activity, R.drawable.ic_forward, R.string.forward, PlayerEvent.Forward ) return if ( !isOfflinePlayer && alternativePiPControls ) { arrayListOf(audioModeAction, playPauseAction, skipNextAction) } else { arrayListOf(rewindAction, playPauseAction, forwardAction) } } /** * 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) */ 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() } /** * Load playback parameters such as speed and skip silence */ fun ExoPlayer.loadPlaybackParams(): ExoPlayer { skipSilenceEnabled = skipSilence playbackParameters = PlaybackParameters( playbackSpeed.toFloat(), 1.0f ) return this } }