package com.github.libretube.services import android.net.Uri import android.os.Bundle import androidx.annotation.OptIn import androidx.core.net.toUri import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem.SubtitleConfiguration import androidx.media3.common.MimeTypes import androidx.media3.common.util.UnstableApi import androidx.media3.datasource.cronet.CronetDataSource import androidx.media3.exoplayer.hls.HlsMediaSource import com.github.libretube.R import com.github.libretube.api.CronetHelper import com.github.libretube.api.obj.Streams import com.github.libretube.api.obj.Subtitle import com.github.libretube.constants.IntentData import com.github.libretube.constants.PreferenceKeys import com.github.libretube.enums.PlayerCommand import com.github.libretube.extensions.parcelable import com.github.libretube.extensions.setMetadata import com.github.libretube.extensions.toastFromMainThread import com.github.libretube.extensions.updateParameters import com.github.libretube.helpers.PlayerHelper import com.github.libretube.helpers.PreferenceHelper import com.github.libretube.helpers.ProxyHelper import com.github.libretube.ui.activities.MainActivity import com.github.libretube.util.YoutubeHlsPlaylistParser import java.util.concurrent.Executors @OptIn(UnstableApi::class) class VideoOnlinePlayerService : AbstractPlayerService() { override val isOfflinePlayer: Boolean = false override val isAudioOnlyPlayer: Boolean = false override val intentActivity: Class<*> = MainActivity::class.java private val cronetDataSourceFactory = CronetDataSource.Factory( CronetHelper.cronetEngine, Executors.newCachedThreadPool() ) private lateinit var streams: Streams override suspend fun onServiceCreated(args: Bundle) { this.streams = args.parcelable(IntentData.streams) ?: return startPlayback() } override suspend fun startPlayback() = Unit override fun runPlayerCommand(args: Bundle) { when { args.containsKey(PlayerCommand.START_PLAYBACK.name) -> setStreamSource() args.containsKey(PlayerCommand.SKIP_SILENCE.name) -> exoPlayer?.skipSilenceEnabled = args.getBoolean(PlayerCommand.SKIP_SILENCE.name) args.containsKey(PlayerCommand.SET_VIDEO_TRACK_TYPE_DISABLED.name) -> trackSelector?.updateParameters { setTrackTypeDisabled( C.TRACK_TYPE_VIDEO, args.getBoolean(PlayerCommand.SET_VIDEO_TRACK_TYPE_DISABLED.name) ) } args.containsKey(PlayerCommand.SET_AUDIO_ROLE_FLAGS.name) -> { trackSelector?.updateParameters { setPreferredAudioRoleFlags(args.getInt(PlayerCommand.SET_AUDIO_ROLE_FLAGS.name)) } } args.containsKey(PlayerCommand.SET_AUDIO_LANGUAGE.name) -> { trackSelector?.updateParameters { setPreferredAudioLanguage(args.getString(PlayerCommand.SET_AUDIO_LANGUAGE.name)) } } args.containsKey(PlayerCommand.SET_RESOLUTION.name) -> { trackSelector?.updateParameters { val resolution = args.getInt(PlayerCommand.SET_RESOLUTION.name) setMinVideoSize(Int.MIN_VALUE, resolution) setMaxVideoSize(Int.MAX_VALUE, resolution) } } args.containsKey(PlayerCommand.SET_SUBTITLE.name) -> { updateCurrentSubtitle(args.parcelable(PlayerCommand.SET_SUBTITLE.name)) } } } private fun setStreamSource() { if (!this::streams.isInitialized) return val (uri, mimeType) = when { // LBRY HLS PreferenceHelper.getBoolean( PreferenceKeys.LBRY_HLS, false ) && streams.videoStreams.any { it.quality.orEmpty().contains("LBRY HLS") } -> { val lbryHlsUrl = streams.videoStreams.first { it.quality!!.contains("LBRY HLS") }.url!! lbryHlsUrl.toUri() to MimeTypes.APPLICATION_M3U8 } // DASH !PlayerHelper.useHlsOverDash && streams.videoStreams.isNotEmpty() -> { // only use the dash manifest generated by YT if either it's a livestream or no other source is available val dashUri = if (streams.isLive && streams.dash != null) { ProxyHelper.unwrapStreamUrl( streams.dash!! ).toUri() } else { // skip LBRY urls when checking whether the stream source is usable PlayerHelper.createDashSource(streams, this) } dashUri to MimeTypes.APPLICATION_MPD } // HLS streams.hls != null -> { val hlsMediaSourceFactory = HlsMediaSource.Factory(cronetDataSourceFactory) .setPlaylistParserFactory(YoutubeHlsPlaylistParser.Factory()) val mediaSource = hlsMediaSourceFactory.createMediaSource( createMediaItem( ProxyHelper.unwrapStreamUrl(streams.hls!!).toUri(), MimeTypes.APPLICATION_M3U8 ) ) exoPlayer?.setMediaSource(mediaSource) return } // NO STREAM FOUND else -> { toastFromMainThread(R.string.unknown_error) return } } setMediaSource(uri, mimeType) } private fun getSubtitleConfigs(): List = streams.subtitles.map { val roleFlags = getSubtitleRoleFlags(it) SubtitleConfiguration.Builder(it.url!!.toUri()) .setRoleFlags(roleFlags) .setLanguage(it.code) .setMimeType(it.mimeType).build() } private fun createMediaItem(uri: Uri, mimeType: String) = MediaItem.Builder() .setUri(uri) .setMimeType(mimeType) .setSubtitleConfigurations(getSubtitleConfigs()) .setMetadata(streams) .build() private fun setMediaSource(uri: Uri, mimeType: String) { val mediaItem = createMediaItem(uri, mimeType) exoPlayer?.setMediaItem(mediaItem) } private fun getSubtitleRoleFlags(subtitle: Subtitle?): Int { return if (subtitle?.autoGenerated != true) { C.ROLE_FLAG_CAPTION } else { PlayerHelper.ROLE_FLAG_AUTO_GEN_SUBTITLE } } private fun updateCurrentSubtitle(subtitle: Subtitle?) = trackSelector?.updateParameters { val roleFlags = if (subtitle?.code != null) getSubtitleRoleFlags(subtitle) else 0 setPreferredTextRoleFlags(roleFlags) setPreferredTextLanguage(subtitle?.code) } }