added drm player support

This commit is contained in:
LagradOst 2023-09-06 20:53:43 +02:00
parent 0839775172
commit 3fe247fb19
2 changed files with 132 additions and 10 deletions

View File

@ -1,5 +1,6 @@
package com.lagradost.cloudstream3.ui.player package com.lagradost.cloudstream3.ui.player
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.Handler import android.os.Handler
@ -7,6 +8,7 @@ import android.os.Looper
import android.util.Log import android.util.Log
import android.util.Rational import android.util.Rational
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.media3.common.C
import androidx.media3.common.C.* import androidx.media3.common.C.*
import androidx.media3.common.Format import androidx.media3.common.Format
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
@ -31,6 +33,10 @@ import androidx.media3.exoplayer.DefaultRenderersFactory
import androidx.media3.exoplayer.DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON import androidx.media3.exoplayer.DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.SeekParameters import androidx.media3.exoplayer.SeekParameters
import androidx.media3.exoplayer.dash.DashMediaSource
import androidx.media3.exoplayer.drm.DefaultDrmSessionManager
import androidx.media3.exoplayer.drm.FrameworkMediaDrm
import androidx.media3.exoplayer.drm.LocalMediaDrmCallback
import androidx.media3.exoplayer.source.ClippingMediaSource import androidx.media3.exoplayer.source.ClippingMediaSource
import androidx.media3.exoplayer.source.ConcatenatingMediaSource import androidx.media3.exoplayer.source.ConcatenatingMediaSource
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
@ -50,6 +56,7 @@ import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData
import com.lagradost.cloudstream3.utils.DrmExtractorLink
import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.EpisodeSkip
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList
@ -58,6 +65,7 @@ import com.lagradost.cloudstream3.utils.ExtractorUri
import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage
import java.io.File import java.io.File
import java.lang.IllegalArgumentException import java.lang.IllegalArgumentException
import java.util.UUID
import javax.net.ssl.HttpsURLConnection import javax.net.ssl.HttpsURLConnection
import javax.net.ssl.SSLContext import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSession import javax.net.ssl.SSLSession
@ -104,7 +112,16 @@ class CS3IPlayer : IPlayer {
* */ * */
data class MediaItemSlice( data class MediaItemSlice(
val mediaItem: MediaItem, val mediaItem: MediaItem,
val durationUs: Long val durationUs: Long,
val drm: DrmMetadata? = null
)
data class DrmMetadata(
val kid: String,
val key: String,
val uuid: UUID,
val kty: String,
val keyRequestParameters: HashMap<String, String>,
) )
override fun getDuration(): Long? = exoPlayer?.duration override fun getDuration(): Long? = exoPlayer?.duration
@ -340,6 +357,7 @@ class CS3IPlayer : IPlayer {
}.flatten() }.flatten()
} }
@SuppressLint("UnsafeOptInUsageError")
private fun Tracks.Group.getFormats(): List<Pair<Format, Int>> { private fun Tracks.Group.getFormats(): List<Pair<Format, Int>> {
return (0 until this.mediaTrackGroup.length).mapNotNull { i -> return (0 until this.mediaTrackGroup.length).mapNotNull { i ->
if (this.isSupported) if (this.isSupported)
@ -368,6 +386,7 @@ class CS3IPlayer : IPlayer {
) )
} }
@SuppressLint("UnsafeOptInUsageError")
override fun getVideoTracks(): CurrentTracks { override fun getVideoTracks(): CurrentTracks {
val allTracks = exoPlayer?.currentTracks?.groups ?: emptyList() val allTracks = exoPlayer?.currentTracks?.groups ?: emptyList()
val videoTracks = allTracks.filter { it.type == TRACK_TYPE_VIDEO } val videoTracks = allTracks.filter { it.type == TRACK_TYPE_VIDEO }
@ -387,6 +406,7 @@ class CS3IPlayer : IPlayer {
/** /**
* @return True if the player should be reloaded * @return True if the player should be reloaded
* */ * */
@SuppressLint("UnsafeOptInUsageError")
override fun setPreferredSubtitles(subtitle: SubtitleData?): Boolean { override fun setPreferredSubtitles(subtitle: SubtitleData?): Boolean {
Log.i(TAG, "setPreferredSubtitles init $subtitle") Log.i(TAG, "setPreferredSubtitles init $subtitle")
currentSubtitles = subtitle currentSubtitles = subtitle
@ -465,6 +485,7 @@ class CS3IPlayer : IPlayer {
} }
} }
@SuppressLint("UnsafeOptInUsageError")
override fun getAspectRatio(): Rational? { override fun getAspectRatio(): Rational? {
return exoPlayer?.videoFormat?.let { format -> return exoPlayer?.videoFormat?.let { format ->
Rational(format.width, format.height) Rational(format.width, format.height)
@ -475,6 +496,7 @@ class CS3IPlayer : IPlayer {
subtitleHelper.setSubStyle(style) subtitleHelper.setSubStyle(style)
} }
@SuppressLint("UnsafeOptInUsageError")
override fun saveData() { override fun saveData() {
Log.i(TAG, "saveData") Log.i(TAG, "saveData")
updatedTime() updatedTime()
@ -548,6 +570,7 @@ class CS3IPlayer : IPlayer {
var requestSubtitleUpdate: (() -> Unit)? = null var requestSubtitleUpdate: (() -> Unit)? = null
@SuppressLint("UnsafeOptInUsageError")
private fun createOnlineSource(headers: Map<String, String>): HttpDataSource.Factory { private fun createOnlineSource(headers: Map<String, String>): HttpDataSource.Factory {
val source = OkHttpDataSource.Factory(app.baseClient).setUserAgent(USER_AGENT) val source = OkHttpDataSource.Factory(app.baseClient).setUserAgent(USER_AGENT)
return source.apply { return source.apply {
@ -555,6 +578,7 @@ class CS3IPlayer : IPlayer {
} }
} }
@SuppressLint("UnsafeOptInUsageError")
private fun createOnlineSource(link: ExtractorLink): HttpDataSource.Factory { private fun createOnlineSource(link: ExtractorLink): HttpDataSource.Factory {
val provider = getApiFromNameNull(link.source) val provider = getApiFromNameNull(link.source)
val interceptor = provider?.getVideoInterceptor(link) val interceptor = provider?.getVideoInterceptor(link)
@ -632,6 +656,7 @@ class CS3IPlayer : IPlayer {
return Pair(subSources, activeSubtitles) return Pair(subSources, activeSubtitles)
}*/ }*/
@SuppressLint("UnsafeOptInUsageError")
private fun getCache(context: Context, cacheSize: Long): SimpleCache? { private fun getCache(context: Context, cacheSize: Long): SimpleCache? {
return try { return try {
val databaseProvider = StandaloneDatabaseProvider(context) val databaseProvider = StandaloneDatabaseProvider(context)
@ -663,6 +688,7 @@ class CS3IPlayer : IPlayer {
return getMediaItemBuilder(mimeType).setUri(url).build() return getMediaItemBuilder(mimeType).setUri(url).build()
} }
@SuppressLint("UnsafeOptInUsageError")
private fun getTrackSelector(context: Context, maxVideoHeight: Int?): TrackSelector { private fun getTrackSelector(context: Context, maxVideoHeight: Int?): TrackSelector {
val trackSelector = DefaultTrackSelector(context) val trackSelector = DefaultTrackSelector(context)
trackSelector.parameters = trackSelector.buildUponParameters() trackSelector.parameters = trackSelector.buildUponParameters()
@ -676,6 +702,7 @@ class CS3IPlayer : IPlayer {
var currentTextRenderer: CustomTextRenderer? = null var currentTextRenderer: CustomTextRenderer? = null
@SuppressLint("UnsafeOptInUsageError")
private fun buildExoPlayer( private fun buildExoPlayer(
context: Context, context: Context,
mediaItemSlices: List<MediaItemSlice>, mediaItemSlices: List<MediaItemSlice>,
@ -760,15 +787,33 @@ class CS3IPlayer : IPlayer {
// If there is only one item then treat it as normal, if multiple: concatenate the items. // If there is only one item then treat it as normal, if multiple: concatenate the items.
val videoMediaSource = if (mediaItemSlices.size == 1) { val videoMediaSource = if (mediaItemSlices.size == 1) {
factory.createMediaSource(mediaItemSlices.first().mediaItem) val item = mediaItemSlices.first()
item.drm?.let { drm ->
val drmCallback =
LocalMediaDrmCallback("{\"keys\":[{\"kty\":\"${drm.kty}\",\"k\":\"${drm.key}\",\"kid\":\"${drm.kid}\"}],\"type\":\"temporary\"}".toByteArray())
val manager = DefaultDrmSessionManager.Builder()
.setPlayClearSamplesWithoutKeys(true)
.setMultiSession(false)
.setKeyRequestParameters(drm.keyRequestParameters)
.setUuidAndExoMediaDrmProvider(drm.uuid, FrameworkMediaDrm.DEFAULT_PROVIDER)
.build(drmCallback)
val manifestDataSourceFactory = DefaultHttpDataSource.Factory()
DashMediaSource.Factory(manifestDataSourceFactory)
.setDrmSessionManagerProvider { manager }
.createMediaSource(item.mediaItem)
} ?: run {
factory.createMediaSource(item.mediaItem)
}
} else { } else {
val source = ConcatenatingMediaSource() val source = ConcatenatingMediaSource()
mediaItemSlices.map { mediaItemSlices.map { item ->
source.addMediaSource( source.addMediaSource(
// The duration MUST be known for it to work properly, see https://github.com/google/ExoPlayer/issues/4727 // The duration MUST be known for it to work properly, see https://github.com/google/ExoPlayer/issues/4727
ClippingMediaSource( ClippingMediaSource(
factory.createMediaSource(it.mediaItem), factory.createMediaSource(item.mediaItem),
it.durationUs item.durationUs
) )
) )
} }
@ -1105,6 +1150,8 @@ class CS3IPlayer : IPlayer {
} }
private var lastTimeStamps: List<EpisodeSkip.SkipStamp> = emptyList() private var lastTimeStamps: List<EpisodeSkip.SkipStamp> = emptyList()
@SuppressLint("UnsafeOptInUsageError")
override fun addTimeStamps(timeStamps: List<EpisodeSkip.SkipStamp>) { override fun addTimeStamps(timeStamps: List<EpisodeSkip.SkipStamp>) {
lastTimeStamps = timeStamps lastTimeStamps = timeStamps
timeStamps.forEach { timestamp -> timeStamps.forEach { timestamp ->
@ -1122,6 +1169,7 @@ class CS3IPlayer : IPlayer {
updatedTime() updatedTime()
} }
@SuppressLint("UnsafeOptInUsageError")
fun onRenderFirst() { fun onRenderFirst() {
if (hasUsedFirstRender) { // this insures that we only call this once per player load if (hasUsedFirstRender) { // this insures that we only call this once per player load
return return
@ -1188,6 +1236,7 @@ class CS3IPlayer : IPlayer {
} }
} }
@SuppressLint("UnsafeOptInUsageError")
private fun getSubSources( private fun getSubSources(
onlineSourceFactory: HttpDataSource.Factory?, onlineSourceFactory: HttpDataSource.Factory?,
offlineSourceFactory: DataSource.Factory?, offlineSourceFactory: DataSource.Factory?,
@ -1243,6 +1292,7 @@ class CS3IPlayer : IPlayer {
return exoPlayer != null return exoPlayer != null
} }
@SuppressLint("UnsafeOptInUsageError")
private fun loadOnlinePlayer(context: Context, link: ExtractorLink) { private fun loadOnlinePlayer(context: Context, link: ExtractorLink) {
Log.i(TAG, "loadOnlinePlayer $link") Log.i(TAG, "loadOnlinePlayer $link")
try { try {
@ -1259,7 +1309,7 @@ class CS3IPlayer : IPlayer {
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.socketFactory) HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.socketFactory)
} }
val mime = when(link.type) { val mime = when (link.type) {
ExtractorLinkType.M3U8 -> MimeTypes.APPLICATION_M3U8 ExtractorLinkType.M3U8 -> MimeTypes.APPLICATION_M3U8
ExtractorLinkType.DASH -> MimeTypes.APPLICATION_MPD ExtractorLinkType.DASH -> MimeTypes.APPLICATION_MPD
ExtractorLinkType.VIDEO -> MimeTypes.VIDEO_MP4 ExtractorLinkType.VIDEO -> MimeTypes.VIDEO_MP4
@ -1267,12 +1317,29 @@ class CS3IPlayer : IPlayer {
ExtractorLinkType.MAGNET -> throw IllegalArgumentException("No magnet support") ExtractorLinkType.MAGNET -> throw IllegalArgumentException("No magnet support")
} }
val mediaItems = if (link is ExtractorLinkPlayList) {
link.playlist.map { val mediaItems = when (link) {
is ExtractorLinkPlayList -> link.playlist.map {
MediaItemSlice(getMediaItem(mime, it.url), it.durationUs) MediaItemSlice(getMediaItem(mime, it.url), it.durationUs)
} }
} else {
listOf( is DrmExtractorLink -> {
listOf(
// Single sliced list with unset length
MediaItemSlice(
getMediaItem(mime, link.url), Long.MIN_VALUE,
drm = DrmMetadata(
kid = link.kid,
key = link.key,
uuid = link.uuid ?: C.CLEARKEY_UUID,
kty = link.kty,
keyRequestParameters = link.keyRequestParameters
)
)
)
}
else -> listOf(
// Single sliced list with unset length // Single sliced list with unset length
MediaItemSlice(getMediaItem(mime, link.url), Long.MIN_VALUE) MediaItemSlice(getMediaItem(mime, link.url), Long.MIN_VALUE)
) )

View File

@ -8,6 +8,7 @@ import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import org.jsoup.Jsoup import org.jsoup.Jsoup
import java.net.URL import java.net.URL
import java.util.UUID
import kotlin.collections.MutableList import kotlin.collections.MutableList
/** /**
@ -99,6 +100,60 @@ private fun inferTypeFromUrl(url: String): ExtractorLinkType {
} }
} }
val INFER_TYPE : ExtractorLinkType? = null val INFER_TYPE : ExtractorLinkType? = null
open class DrmExtractorLink private constructor(
override val source: String,
override val name: String,
override val url: String,
override val referer: String,
override val quality: Int,
override val headers: Map<String, String> = mapOf(),
/** Used for getExtractorVerifierJob() */
override val extractorData: String? = null,
override val type: ExtractorLinkType,
open val kid : String,
open val key : String,
/** if null then it uses the UUID for the ClearKey DRM scheme */
open val uuid : UUID?,
open val kty : String,
open val keyRequestParameters : HashMap<String, String>
) : ExtractorLink(
source, name, url, referer, quality, type, headers, extractorData
) {
constructor(
source: String,
name: String,
url: String,
referer: String,
quality: Int,
/** the type of the media, use INFER_TYPE if you want to auto infer the type from the url */
type: ExtractorLinkType?,
headers: Map<String, String> = mapOf(),
/** Used for getExtractorVerifierJob() */
extractorData: String? = null,
kid : String,
key : String,
uuid : UUID? = null,
kty : String = "oct",
keyRequestParameters : HashMap<String, String> = hashMapOf(),
) : this(
source = source,
name = name,
url = url,
referer = referer,
quality = quality,
headers = headers,
extractorData = extractorData,
type = type ?: inferTypeFromUrl(url),
kid = kid,
key = key,
uuid = uuid,
keyRequestParameters = keyRequestParameters,
kty = kty,
)
}
open class ExtractorLink constructor( open class ExtractorLink constructor(
open val source: String, open val source: String,
open val name: String, open val name: String,