diff --git a/app/src/main/assets/scripts/source.js b/app/src/main/assets/scripts/source.js index fba714fc..402d4aa0 100644 --- a/app/src/main/assets/scripts/source.js +++ b/app/src/main/assets/scripts/source.js @@ -367,6 +367,16 @@ class VideoUrlSource { this.requestModifier = obj.requestModifier; } } +class VideoUrlWidevineSource extends VideoUrlSource { + constructor(obj) { + super(obj); + this.plugin_type = "VideoUrlWidevineSource"; + + this.licenseUri = obj.licenseUri; + if(obj.getLicenseRequestExecutor) + this.getLicenseRequestExecutor = obj.getLicenseRequestExecutor; + } +} class VideoUrlRangeSource extends VideoUrlSource { constructor(obj) { super(obj); @@ -399,8 +409,26 @@ class AudioUrlWidevineSource extends AudioUrlSource { super(obj); this.plugin_type = "AudioUrlWidevineSource"; - this.bearerToken = obj.bearerToken; this.licenseUri = obj.licenseUri; + if(obj.getLicenseRequestExecutor) + this.getLicenseRequestExecutor = obj.getLicenseRequestExecutor; + + // deprecated api conversion + if(obj.bearerToken) { + this.getLicenseRequestExecutor = () => { + return { + executeRequest: (url, _headers, _method, license_request_data) => { + return http.POST( + url, + license_request_data, + { Authorization: `Bearer ${obj.bearerToken}` }, + false, + true + ).body + } + } + } + } } } class AudioUrlRangeSource extends AudioUrlSource { @@ -443,6 +471,16 @@ class DashSource { this.requestModifier = obj.requestModifier; } } +class DashWidevineSource extends DashSource { + constructor(obj) { + super(obj); + this.plugin_type = "DashWidevineSource"; + + this.licenseUri = obj.licenseUri; + if(obj.getLicenseRequestExecutor) + this.getLicenseRequestExecutor = obj.getLicenseRequestExecutor; + } +} class DashManifestRawSource { constructor(obj) { obj = obj ?? {}; diff --git a/app/src/main/java/com/futo/platformplayer/AdvancedOrientationListener.kt b/app/src/main/java/com/futo/platformplayer/AdvancedOrientationListener.kt deleted file mode 100644 index 2b0897d5..00000000 --- a/app/src/main/java/com/futo/platformplayer/AdvancedOrientationListener.kt +++ /dev/null @@ -1,122 +0,0 @@ -import android.app.Activity -import android.content.Context -import android.content.pm.ActivityInfo -import android.hardware.Sensor -import android.hardware.SensorEvent -import android.hardware.SensorEventListener -import android.hardware.SensorManager -import com.futo.platformplayer.constructs.Event1 -import com.futo.platformplayer.logging.Logger -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch - - -class AdvancedOrientationListener(private val activity: Activity, private val lifecycleScope: CoroutineScope) { - private val sensorManager: SensorManager = activity.getSystemService(Context.SENSOR_SERVICE) as SensorManager - private val accelerometer: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) - private val magnetometer: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD) - - private var lastOrientation: Int = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED - private var lastStableOrientation: Int = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED - private var lastOrientationChangeTime = 0L - private val debounceTime = 200L - private val stabilityThresholdTime = 800L - private var deviceAspectRatio: Float = 1.0f - - private val gravity = FloatArray(3) - private val geomagnetic = FloatArray(3) - private val rotationMatrix = FloatArray(9) - private val orientationAngles = FloatArray(3) - - val onOrientationChanged = Event1() - - private val sensorListener = object : SensorEventListener { - override fun onSensorChanged(event: SensorEvent) { - when (event.sensor.type) { - Sensor.TYPE_ACCELEROMETER -> { - System.arraycopy(event.values, 0, gravity, 0, gravity.size) - } - Sensor.TYPE_MAGNETIC_FIELD -> { - System.arraycopy(event.values, 0, geomagnetic, 0, geomagnetic.size) - } - } - - if (gravity.isNotEmpty() && geomagnetic.isNotEmpty()) { - val success = SensorManager.getRotationMatrix(rotationMatrix, null, gravity, geomagnetic) - if (success) { - SensorManager.getOrientation(rotationMatrix, orientationAngles) - - val azimuth = Math.toDegrees(orientationAngles[0].toDouble()).toFloat() - val pitch = Math.toDegrees(orientationAngles[1].toDouble()).toFloat() - val roll = Math.toDegrees(orientationAngles[2].toDouble()).toFloat() - - val newOrientation = when { - roll in -155f .. -15f && isWithinThreshold(pitch, 0f, 30.0) -> { - ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE - } - roll in 15f .. 155f && isWithinThreshold(pitch, 0f, 30.0) -> { - ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE - } - isWithinThreshold(pitch, -90f, 30.0 * deviceAspectRatio) && roll in -15f .. 15f -> { - ActivityInfo.SCREEN_ORIENTATION_PORTRAIT - } - isWithinThreshold(pitch, 90f, 30.0 * deviceAspectRatio) && roll in -15f .. 15f -> { - ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT - } - else -> lastOrientation - } - - //Logger.i("AdvancedOrientationListener", "newOrientation = ${newOrientation}, roll = ${roll}, pitch = ${pitch}, azimuth = ${azimuth}") - - if (newOrientation != lastStableOrientation) { - val currentTime = System.currentTimeMillis() - if (currentTime - lastOrientationChangeTime > debounceTime) { - lastOrientationChangeTime = currentTime - lastStableOrientation = newOrientation - - lifecycleScope.launch(Dispatchers.Main) { - try { - delay(stabilityThresholdTime) - if (newOrientation == lastStableOrientation) { - lastOrientation = newOrientation - onOrientationChanged.emit(newOrientation) - } - } catch (e: Throwable) { - Logger.i(TAG, "Failed to trigger onOrientationChanged", e) - } - } - } - } - } - } - } - - override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {} - } - - private fun isWithinThreshold(value: Float, target: Float, threshold: Double): Boolean { - return Math.abs(value - target) <= threshold - } - - init { - sensorManager.registerListener(sensorListener, accelerometer, SensorManager.SENSOR_DELAY_GAME) - sensorManager.registerListener(sensorListener, magnetometer, SensorManager.SENSOR_DELAY_GAME) - - val metrics = activity.resources.displayMetrics - deviceAspectRatio = (metrics.heightPixels.toFloat() / metrics.widthPixels.toFloat()) - if (deviceAspectRatio == 0.0f) - deviceAspectRatio = 1.0f - - lastOrientation = activity.resources.configuration.orientation - } - - fun stopListening() { - sensorManager.unregisterListener(sensorListener) - } - - companion object { - private val TAG = "AdvancedOrientationListener" - } -} diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/streams/sources/IAudioUrlWidevineSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/streams/sources/IAudioUrlWidevineSource.kt index 311c5ed0..33d4fa54 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/models/streams/sources/IAudioUrlWidevineSource.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/models/streams/sources/IAudioUrlWidevineSource.kt @@ -1,6 +1,3 @@ package com.futo.platformplayer.api.media.models.streams.sources -interface IAudioUrlWidevineSource : IAudioUrlSource { - val bearerToken: String - val licenseUri: String -} +interface IAudioUrlWidevineSource : IAudioUrlSource, IWidevineSource diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/streams/sources/IDashManifestWidevineSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/streams/sources/IDashManifestWidevineSource.kt new file mode 100644 index 00000000..c1b5ea35 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/api/media/models/streams/sources/IDashManifestWidevineSource.kt @@ -0,0 +1,5 @@ +package com.futo.platformplayer.api.media.models.streams.sources + +interface IDashManifestWidevineSource : IWidevineSource { + val url: String +} diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/streams/sources/IVideoUrlWidevineSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/streams/sources/IVideoUrlWidevineSource.kt new file mode 100644 index 00000000..073af50a --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/api/media/models/streams/sources/IVideoUrlWidevineSource.kt @@ -0,0 +1,3 @@ +package com.futo.platformplayer.api.media.models.streams.sources + +interface IVideoUrlWidevineSource : IVideoUrlSource, IWidevineSource diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/streams/sources/IWidevineSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/streams/sources/IWidevineSource.kt new file mode 100644 index 00000000..b94cf12c --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/api/media/models/streams/sources/IWidevineSource.kt @@ -0,0 +1,9 @@ +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? +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSRequestExecutor.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSRequestExecutor.kt index a9ab2c1a..70dfecfd 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSRequestExecutor.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSRequestExecutor.kt @@ -42,7 +42,7 @@ class JSRequestExecutor { //TODO: Executor properties? @Throws(ScriptException::class) - open fun executeRequest(url: String, headers: Map): ByteArray { + open fun executeRequest(method: String, url: String, body: ByteArray?, headers: Map): ByteArray { if (_executor.isClosed) throw IllegalStateException("Executor object is closed"); @@ -53,7 +53,7 @@ class JSRequestExecutor { "[${_config.name}] JSRequestExecutor", "builder.modifyRequest()" ) { - _executor.invoke("executeRequest", url, headers); + _executor.invoke("executeRequest", url, headers, method, body); } as V8Value; } else V8Plugin.catchScriptErrors( @@ -61,7 +61,7 @@ class JSRequestExecutor { "[${_config.name}] JSRequestExecutor", "builder.modifyRequest()" ) { - _executor.invoke("executeRequest", url, headers); + _executor.invoke("executeRequest", url, headers, method, body); } as V8Value; try { diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSAudioUrlWidevineSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSAudioUrlWidevineSource.kt index dcacdb72..516e07ff 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSAudioUrlWidevineSource.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSAudioUrlWidevineSource.kt @@ -3,22 +3,39 @@ 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 bearerToken: String 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 - bearerToken = _obj.getOrThrow(config, "bearerToken", contextName) + licenseUri = _obj.getOrThrow(config, "licenseUri", contextName) + hasLicenseRequestExecutor = obj.has("getLicenseRequestExecutor") + } + + override fun getLicenseRequestExecutor(): JSRequestExecutor? { + if (!hasLicenseRequestExecutor || _obj.isClosed) + return null + + val result = V8Plugin.catchScriptErrors(_config, "[${_config.name}] JSAudioUrlWidevineSource", "obj.getLicenseRequestExecutor()") { + _obj.invoke("getLicenseRequestExecutor", arrayOf()) + } + + 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, bearerToken=$bearerToken, licenseUri=$licenseUri)" + return "(name=$name, container=$container, bitrate=$bitrate, codec=$codec, url=$url, language=$language, duration=$duration, hasLicenseRequestExecutor=${hasLicenseRequestExecutor}, licenseUri=$licenseUri)" } } diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestWidevineSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestWidevineSource.kt new file mode 100644 index 00000000..be72d3a0 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestWidevineSource.kt @@ -0,0 +1,60 @@ +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(_config, "[${_config.name}] JSDashManifestWidevineSource", "obj.getLicenseRequestExecutor()") { + _obj.invoke("getLicenseRequestExecutor", arrayOf()) + } + + if (result !is V8ValueObject) + return null + + return JSRequestExecutor(_plugin, result) + } + + override fun getVideoUrl(): String { + return url + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSSource.kt index 45ace168..3c76e23d 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSSource.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSSource.kt @@ -98,18 +98,22 @@ abstract class JSSource { const val TYPE_AUDIO_WITH_METADATA = "AudioUrlRangeSource"; const val TYPE_VIDEO_WITH_METADATA = "VideoUrlRangeSource"; const val TYPE_DASH = "DashSource"; + const val TYPE_DASH_WIDEVINE = "DashWidevineSource"; const val TYPE_DASH_RAW = "DashRawSource"; const val TYPE_DASH_RAW_AUDIO = "DashRawAudioSource"; 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 fromV8Video(plugin: JSClient, obj: V8ValueObject) : IVideoSource? { val type = obj.getString("plugin_type"); return when(type) { TYPE_VIDEOURL -> JSVideoUrlSource(plugin, obj); + TYPE_VIDEOURL_WIDEVINE -> JSVideoUrlWidevineSource(plugin, obj); TYPE_VIDEO_WITH_METADATA -> JSVideoUrlRangeSource(plugin, obj); TYPE_HLS -> fromV8HLS(plugin, obj); + TYPE_DASH_WIDEVINE -> JSDashManifestWidevineSource(plugin, obj) TYPE_DASH -> fromV8Dash(plugin, obj); TYPE_DASH_RAW -> fromV8DashRaw(plugin, obj); else -> { diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoUrlWidevineSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoUrlWidevineSource.kt new file mode 100644 index 00000000..bcd6607d --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoUrlWidevineSource.kt @@ -0,0 +1,41 @@ +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(_config, "[${_config.name}] JSAudioUrlWidevineSource", "obj.getLicenseRequestExecutor()") { + _obj.invoke("getLicenseRequestExecutor", arrayOf()) + } + + 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)" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt b/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt index 23a9c5a5..db2e231b 100644 --- a/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt +++ b/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt @@ -1245,7 +1245,7 @@ class StateCasting { val videoExecutor = _videoExecutor; if (videoExecutor != null) { - val data = videoExecutor.executeRequest(originalUrl, httpContext.headers) + val data = videoExecutor.executeRequest("GET", originalUrl, null, httpContext.headers) httpContext.respondBytes(200, HttpHeaders().apply { put("Content-Type", mediaType) }, data); @@ -1263,7 +1263,7 @@ class StateCasting { val audioExecutor = _audioExecutor; if (audioExecutor != null) { - val data = audioExecutor.executeRequest(originalUrl, httpContext.headers) + val data = audioExecutor.executeRequest("GET", originalUrl, null, httpContext.headers) httpContext.respondBytes(200, HttpHeaders().apply { put("Content-Type", mediaType) }, data); diff --git a/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt b/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt index debcd660..e4e9bedb 100644 --- a/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt +++ b/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt @@ -663,7 +663,7 @@ class VideoDownload { val url = foundTemplateUrl.replace("\$Number\$", indexCounter.toString()); val data = if(executor != null) - executor.executeRequest(url, mapOf()); + executor.executeRequest("GET", url, null, mapOf()); else { val resp = client.get(url, mutableMapOf()); if(!resp.isOk) diff --git a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt index fcc6dc6d..9ca559ca 100644 --- a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt +++ b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt @@ -3,14 +3,11 @@ package com.futo.platformplayer.views.video import android.content.Context import android.net.Uri import android.util.AttributeSet -import android.util.Xml import android.widget.RelativeLayout import androidx.annotation.OptIn -import androidx.fragment.app.findFragment import androidx.lifecycle.coroutineScope import androidx.lifecycle.findViewTreeLifecycleOwner import androidx.media3.common.C -import androidx.media3.common.C.Encoding import androidx.media3.common.MediaItem import androidx.media3.common.PlaybackException import androidx.media3.common.Player @@ -22,9 +19,9 @@ import androidx.media3.datasource.DefaultHttpDataSource import androidx.media3.datasource.HttpDataSource import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.dash.DashMediaSource -import androidx.media3.exoplayer.dash.manifest.DashManifest import androidx.media3.exoplayer.dash.manifest.DashManifestParser -import androidx.media3.exoplayer.drm.DefaultDrmSessionManagerProvider +import androidx.media3.exoplayer.drm.DefaultDrmSessionManager +import androidx.media3.exoplayer.drm.HttpMediaDrmCallback import androidx.media3.exoplayer.hls.HlsMediaSource import androidx.media3.exoplayer.source.MediaSource import androidx.media3.exoplayer.source.MergingMediaSource @@ -34,18 +31,18 @@ import androidx.media3.exoplayer.trackselection.DefaultTrackSelector import com.futo.platformplayer.Settings 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.sources.AudioUrlSource 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.IAudioUrlWidevineSource 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.IHLSManifestSource 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.IVideoUrlWidevineSource 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.VideoUrlSource import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails import com.futo.platformplayer.api.media.platforms.js.models.sources.JSAudioUrlRangeSource @@ -55,15 +52,13 @@ import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManif import com.futo.platformplayer.api.media.platforms.js.models.sources.JSHLSManifestAudioSource 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.JSVideoUrlSource import com.futo.platformplayer.constructs.Event1 -import com.futo.platformplayer.engine.dev.V8RemoteObject import com.futo.platformplayer.helpers.VideoHelper import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.video.PlayerManager +import com.futo.platformplayer.views.video.datasources.PluginMediaDrmCallback import com.futo.platformplayer.views.video.datasources.JSHttpDataSource -import com.google.gson.Gson import getHttpDataSourceFactory import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -417,9 +412,11 @@ abstract class FutoVideoPlayerBase : RelativeLayout { val didSet = when(videoSource) { is LocalVideoSource -> { swapVideoSourceLocal(videoSource); true; } is JSVideoUrlRangeSource -> { swapVideoSourceUrlRange(videoSource); true; } + is IDashManifestWidevineSource -> { swapVideoSourceDashWidevine(videoSource); true } is IDashManifestSource -> { swapVideoSourceDash(videoSource); true;} is JSDashManifestRawSource -> swapVideoSourceDashRaw(videoSource, play, resume); is IHLSManifestSource -> { swapVideoSourceHLS(videoSource); true; } + is IVideoUrlWidevineSource -> { swapVideoSourceUrlWidevine(videoSource); true; } is IVideoUrlSource -> { swapVideoSourceUrl(videoSource); true; } null -> { _lastVideoMediaSource = null; true;} else -> throw IllegalArgumentException("Unsupported video source [${videoSource.javaClass.simpleName}]"); @@ -484,6 +481,32 @@ abstract class FutoVideoPlayerBase : RelativeLayout { .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 callback = if (videoSource.hasLicenseRequestExecutor) { + PluginMediaDrmCallback(baseCallback, videoSource.getLicenseRequestExecutor()!!, videoSource.licenseUri) + } else { + baseCallback + } + + _lastVideoMediaSource = ProgressiveMediaSource.Factory(dataSource) + .setDrmSessionManagerProvider { + DefaultDrmSessionManager.Builder() + .setMultiSession(true) + .build(callback) + } + .createMediaSource( + MediaItem.fromUri(videoSource.getVideoUrl()) + ) + } + @OptIn(UnstableApi::class) private fun swapVideoSourceDash(videoSource: IDashManifestSource) { Logger.i(TAG, "Loading VideoSource [Dash]"); val dataSource = if(videoSource is JSSource && (videoSource.requiresCustomDatasource)) @@ -494,6 +517,25 @@ abstract class FutoVideoPlayerBase : RelativeLayout { .createMediaSource(MediaItem.fromUri(videoSource.url)) } @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 { + DefaultDrmSessionManager.Builder().setMultiSession(true).build(callback) + }.createMediaSource(MediaItem.fromUri(videoSource.url)) + } + @OptIn(UnstableApi::class) private fun swapVideoSourceDashRaw(videoSource: JSDashManifestRawSource, play: Boolean, resume: Boolean): Boolean { Logger.i(TAG, "Loading VideoSource [Dash]"); @@ -639,6 +681,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout { return true; } } + @OptIn(UnstableApi::class) private fun swapAudioSourceUrlWidevine(audioSource: IAudioUrlWidevineSource) { Logger.i(TAG, "Loading AudioSource [UrlWidevine]") @@ -647,20 +690,22 @@ abstract class FutoVideoPlayerBase : RelativeLayout { else DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT) - val httpRequestHeaders = mapOf("Authorization" to "Bearer " + audioSource.bearerToken) - val provider = DefaultDrmSessionManagerProvider() - provider.setDrmHttpDataSourceFactory(dataSource) + val baseCallback = HttpMediaDrmCallback(audioSource.licenseUri, dataSource) + + val callback = if (audioSource.hasLicenseRequestExecutor) { + PluginMediaDrmCallback(baseCallback, audioSource.getLicenseRequestExecutor()!!, audioSource.licenseUri) + } else { + baseCallback + } + _lastAudioMediaSource = ProgressiveMediaSource.Factory(dataSource) - .setDrmSessionManagerProvider(provider) + .setDrmSessionManagerProvider { + DefaultDrmSessionManager.Builder() + .setMultiSession(true) + .build(callback) + } .createMediaSource( - MediaItem.Builder() - .setUri(audioSource.getAudioUrl()).setDrmConfiguration( - MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID) - .setLicenseUri(audioSource.licenseUri) - .setMultiSession(true) - .setLicenseRequestHeaders(httpRequestHeaders) - .build() - ).build() + MediaItem.fromUri(audioSource.getAudioUrl()) ) } diff --git a/app/src/main/java/com/futo/platformplayer/views/video/datasources/JSHttpDataSource.java b/app/src/main/java/com/futo/platformplayer/views/video/datasources/JSHttpDataSource.java index 40e1a503..859c1b2f 100644 --- a/app/src/main/java/com/futo/platformplayer/views/video/datasources/JSHttpDataSource.java +++ b/app/src/main/java/com/futo/platformplayer/views/video/datasources/JSHttpDataSource.java @@ -360,7 +360,7 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource { if(executor != null) { try { Logger.Companion.i(TAG, "Executor for " + dataSpec.uri.toString(), null); - byte[] data = executor.executeRequest(dataSpec.uri.toString(), dataSpec.httpRequestHeaders); + byte[] data = executor.executeRequest("GET", dataSpec.uri.toString(), null, dataSpec.httpRequestHeaders); Logger.Companion.i(TAG, "Executor result for " + dataSpec.uri.toString() + " : " + data.length, null); if (data == null) throw new HttpDataSourceException( diff --git a/app/src/main/java/com/futo/platformplayer/views/video/datasources/PluginMediaDrmCallback.kt b/app/src/main/java/com/futo/platformplayer/views/video/datasources/PluginMediaDrmCallback.kt new file mode 100644 index 00000000..2fa5dbb7 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/views/video/datasources/PluginMediaDrmCallback.kt @@ -0,0 +1,23 @@ +package com.futo.platformplayer.views.video.datasources + +import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.drm.ExoMediaDrm +import androidx.media3.exoplayer.drm.MediaDrmCallback +import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor +import java.util.UUID +import kotlin.io.encoding.ExperimentalEncodingApi + +@UnstableApi +class PluginMediaDrmCallback( + private val delegate: MediaDrmCallback, + private val requestExecutor: JSRequestExecutor, + private val licenseUrl: String +) : MediaDrmCallback by delegate { + + @ExperimentalEncodingApi + override fun executeKeyRequest(uuid: UUID, request: ExoMediaDrm.KeyRequest): ByteArray { + val pluginResponse = requestExecutor.executeRequest("POST", licenseUrl, request.data, mapOf()) + + return pluginResponse + } +}