Merge branch 'raw-dash-audio-widevine' into 'master'

add raw dash audio source widevine support

See merge request videostreaming/grayjay!68
This commit is contained in:
Kai DeLorenzo 2025-04-15 09:28:15 +00:00
commit bcd6255b45
14 changed files with 168 additions and 289 deletions

View file

@ -372,16 +372,20 @@ class VideoUrlSource {
this.url = obj.url; this.url = obj.url;
if(obj.requestModifier) if(obj.requestModifier)
this.requestModifier = obj.requestModifier; this.requestModifier = obj.requestModifier;
// deprecated api conversion
if(obj.licenseUri)
this.drmLicenseUri = obj.licenseUri;
if(obj.drmLicenseUri)
this.drmLicenseUri = obj.drmLicenseUri;
if(obj.getLicenseRequestExecutor)
this.getLicenseRequestExecutor = obj.getLicenseRequestExecutor;
} }
} }
class VideoUrlWidevineSource extends VideoUrlSource { class VideoUrlWidevineSource extends VideoUrlSource {
constructor(obj) { constructor(obj) {
super(obj); super(obj);
this.plugin_type = "VideoUrlWidevineSource";
this.licenseUri = obj.licenseUri;
if(obj.getLicenseRequestExecutor)
this.getLicenseRequestExecutor = obj.getLicenseRequestExecutor;
} }
} }
class VideoUrlRangeSource extends VideoUrlSource { class VideoUrlRangeSource extends VideoUrlSource {
@ -409,16 +413,6 @@ class AudioUrlSource {
this.language = obj.language ?? Language.UNKNOWN; this.language = obj.language ?? Language.UNKNOWN;
if(obj.requestModifier) if(obj.requestModifier)
this.requestModifier = obj.requestModifier; this.requestModifier = obj.requestModifier;
}
}
class AudioUrlWidevineSource extends AudioUrlSource {
constructor(obj) {
super(obj);
this.plugin_type = "AudioUrlWidevineSource";
this.licenseUri = obj.licenseUri;
if(obj.getLicenseRequestExecutor)
this.getLicenseRequestExecutor = obj.getLicenseRequestExecutor;
// deprecated api conversion // deprecated api conversion
if(obj.bearerToken) { if(obj.bearerToken) {
@ -436,6 +430,19 @@ class AudioUrlWidevineSource extends AudioUrlSource {
} }
} }
} }
// deprecated api conversion
if(obj.licenseUri)
this.drmLicenseUri = obj.licenseUri;
if(obj.drmLicenseUri)
this.drmLicenseUri = obj.drmLicenseUri;
if(obj.getLicenseRequestExecutor)
this.getLicenseRequestExecutor = obj.getLicenseRequestExecutor;
}
}
class AudioUrlWidevineSource extends AudioUrlSource {
constructor(obj) {
super(obj);
} }
} }
class AudioUrlRangeSource extends AudioUrlSource { class AudioUrlRangeSource extends AudioUrlSource {
@ -476,16 +483,22 @@ class DashSource {
this.language = obj.language; this.language = obj.language;
if(obj.requestModifier) if(obj.requestModifier)
this.requestModifier = obj.requestModifier; this.requestModifier = obj.requestModifier;
if(obj.getRequestExecutor)
this.getRequestExecutor = obj.getRequestExecutor;
// deprecated api conversion
if(obj.licenseUri)
this.drmLicenseUri = obj.licenseUri;
if(obj.drmLicenseUri)
this.drmLicenseUri = obj.drmLicenseUri;
if(obj.getLicenseRequestExecutor)
this.getLicenseRequestExecutor = obj.getLicenseRequestExecutor;
} }
} }
class DashWidevineSource extends DashSource { class DashWidevineSource extends DashSource {
constructor(obj) { constructor(obj) {
super(obj); super(obj);
this.plugin_type = "DashWidevineSource";
this.licenseUri = obj.licenseUri;
if(obj.getLicenseRequestExecutor)
this.getLicenseRequestExecutor = obj.getLicenseRequestExecutor;
} }
} }
class DashManifestRawSource { class DashManifestRawSource {
@ -518,6 +531,11 @@ class DashManifestRawAudioSource {
this.manifest = obj.manifest ?? null; this.manifest = obj.manifest ?? null;
if(obj.requestModifier) if(obj.requestModifier)
this.requestModifier = obj.requestModifier; this.requestModifier = obj.requestModifier;
if(obj.drmLicenseUri)
this.drmLicenseUri = obj.drmLicenseUri;
if(obj.getLicenseRequestExecutor)
this.getLicenseRequestExecutor = obj.getLicenseRequestExecutor;
} }
} }

View file

@ -1,3 +0,0 @@
package com.futo.platformplayer.api.media.models.streams.sources
interface IAudioUrlWidevineSource : IAudioUrlSource, IWidevineSource

View file

@ -1,5 +0,0 @@
package com.futo.platformplayer.api.media.models.streams.sources
interface IDashManifestWidevineSource : IWidevineSource {
val url: String
}

View file

@ -1,3 +0,0 @@
package com.futo.platformplayer.api.media.models.streams.sources
interface IVideoUrlWidevineSource : IVideoUrlSource, IWidevineSource

View file

@ -1,9 +0,0 @@
package com.futo.platformplayer.api.media.models.streams.sources
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
interface IWidevineSource {
val licenseUri: String
val hasLicenseRequestExecutor: Boolean
fun getLicenseRequestExecutor(): JSRequestExecutor?
}

View file

@ -1,41 +0,0 @@
package com.futo.platformplayer.api.media.platforms.js.models.sources
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlWidevineSource
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrThrow
class JSAudioUrlWidevineSource : JSAudioUrlSource, IAudioUrlWidevineSource {
override val licenseUri: String
override val hasLicenseRequestExecutor: Boolean
@Suppress("ConvertSecondaryConstructorToPrimary")
constructor(plugin: JSClient, obj: V8ValueObject) : super(plugin, obj) {
val contextName = "JSAudioUrlWidevineSource"
val config = plugin.config
licenseUri = _obj.getOrThrow(config, "licenseUri", contextName)
hasLicenseRequestExecutor = obj.has("getLicenseRequestExecutor")
}
override fun getLicenseRequestExecutor(): JSRequestExecutor? {
if (!hasLicenseRequestExecutor || _obj.isClosed)
return null
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSAudioUrlWidevineSource", "obj.getLicenseRequestExecutor()") {
_obj.invoke("getLicenseRequestExecutor", arrayOf<Any>())
}
if (result !is V8ValueObject)
return null
return JSRequestExecutor(_plugin, result)
}
override fun toString(): String {
val url = getAudioUrl()
return "(name=$name, container=$container, bitrate=$bitrate, codec=$codec, url=$url, language=$language, duration=$duration, hasLicenseRequestExecutor=${hasLicenseRequestExecutor}, licenseUri=$licenseUri)"
}
}

View file

@ -2,16 +2,11 @@ package com.futo.platformplayer.api.media.platforms.js.models.sources
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
import com.futo.platformplayer.api.media.models.streams.sources.other.IStreamMetaDataSource import com.futo.platformplayer.api.media.models.streams.sources.other.IStreamMetaDataSource
import com.futo.platformplayer.api.media.models.streams.sources.other.StreamMetaData import com.futo.platformplayer.api.media.models.streams.sources.other.StreamMetaData
import com.futo.platformplayer.api.media.platforms.js.DevJSClient import com.futo.platformplayer.api.media.platforms.js.DevJSClient
import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.others.Language import com.futo.platformplayer.others.Language
import com.futo.platformplayer.states.StateDeveloper import com.futo.platformplayer.states.StateDeveloper

View file

@ -1,60 +0,0 @@
package com.futo.platformplayer.api.media.platforms.js.models.sources
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestWidevineSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow
class JSDashManifestWidevineSource : IVideoUrlSource, IDashManifestSource,
IDashManifestWidevineSource, JSSource {
override val width: Int = 0
override val height: Int = 0
override val container: String = "application/dash+xml"
override val codec: String = "Dash"
override val name: String
override val bitrate: Int? = null
override val url: String
override val duration: Long
override var priority: Boolean = false
override val licenseUri: String
override val hasLicenseRequestExecutor: Boolean
@Suppress("ConvertSecondaryConstructorToPrimary")
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH, plugin, obj) {
val contextName = "DashWidevineSource"
val config = plugin.config
name = _obj.getOrThrow(config, "name", contextName)
url = _obj.getOrThrow(config, "url", contextName)
duration = _obj.getOrThrow(config, "duration", contextName)
priority = obj.getOrNull(config, "priority", contextName) ?: false
licenseUri = _obj.getOrThrow(config, "licenseUri", contextName)
hasLicenseRequestExecutor = obj.has("getLicenseRequestExecutor")
}
override fun getLicenseRequestExecutor(): JSRequestExecutor? {
if (!hasLicenseRequestExecutor || _obj.isClosed)
return null
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSDashManifestWidevineSource", "obj.getLicenseRequestExecutor()") {
_obj.invoke("getLicenseRequestExecutor", arrayOf<Any>())
}
if (result !is V8ValueObject)
return null
return JSRequestExecutor(_plugin, result)
}
override fun getVideoUrl(): String {
return url
}
}

View file

@ -1,7 +1,5 @@
package com.futo.platformplayer.api.media.platforms.js.models.sources package com.futo.platformplayer.api.media.platforms.js.models.sources
import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.datasource.HttpDataSource
import com.caoccao.javet.values.V8Value import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.modifier.AdhocRequestModifier import com.futo.platformplayer.api.media.models.modifier.AdhocRequestModifier
@ -15,9 +13,9 @@ import com.futo.platformplayer.api.media.platforms.js.models.JSRequestModifier
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.orNull import com.futo.platformplayer.orNull
import com.futo.platformplayer.views.video.datasources.JSHttpDataSource
abstract class JSSource { abstract class JSSource {
protected val _plugin: JSClient; protected val _plugin: JSClient;
@ -30,6 +28,9 @@ abstract class JSSource {
val hasRequestExecutor: Boolean; val hasRequestExecutor: Boolean;
private val _requestExecutor: JSRequest?; private val _requestExecutor: JSRequest?;
val drmLicenseUri: String?
val hasLicenseRequestExecutor: Boolean
val requiresCustomDatasource: Boolean get() { val requiresCustomDatasource: Boolean get() {
return hasRequestModifier || hasRequestExecutor; return hasRequestModifier || hasRequestExecutor;
} }
@ -51,6 +52,9 @@ abstract class JSSource {
JSRequest(plugin, it, null, null, true); JSRequest(plugin, it, null, null, true);
} }
hasRequestExecutor = _requestExecutor != null || obj.has("getRequestExecutor"); hasRequestExecutor = _requestExecutor != null || obj.has("getRequestExecutor");
drmLicenseUri = _obj.getOrDefault(_config, "drmLicenseUri", "JSSource.drmLicenseUri", null)
hasLicenseRequestExecutor = obj.has("getLicenseRequestExecutor")
} }
fun getRequestModifier(): IRequestModifier? { fun getRequestModifier(): IRequestModifier? {
@ -84,6 +88,19 @@ abstract class JSSource {
return JSRequestExecutor(_plugin, result) return JSRequestExecutor(_plugin, result)
} }
fun getLicenseRequestExecutor(): JSRequestExecutor? {
if (!hasLicenseRequestExecutor || _obj.isClosed)
return null
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSSource", "obj.getLicenseRequestExecutor()") {
_obj.invoke("getLicenseRequestExecutor", arrayOf<Any>())
}
if (result !is V8ValueObject)
return null
return JSRequestExecutor(_plugin, result)
}
fun getUnderlyingPlugin(): JSClient? { fun getUnderlyingPlugin(): JSClient? {
return _plugin; return _plugin;
@ -98,22 +115,17 @@ abstract class JSSource {
const val TYPE_AUDIO_WITH_METADATA = "AudioUrlRangeSource"; const val TYPE_AUDIO_WITH_METADATA = "AudioUrlRangeSource";
const val TYPE_VIDEO_WITH_METADATA = "VideoUrlRangeSource"; const val TYPE_VIDEO_WITH_METADATA = "VideoUrlRangeSource";
const val TYPE_DASH = "DashSource"; const val TYPE_DASH = "DashSource";
const val TYPE_DASH_WIDEVINE = "DashWidevineSource";
const val TYPE_DASH_RAW = "DashRawSource"; const val TYPE_DASH_RAW = "DashRawSource";
const val TYPE_DASH_RAW_AUDIO = "DashRawAudioSource"; const val TYPE_DASH_RAW_AUDIO = "DashRawAudioSource";
const val TYPE_HLS = "HLSSource"; const val TYPE_HLS = "HLSSource";
const val TYPE_AUDIOURL_WIDEVINE = "AudioUrlWidevineSource"
const val TYPE_VIDEOURL_WIDEVINE = "VideoUrlWidevineSource"
fun fromV8VideoNullable(plugin: JSClient, obj: V8Value?) : IVideoSource? = obj.orNull { fromV8Video(plugin, it as V8ValueObject) }; fun fromV8VideoNullable(plugin: JSClient, obj: V8Value?) : IVideoSource? = obj.orNull { fromV8Video(plugin, it as V8ValueObject) };
fun fromV8Video(plugin: JSClient, obj: V8ValueObject) : IVideoSource? { fun fromV8Video(plugin: JSClient, obj: V8ValueObject) : IVideoSource? {
val type = obj.getString("plugin_type"); val type = obj.getString("plugin_type");
return when(type) { return when(type) {
TYPE_VIDEOURL -> JSVideoUrlSource(plugin, obj); TYPE_VIDEOURL -> JSVideoUrlSource(plugin, obj);
TYPE_VIDEOURL_WIDEVINE -> JSVideoUrlWidevineSource(plugin, obj);
TYPE_VIDEO_WITH_METADATA -> JSVideoUrlRangeSource(plugin, obj); TYPE_VIDEO_WITH_METADATA -> JSVideoUrlRangeSource(plugin, obj);
TYPE_HLS -> fromV8HLS(plugin, obj); TYPE_HLS -> fromV8HLS(plugin, obj);
TYPE_DASH_WIDEVINE -> JSDashManifestWidevineSource(plugin, obj)
TYPE_DASH -> fromV8Dash(plugin, obj); TYPE_DASH -> fromV8Dash(plugin, obj);
TYPE_DASH_RAW -> fromV8DashRaw(plugin, obj); TYPE_DASH_RAW -> fromV8DashRaw(plugin, obj);
else -> { else -> {
@ -135,7 +147,6 @@ abstract class JSSource {
TYPE_HLS -> JSHLSManifestAudioSource.fromV8HLS(plugin, obj); TYPE_HLS -> JSHLSManifestAudioSource.fromV8HLS(plugin, obj);
TYPE_AUDIOURL -> JSAudioUrlSource(plugin, obj); TYPE_AUDIOURL -> JSAudioUrlSource(plugin, obj);
TYPE_DASH_RAW_AUDIO -> fromV8DashRawAudio(plugin, obj); TYPE_DASH_RAW_AUDIO -> fromV8DashRawAudio(plugin, obj);
TYPE_AUDIOURL_WIDEVINE -> JSAudioUrlWidevineSource(plugin, obj);
TYPE_AUDIO_WITH_METADATA -> JSAudioUrlRangeSource(plugin, obj); TYPE_AUDIO_WITH_METADATA -> JSAudioUrlRangeSource(plugin, obj);
else -> { else -> {
Logger.w("JSSource", "Unknown audio type ${type}"); Logger.w("JSSource", "Unknown audio type ${type}");

View file

@ -1,41 +0,0 @@
package com.futo.platformplayer.api.media.platforms.js.models.sources
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlWidevineSource
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrThrow
class JSVideoUrlWidevineSource : JSVideoUrlSource, IVideoUrlWidevineSource {
override val licenseUri: String
override val hasLicenseRequestExecutor: Boolean
@Suppress("ConvertSecondaryConstructorToPrimary")
constructor(plugin: JSClient, obj: V8ValueObject) : super(plugin, obj) {
val contextName = "JSAudioUrlWidevineSource"
val config = plugin.config
licenseUri = _obj.getOrThrow(config, "licenseUri", contextName)
hasLicenseRequestExecutor = obj.has("getLicenseRequestExecutor")
}
override fun getLicenseRequestExecutor(): JSRequestExecutor? {
if (!hasLicenseRequestExecutor || _obj.isClosed)
return null
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSAudioUrlWidevineSource", "obj.getLicenseRequestExecutor()") {
_obj.invoke("getLicenseRequestExecutor", arrayOf<Any>())
}
if (result !is V8ValueObject)
return null
return JSRequestExecutor(_plugin, result)
}
override fun toString(): String {
val url = getVideoUrl()
return "(width=$width, height=$height, container=$container, codec=$codec, name=$name, bitrate=$bitrate, duration=$duration, url=$url, hasLicenseRequestExecutor=$hasLicenseRequestExecutor, licenseUri=$licenseUri)"
}
}

View file

@ -31,11 +31,13 @@ import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat.startActivity
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.media3.common.C import androidx.media3.common.C
import androidx.media3.common.Format import androidx.media3.common.Format
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.HttpDataSource import androidx.media3.datasource.HttpDataSource
import androidx.media3.exoplayer.analytics.AnalyticsListener
import androidx.media3.ui.PlayerControlView import androidx.media3.ui.PlayerControlView
import androidx.media3.ui.TimeBar import androidx.media3.ui.TimeBar
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
@ -442,6 +444,21 @@ class VideoDetailView : ConstraintLayout {
_player.attachPlayer(); _player.attachPlayer();
_player.exoPlayer?.player?.addAnalyticsListener(object : AnalyticsListener {
override fun onDrmSessionManagerError(
eventTime: AnalyticsListener.EventTime, error: Exception
) {
super.onDrmSessionManagerError(eventTime, error)
UIDialogs.showDialog(context, R.drawable.ic_lock, context.getString(R.string.drm_not_supported), context.getString(R.string.open_and_play_in_mobile_browser), null, 1, UIDialogs.Action(context.getString(R.string.open_in_browser), {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(video?.url))
val options =
android.app.ActivityOptions.makeCustomAnimation(context, android.R.anim.fade_in, android.R.anim.fade_out)
startActivity(context, intent, options.toBundle())
}, UIDialogs.ActionStyle.NONE), UIDialogs.Action(context.getString(R.string.close), {}, UIDialogs.ActionStyle.PRIMARY)
)
}
})
_container_content_liveChat.onRaidNow.subscribe { _container_content_liveChat.onRaidNow.subscribe {
StatePlayer.instance.clearQueue(); StatePlayer.instance.clearQueue();
fragment.navigate<VideoDetailFragment>(it.targetUrl); fragment.navigate<VideoDetailFragment>(it.targetUrl);

View file

@ -18,11 +18,11 @@ import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudi
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
import com.futo.platformplayer.api.media.models.streams.sources.IWidevineSource
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSAudioUrlRangeSource import com.futo.platformplayer.api.media.platforms.js.models.sources.JSAudioUrlRangeSource
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSVideoUrlRangeSource import com.futo.platformplayer.api.media.platforms.js.models.sources.JSVideoUrlRangeSource
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.others.Language import com.futo.platformplayer.others.Language
@ -46,10 +46,8 @@ class VideoHelper {
return false return false
} }
fun isDownloadable(source: IVideoSource) = (source is IVideoUrlSource || source is IHLSManifestSource || source is JSDashManifestRawSource) && !(source is JSSource && source.drmLicenseUri != null)
fun isDownloadable(source: IVideoSource) = (source is IVideoUrlSource || source is IHLSManifestSource || source is JSDashManifestRawSource) && source !is IWidevineSource fun isDownloadable(source: IAudioSource) = (source is IAudioUrlSource || source is IHLSManifestAudioSource || source is JSDashManifestRawAudioSource) && !(source is JSSource && source.drmLicenseUri != null)
fun isDownloadable(source: IAudioSource) = (source is IAudioUrlSource || source is IHLSManifestAudioSource || source is JSDashManifestRawAudioSource) && source !is IWidevineSource
fun selectBestVideoSource(desc: IVideoSourceDescriptor, desiredPixelCount : Int, prefContainers : Array<String>) : IVideoSource? = selectBestVideoSource(desc.videoSources.toList(), desiredPixelCount, prefContainers); fun selectBestVideoSource(desc: IVideoSourceDescriptor, desiredPixelCount : Int, prefContainers : Array<String>) : IVideoSource? = selectBestVideoSource(desc.videoSources.toList(), desiredPixelCount, prefContainers);
fun selectBestVideoSource(sources: Iterable<IVideoSource>, desiredPixelCount : Int, prefContainers : Array<String>) : IVideoSource? { fun selectBestVideoSource(sources: Iterable<IVideoSource>, desiredPixelCount : Int, prefContainers : Array<String>) : IVideoSource? {
val targetVideo = if(desiredPixelCount > 0) { val targetVideo = if(desiredPixelCount > 0) {

View file

@ -1,6 +1,7 @@
package com.futo.platformplayer.views.video package com.futo.platformplayer.views.video
import android.content.Context import android.content.Context
import android.media.MediaDrm
import android.net.Uri import android.net.Uri
import android.util.AttributeSet import android.util.AttributeSet
import android.widget.RelativeLayout import android.widget.RelativeLayout
@ -22,6 +23,7 @@ import androidx.media3.exoplayer.dash.DashMediaSource
import androidx.media3.exoplayer.dash.manifest.DashManifestParser import androidx.media3.exoplayer.dash.manifest.DashManifestParser
import androidx.media3.exoplayer.drm.DefaultDrmSessionManager import androidx.media3.exoplayer.drm.DefaultDrmSessionManager
import androidx.media3.exoplayer.drm.HttpMediaDrmCallback import androidx.media3.exoplayer.drm.HttpMediaDrmCallback
import androidx.media3.exoplayer.drm.MediaDrmCallback
import androidx.media3.exoplayer.hls.HlsMediaSource import androidx.media3.exoplayer.hls.HlsMediaSource
import androidx.media3.exoplayer.source.MediaSource import androidx.media3.exoplayer.source.MediaSource
import androidx.media3.exoplayer.source.MergingMediaSource import androidx.media3.exoplayer.source.MergingMediaSource
@ -33,14 +35,11 @@ import com.futo.platformplayer.api.media.models.chapters.IChapter
import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlWidevineSource
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestWidevineSource
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudioSource import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlWidevineSource
import com.futo.platformplayer.api.media.models.streams.sources.LocalAudioSource import com.futo.platformplayer.api.media.models.streams.sources.LocalAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.LocalVideoSource import com.futo.platformplayer.api.media.models.streams.sources.LocalVideoSource
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
@ -57,8 +56,8 @@ import com.futo.platformplayer.helpers.VideoHelper
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.video.PlayerManager import com.futo.platformplayer.video.PlayerManager
import com.futo.platformplayer.views.video.datasources.PluginMediaDrmCallback
import com.futo.platformplayer.views.video.datasources.JSHttpDataSource import com.futo.platformplayer.views.video.datasources.JSHttpDataSource
import com.futo.platformplayer.views.video.datasources.PluginMediaDrmCallback
import getHttpDataSourceFactory import getHttpDataSourceFactory
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -421,11 +420,9 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
val didSet = when(videoSource) { val didSet = when(videoSource) {
is LocalVideoSource -> { swapVideoSourceLocal(videoSource); true; } is LocalVideoSource -> { swapVideoSourceLocal(videoSource); true; }
is JSVideoUrlRangeSource -> { swapVideoSourceUrlRange(videoSource); true; } is JSVideoUrlRangeSource -> { swapVideoSourceUrlRange(videoSource); true; }
is IDashManifestWidevineSource -> { swapVideoSourceDashWidevine(videoSource); true }
is IDashManifestSource -> { swapVideoSourceDash(videoSource); true;} is IDashManifestSource -> { swapVideoSourceDash(videoSource); true;}
is JSDashManifestRawSource -> swapVideoSourceDashRaw(videoSource, play, resume); is JSDashManifestRawSource -> swapVideoSourceDashRaw(videoSource, play, resume)
is IHLSManifestSource -> { swapVideoSourceHLS(videoSource); true; } is IHLSManifestSource -> { swapVideoSourceHLS(videoSource); true; }
is IVideoUrlWidevineSource -> { swapVideoSourceUrlWidevine(videoSource); true; }
is IVideoUrlSource -> { swapVideoSourceUrl(videoSource); true; } is IVideoUrlSource -> { swapVideoSourceUrl(videoSource); true; }
null -> { _lastVideoMediaSource = null; true;} null -> { _lastVideoMediaSource = null; true;}
else -> throw IllegalArgumentException("Unsupported video source [${videoSource.javaClass.simpleName}]"); else -> throw IllegalArgumentException("Unsupported video source [${videoSource.javaClass.simpleName}]");
@ -438,8 +435,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
is LocalAudioSource -> {swapAudioSourceLocal(audioSource); true; } is LocalAudioSource -> {swapAudioSourceLocal(audioSource); true; }
is JSAudioUrlRangeSource -> { swapAudioSourceUrlRange(audioSource); true; } is JSAudioUrlRangeSource -> { swapAudioSourceUrlRange(audioSource); true; }
is JSHLSManifestAudioSource -> { swapAudioSourceHLS(audioSource); true; } is JSHLSManifestAudioSource -> { swapAudioSourceHLS(audioSource); true; }
is JSDashManifestRawAudioSource -> swapAudioSourceDashRaw(audioSource, play, resume); is JSDashManifestRawAudioSource -> swapAudioSourceDashRaw(audioSource, play, resume)
is IAudioUrlWidevineSource -> { swapAudioSourceUrlWidevine(audioSource); true; }
is IAudioUrlSource -> { swapAudioSourceUrl(audioSource); true; } is IAudioUrlSource -> { swapAudioSourceUrl(audioSource); true; }
null -> { _lastAudioMediaSource = null; true; } null -> { _lastAudioMediaSource = null; true; }
else -> throw IllegalArgumentException("Unsupported video source [${audioSource.javaClass.simpleName}]"); else -> throw IllegalArgumentException("Unsupported video source [${audioSource.javaClass.simpleName}]");
@ -479,6 +475,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
} }
else throw IllegalArgumentException("source without itag data..."); else throw IllegalArgumentException("source without itag data...");
} }
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
private fun swapVideoSourceUrl(videoSource: IVideoUrlSource) { private fun swapVideoSourceUrl(videoSource: IVideoUrlSource) {
Logger.i(TAG, "Loading VideoSource [Url]"); Logger.i(TAG, "Loading VideoSource [Url]");
@ -486,35 +483,22 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
videoSource.getHttpDataSourceFactory() videoSource.getHttpDataSourceFactory()
else else
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT); DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
_lastVideoMediaSource = ProgressiveMediaSource.Factory(dataSource)
.createMediaSource(MediaItem.fromUri(videoSource.getVideoUrl()));
}
@OptIn(UnstableApi::class)
private fun swapVideoSourceUrlWidevine(videoSource: IVideoUrlWidevineSource) {
Logger.i(TAG, "Loading VideoSource [UrlWidevine]");
val dataSource = if(videoSource is JSSource && videoSource.requiresCustomDatasource)
videoSource.getHttpDataSourceFactory()
else
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT)
val baseCallback = HttpMediaDrmCallback(videoSource.licenseUri, dataSource) val drmCallback = if (videoSource is JSSource) getDrmCallback(videoSource, dataSource) else null
val callback = if (videoSource.hasLicenseRequestExecutor) { val factory = ProgressiveMediaSource.Factory(dataSource)
PluginMediaDrmCallback(baseCallback, videoSource.getLicenseRequestExecutor()!!, videoSource.licenseUri) if (drmCallback != null) {
} else { factory.setDrmSessionManagerProvider {
baseCallback
}
_lastVideoMediaSource = ProgressiveMediaSource.Factory(dataSource)
.setDrmSessionManagerProvider {
DefaultDrmSessionManager.Builder() DefaultDrmSessionManager.Builder()
.setMultiSession(true) .setMultiSession(true)
.build(callback) .build(drmCallback)
} }
.createMediaSource(
MediaItem.fromUri(videoSource.getVideoUrl())
)
} }
_lastVideoMediaSource = factory
.createMediaSource(MediaItem.fromUri(videoSource.getVideoUrl()));
}
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
private fun swapVideoSourceDash(videoSource: IDashManifestSource) { private fun swapVideoSourceDash(videoSource: IDashManifestSource) {
Logger.i(TAG, "Loading VideoSource [Dash]"); Logger.i(TAG, "Loading VideoSource [Dash]");
@ -522,27 +506,19 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
videoSource.getHttpDataSourceFactory() videoSource.getHttpDataSourceFactory()
else else
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT); DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
_lastVideoMediaSource = DashMediaSource.Factory(dataSource)
.createMediaSource(MediaItem.fromUri(videoSource.url)) val drmCallback =
if (videoSource is JSSource) getDrmCallback(videoSource, dataSource) else null
val factory = DashMediaSource.Factory(dataSource)
if (drmCallback != null) {
factory.setDrmSessionManagerProvider {
DefaultDrmSessionManager.Builder().setMultiSession(true).build(drmCallback)
} }
@OptIn(UnstableApi::class)
private fun swapVideoSourceDashWidevine(videoSource: IDashManifestWidevineSource) {
Logger.i(TAG, "Loading VideoSource [DashWidevine]")
val dataSource =
if (videoSource is JSSource && (videoSource.requiresCustomDatasource)) videoSource.getHttpDataSourceFactory()
else DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT)
val baseCallback = HttpMediaDrmCallback(videoSource.licenseUri, dataSource)
val callback = if (videoSource.hasLicenseRequestExecutor) {
PluginMediaDrmCallback(baseCallback, videoSource.getLicenseRequestExecutor()!!, videoSource.licenseUri)
} else {
baseCallback
} }
_lastVideoMediaSource = DashMediaSource.Factory(dataSource).setDrmSessionManagerProvider { _lastVideoMediaSource =
DefaultDrmSessionManager.Builder().setMultiSession(true).build(callback) factory.createMediaSource(MediaItem.fromUri(videoSource.url))
}.createMediaSource(MediaItem.fromUri(videoSource.url))
} }
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
private fun swapVideoSourceDashRaw(videoSource: JSDashManifestRawSource, play: Boolean, resume: Boolean): Boolean { private fun swapVideoSourceDashRaw(videoSource: JSDashManifestRawSource, play: Boolean, resume: Boolean): Boolean {
@ -637,6 +613,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
} }
else throw IllegalArgumentException("source without itag data...") else throw IllegalArgumentException("source without itag data...")
} }
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
private fun swapAudioSourceUrl(audioSource: IAudioUrlSource) { private fun swapAudioSourceUrl(audioSource: IAudioUrlSource) {
Logger.i(TAG, "Loading AudioSource [Url]"); Logger.i(TAG, "Loading AudioSource [Url]");
@ -644,8 +621,20 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
audioSource.getHttpDataSourceFactory() audioSource.getHttpDataSourceFactory()
else else
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT); DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
_lastAudioMediaSource = ProgressiveMediaSource.Factory(dataSource)
.createMediaSource(MediaItem.fromUri(audioSource.getAudioUrl())); val drmCallback =
if (audioSource is JSSource) getDrmCallback(audioSource, dataSource) else null
val factory = ProgressiveMediaSource.Factory(dataSource)
if (drmCallback != null) {
factory.setDrmSessionManagerProvider {
DefaultDrmSessionManager.Builder()
.setMultiSession(true)
.build(drmCallback)
}
}
_lastAudioMediaSource =
factory.createMediaSource(MediaItem.fromUri(audioSource.getAudioUrl()));
} }
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
private fun swapAudioSourceHLS(audioSource: IHLSManifestAudioSource) { private fun swapAudioSourceHLS(audioSource: IHLSManifestAudioSource) {
@ -661,26 +650,46 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
private fun swapAudioSourceDashRaw(audioSource: JSDashManifestRawAudioSource, play: Boolean, resume: Boolean): Boolean { private fun swapAudioSourceDashRaw(audioSource: JSDashManifestRawAudioSource, play: Boolean, resume: Boolean): Boolean {
Logger.i(TAG, "Loading AudioSource [DashRaw]"); Logger.i(TAG, "Loading AudioSource [DashRaw]");
val dataSource = if(audioSource is JSSource && (audioSource.requiresCustomDatasource)) val dataSource = if (audioSource.requiresCustomDatasource)
audioSource.getHttpDataSourceFactory() audioSource.getHttpDataSourceFactory()
else else
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT); DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
val drmCallback = getDrmCallback(audioSource, dataSource)
if (audioSource.hasGenerate) { if (audioSource.hasGenerate) {
findViewTreeLifecycleOwner()?.lifecycle?.coroutineScope?.launch(Dispatchers.IO) { findViewTreeLifecycleOwner()?.lifecycle?.coroutineScope?.launch(Dispatchers.IO) {
val generated = audioSource.generate(); val generated = audioSource.generate();
if (generated != null) { if (generated != null) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
_lastVideoMediaSource = DashMediaSource.Factory(dataSource) val factory = DashMediaSource.Factory(dataSource)
.createMediaSource(DashManifestParser().parse(Uri.parse(audioSource.url), if (drmCallback != null) {
ByteArrayInputStream(generated?.toByteArray() ?: ByteArray(0)))); factory.setDrmSessionManagerProvider {
DefaultDrmSessionManager.Builder().setMultiSession(true)
.build(drmCallback)
}
}
_lastVideoMediaSource = factory
.createMediaSource(
DashManifestParser().parse(
Uri.parse(audioSource.url),
ByteArrayInputStream(generated.toByteArray() ?: ByteArray(0))
)
);
loadSelectedSources(play, resume); loadSelectedSources(play, resume);
} }
} }
} }
return false; return false;
} else {
val factory = DashMediaSource.Factory(dataSource)
if (drmCallback != null) {
factory.setDrmSessionManagerProvider {
DefaultDrmSessionManager.Builder().setMultiSession(true)
.build(drmCallback)
} }
else { }
_lastVideoMediaSource = DashMediaSource.Factory(dataSource) _lastVideoMediaSource = factory
.createMediaSource( .createMediaSource(
DashManifestParser().parse( DashManifestParser().parse(
Uri.parse(audioSource.url), Uri.parse(audioSource.url),
@ -692,32 +701,22 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
} }
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
private fun swapAudioSourceUrlWidevine(audioSource: IAudioUrlWidevineSource) { private fun getDrmCallback(source: JSSource, dataSource: HttpDataSource.Factory): MediaDrmCallback? {
Logger.i(TAG, "Loading AudioSource [UrlWidevine]") return if (source.drmLicenseUri != null) {
val dataSource = if (audioSource is JSSource && audioSource.requiresCustomDatasource) if (!MediaDrm.isCryptoSchemeSupported(C.WIDEVINE_UUID)) {
audioSource.getHttpDataSourceFactory() throw IllegalArgumentException("Device does not support Widevine")
else }
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT)
val baseCallback = HttpMediaDrmCallback(audioSource.licenseUri, dataSource) val delegateCallback =
HttpMediaDrmCallback(source.drmLicenseUri, dataSource)
val callback = if (audioSource.hasLicenseRequestExecutor) { if (source.hasLicenseRequestExecutor) {
PluginMediaDrmCallback(baseCallback, audioSource.getLicenseRequestExecutor()!!, audioSource.licenseUri) PluginMediaDrmCallback(delegateCallback, source.getLicenseRequestExecutor()!!, source.drmLicenseUri)
} else { } else {
baseCallback delegateCallback
} }
} else null
_lastAudioMediaSource = ProgressiveMediaSource.Factory(dataSource)
.setDrmSessionManagerProvider {
DefaultDrmSessionManager.Builder()
.setMultiSession(true)
.build(callback)
} }
.createMediaSource(
MediaItem.fromUri(audioSource.getAudioUrl())
)
}
//Prefered source selection //Prefered source selection
fun getPreferredVideoSource(video: IPlatformVideoDetails, targetPixels: Int = -1): IVideoSource? { fun getPreferredVideoSource(video: IPlatformVideoDetails, targetPixels: Int = -1): IVideoSource? {

View file

@ -36,6 +36,7 @@
<string name="preferred_quality_description">Default quality for watching a video</string> <string name="preferred_quality_description">Default quality for watching a video</string>
<string name="update">Update</string> <string name="update">Update</string>
<string name="close">Close</string> <string name="close">Close</string>
<string name="open_in_browser">Open In Browser</string>
<string name="never">Never</string> <string name="never">Never</string>
<string name="import_options">Select any of the following available import options.</string> <string name="import_options">Select any of the following available import options.</string>
<string name="there_is_an_update_available_do_you_wish_to_update">There is an update available, do you wish to update?</string> <string name="there_is_an_update_available_do_you_wish_to_update">There is an update available, do you wish to update?</string>
@ -245,6 +246,8 @@
<string name="locked_content_description">This content is locked</string> <string name="locked_content_description">This content is locked</string>
<string name="unknown">Unknown</string> <string name="unknown">Unknown</string>
<string name="tap_to_open_in_browser">Tap to open in browser</string> <string name="tap_to_open_in_browser">Tap to open in browser</string>
<string name="drm_not_supported">This device does not support the DRM required to play this content</string>
<string name="open_and_play_in_mobile_browser">Please try playing this content in a mobile browser on this device. If it plays open a bug report and share your device model and OS version.</string>
<string name="missing_plugin">Missing Plugin</string> <string name="missing_plugin">Missing Plugin</string>
<string name="viewers_are_raiding">Viewers are raiding</string> <string name="viewers_are_raiding">Viewers are raiding</string>
<string name="go_now">Go now</string> <string name="go_now">Go now</string>