LibreTube/app/src/main/java/com/github/libretube/services/VideoOnlinePlayerService.kt

174 lines
6.9 KiB
Kotlin

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<Streams>(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<SubtitleConfiguration> = 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)
}
}