mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 04:52:39 +02:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 80bb15f3fb | |||
| 27a86a67f0 | |||
| 284b2a24f8 | |||
| 854d1506a6 | |||
| 811fd4e73e | |||
| 335988aa67 | |||
| 29a54fbed4 | |||
| 3a11d0d9d1 | |||
| 894e400819 |
+1
-1
@@ -232,7 +232,7 @@ dependencies {
|
|||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0'
|
||||||
|
|
||||||
//Rust casting SDK
|
//Rust casting SDK
|
||||||
implementation('org.futo.gitlab.videostreaming.fcast-sdk-jitpack:sender-sdk-minimal:0.3.1') {
|
implementation('org.futo.gitlab.videostreaming.fcast-sdk-jitpack:sender-sdk-minimal:0.4.0') {
|
||||||
// Polycentricandroid includes this
|
// Polycentricandroid includes this
|
||||||
exclude group: 'net.java.dev.jna'
|
exclude group: 'net.java.dev.jna'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.futo.platformplayer.casting
|
package com.futo.platformplayer.casting
|
||||||
|
|
||||||
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.models.CastingDeviceInfo
|
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||||
import org.fcast.sender_sdk.Metadata
|
import org.fcast.sender_sdk.Metadata
|
||||||
@@ -16,6 +17,7 @@ abstract class CastingDevice {
|
|||||||
abstract val onDurationChanged: Event1<Double>
|
abstract val onDurationChanged: Event1<Double>
|
||||||
abstract val onVolumeChanged: Event1<Double>
|
abstract val onVolumeChanged: Event1<Double>
|
||||||
abstract val onSpeedChanged: Event1<Double>
|
abstract val onSpeedChanged: Event1<Double>
|
||||||
|
abstract val onMediaItemEnd: Event0
|
||||||
abstract var connectionState: CastConnectionState
|
abstract var connectionState: CastConnectionState
|
||||||
abstract val protocolType: CastProtocolType
|
abstract val protocolType: CastProtocolType
|
||||||
abstract var isPlaying: Boolean
|
abstract var isPlaying: Boolean
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ package com.futo.platformplayer.casting
|
|||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import com.futo.platformplayer.BuildConfig
|
import com.futo.platformplayer.BuildConfig
|
||||||
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.CastingDeviceInfo
|
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||||
|
import com.futo.polycentric.core.Event
|
||||||
import org.fcast.sender_sdk.ApplicationInfo
|
import org.fcast.sender_sdk.ApplicationInfo
|
||||||
import org.fcast.sender_sdk.GenericKeyEvent
|
import org.fcast.sender_sdk.KeyEvent
|
||||||
import org.fcast.sender_sdk.GenericMediaEvent
|
import org.fcast.sender_sdk.MediaEvent
|
||||||
import org.fcast.sender_sdk.PlaybackState
|
import org.fcast.sender_sdk.PlaybackState
|
||||||
import org.fcast.sender_sdk.Source
|
import org.fcast.sender_sdk.Source
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
@@ -15,8 +17,10 @@ import org.fcast.sender_sdk.CastingDevice as RsCastingDevice;
|
|||||||
import org.fcast.sender_sdk.DeviceEventHandler as RsDeviceEventHandler;
|
import org.fcast.sender_sdk.DeviceEventHandler as RsDeviceEventHandler;
|
||||||
import org.fcast.sender_sdk.DeviceConnectionState
|
import org.fcast.sender_sdk.DeviceConnectionState
|
||||||
import org.fcast.sender_sdk.DeviceFeature
|
import org.fcast.sender_sdk.DeviceFeature
|
||||||
|
import org.fcast.sender_sdk.EventSubscription
|
||||||
import org.fcast.sender_sdk.IpAddr
|
import org.fcast.sender_sdk.IpAddr
|
||||||
import org.fcast.sender_sdk.LoadRequest
|
import org.fcast.sender_sdk.LoadRequest
|
||||||
|
import org.fcast.sender_sdk.MediaItemEventType
|
||||||
import org.fcast.sender_sdk.Metadata
|
import org.fcast.sender_sdk.Metadata
|
||||||
import org.fcast.sender_sdk.ProtocolType
|
import org.fcast.sender_sdk.ProtocolType
|
||||||
import org.fcast.sender_sdk.urlFormatIpAddr
|
import org.fcast.sender_sdk.urlFormatIpAddr
|
||||||
@@ -63,6 +67,7 @@ class CastingDeviceExp(val device: RsCastingDevice) : CastingDevice() {
|
|||||||
var onDurationChanged = Event1<Double>()
|
var onDurationChanged = Event1<Double>()
|
||||||
var onVolumeChanged = Event1<Double>()
|
var onVolumeChanged = Event1<Double>()
|
||||||
var onSpeedChanged = Event1<Double>()
|
var onSpeedChanged = Event1<Double>()
|
||||||
|
var onMediaItemEnd = Event0()
|
||||||
|
|
||||||
override fun connectionStateChanged(state: DeviceConnectionState) {
|
override fun connectionStateChanged(state: DeviceConnectionState) {
|
||||||
onConnectionStateChanged.emit(state)
|
onConnectionStateChanged.emit(state)
|
||||||
@@ -92,12 +97,14 @@ class CastingDeviceExp(val device: RsCastingDevice) : CastingDevice() {
|
|||||||
// TODO
|
// TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun keyEvent(event: GenericKeyEvent) {
|
override fun keyEvent(event: KeyEvent) {
|
||||||
// Unreachable
|
// Unreachable
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun mediaEvent(event: GenericMediaEvent) {
|
override fun mediaEvent(event: MediaEvent) {
|
||||||
// Unreachable
|
if (event.type == MediaItemEventType.END) {
|
||||||
|
onMediaItemEnd.emit()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun playbackError(message: String) {
|
override fun playbackError(message: String) {
|
||||||
@@ -127,6 +134,8 @@ class CastingDeviceExp(val device: RsCastingDevice) : CastingDevice() {
|
|||||||
get() = eventHandler.onVolumeChanged
|
get() = eventHandler.onVolumeChanged
|
||||||
override val onSpeedChanged: Event1<Double>
|
override val onSpeedChanged: Event1<Double>
|
||||||
get() = eventHandler.onSpeedChanged
|
get() = eventHandler.onSpeedChanged
|
||||||
|
override val onMediaItemEnd: Event0
|
||||||
|
get() = eventHandler.onMediaItemEnd
|
||||||
|
|
||||||
override fun resumePlayback() = device.resumePlayback()
|
override fun resumePlayback() = device.resumePlayback()
|
||||||
override fun pausePlayback() = device.pausePlayback()
|
override fun pausePlayback() = device.pausePlayback()
|
||||||
@@ -181,7 +190,8 @@ class CastingDeviceExp(val device: RsCastingDevice) : CastingDevice() {
|
|||||||
resumePosition = resumePosition,
|
resumePosition = resumePosition,
|
||||||
speed = speed,
|
speed = speed,
|
||||||
volume = volume,
|
volume = volume,
|
||||||
metadata = metadata
|
metadata = metadata,
|
||||||
|
requestHeaders = null,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -200,6 +210,7 @@ class CastingDeviceExp(val device: RsCastingDevice) : CastingDevice() {
|
|||||||
speed = speed,
|
speed = speed,
|
||||||
volume = volume,
|
volume = volume,
|
||||||
metadata = metadata,
|
metadata = metadata,
|
||||||
|
requestHeaders = null,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -227,6 +238,13 @@ class CastingDeviceExp(val device: RsCastingDevice) : CastingDevice() {
|
|||||||
eventHandler.onConnectionStateChanged.subscribe { newState ->
|
eventHandler.onConnectionStateChanged.subscribe { newState ->
|
||||||
when (newState) {
|
when (newState) {
|
||||||
is DeviceConnectionState.Connected -> {
|
is DeviceConnectionState.Connected -> {
|
||||||
|
if (device.supportsFeature(DeviceFeature.MEDIA_EVENT_SUBSCRIPTION)) {
|
||||||
|
try {
|
||||||
|
device.subscribeEvent(EventSubscription.MediaItemEnd)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.e(TAG, "Failed to subscribe to MediaItemEnd events: $e")
|
||||||
|
}
|
||||||
|
}
|
||||||
usedRemoteAddress = ipAddrToInetAddress(newState.usedRemoteAddr)
|
usedRemoteAddress = ipAddrToInetAddress(newState.usedRemoteAddr)
|
||||||
localAddress = ipAddrToInetAddress(newState.localAddr)
|
localAddress = ipAddrToInetAddress(newState.localAddr)
|
||||||
connectionState = CastConnectionState.CONNECTED
|
connectionState = CastConnectionState.CONNECTED
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.futo.platformplayer.casting
|
package com.futo.platformplayer.casting
|
||||||
|
|
||||||
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.models.CastingDeviceInfo
|
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||||
import kotlinx.serialization.KSerializer
|
import kotlinx.serialization.KSerializer
|
||||||
@@ -181,6 +182,7 @@ class CastingDeviceLegacyWrapper(val inner: CastingDeviceLegacy) : CastingDevice
|
|||||||
override val onDurationChanged: Event1<Double> get() = inner.onDurationChanged
|
override val onDurationChanged: Event1<Double> get() = inner.onDurationChanged
|
||||||
override val onVolumeChanged: Event1<Double> get() = inner.onVolumeChanged
|
override val onVolumeChanged: Event1<Double> get() = inner.onVolumeChanged
|
||||||
override val onSpeedChanged: Event1<Double> get() = inner.onSpeedChanged
|
override val onSpeedChanged: Event1<Double> get() = inner.onSpeedChanged
|
||||||
|
override val onMediaItemEnd: Event0 = Event0()
|
||||||
override var connectionState: CastConnectionState
|
override var connectionState: CastConnectionState
|
||||||
get() = inner.connectionState
|
get() = inner.connectionState
|
||||||
set(_) = Unit
|
set(_) = Unit
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalAud
|
|||||||
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoContentSource
|
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoContentSource
|
||||||
import com.futo.platformplayer.awaitCancelConverted
|
import com.futo.platformplayer.awaitCancelConverted
|
||||||
import com.futo.platformplayer.builders.DashBuilder
|
import com.futo.platformplayer.builders.DashBuilder
|
||||||
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.models.CastingDeviceInfo
|
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.constructs.Event2
|
import com.futo.platformplayer.constructs.Event2
|
||||||
@@ -82,6 +83,7 @@ abstract class StateCasting {
|
|||||||
val onActiveDeviceTimeChanged = Event1<Double>();
|
val onActiveDeviceTimeChanged = Event1<Double>();
|
||||||
val onActiveDeviceDurationChanged = Event1<Double>();
|
val onActiveDeviceDurationChanged = Event1<Double>();
|
||||||
val onActiveDeviceVolumeChanged = Event1<Double>();
|
val onActiveDeviceVolumeChanged = Event1<Double>();
|
||||||
|
val onActiveDeviceMediaItemEnd = Event0()
|
||||||
var activeDevice: CastingDevice? = null;
|
var activeDevice: CastingDevice? = null;
|
||||||
private var _videoExecutor: JSRequestExecutor? = null
|
private var _videoExecutor: JSRequestExecutor? = null
|
||||||
private var _audioExecutor: JSRequestExecutor? = null
|
private var _audioExecutor: JSRequestExecutor? = null
|
||||||
@@ -145,6 +147,7 @@ abstract class StateCasting {
|
|||||||
device.onTimeChanged.clear();
|
device.onTimeChanged.clear();
|
||||||
device.onVolumeChanged.clear();
|
device.onVolumeChanged.clear();
|
||||||
device.onDurationChanged.clear();
|
device.onDurationChanged.clear();
|
||||||
|
device.onMediaItemEnd.clear();
|
||||||
ad.disconnect()
|
ad.disconnect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,6 +162,7 @@ abstract class StateCasting {
|
|||||||
device.onTimeChanged.clear();
|
device.onTimeChanged.clear();
|
||||||
device.onVolumeChanged.clear();
|
device.onVolumeChanged.clear();
|
||||||
device.onDurationChanged.clear();
|
device.onDurationChanged.clear();
|
||||||
|
device.onMediaItemEnd.clear();
|
||||||
activeDevice = null;
|
activeDevice = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,6 +226,9 @@ abstract class StateCasting {
|
|||||||
device.onTimeChanged.subscribe {
|
device.onTimeChanged.subscribe {
|
||||||
invokeInMainScopeIfRequired { onActiveDeviceTimeChanged.emit(it) };
|
invokeInMainScopeIfRequired { onActiveDeviceTimeChanged.emit(it) };
|
||||||
};
|
};
|
||||||
|
device.onMediaItemEnd.subscribe {
|
||||||
|
invokeInMainScopeIfRequired { onActiveDeviceMediaItemEnd.emit() }
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
device.connect();
|
device.connect();
|
||||||
@@ -232,6 +239,7 @@ abstract class StateCasting {
|
|||||||
device.onTimeChanged.clear();
|
device.onTimeChanged.clear();
|
||||||
device.onVolumeChanged.clear();
|
device.onVolumeChanged.clear();
|
||||||
device.onDurationChanged.clear();
|
device.onDurationChanged.clear();
|
||||||
|
device.onMediaItemEnd.clear();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1323,8 +1331,14 @@ abstract class StateCasting {
|
|||||||
return emptyList()
|
return emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var hasAudioInDash = false
|
||||||
for (representation in representationRegex.findAll(dashContent)) {
|
for (representation in representationRegex.findAll(dashContent)) {
|
||||||
val mediaType = representation.groups[1]?.value ?: throw Exception("Media type should be found")
|
val mediaType = representation.groups[1]?.value ?: throw Exception("Media type should be found")
|
||||||
|
|
||||||
|
if (mediaType.startsWith("audio/")) {
|
||||||
|
hasAudioInDash = true
|
||||||
|
}
|
||||||
|
|
||||||
dashContent = mediaInitializationRegex.replace(dashContent) {
|
dashContent = mediaInitializationRegex.replace(dashContent) {
|
||||||
if (it.range.first < representation.range.first || it.range.last > representation.range.last) {
|
if (it.range.first < representation.range.first || it.range.last > representation.range.last) {
|
||||||
return@replace it.value
|
return@replace it.value
|
||||||
@@ -1348,16 +1362,20 @@ abstract class StateCasting {
|
|||||||
throw Exception("Audio source without request executor not supported")
|
throw Exception("Audio source without request executor not supported")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (audioSource != null && audioSource.hasRequestExecutor) {
|
if (videoSource != null && videoSource.hasRequestExecutor) {
|
||||||
val oldExecutor = _audioExecutor;
|
val oldVideoExecutor = _videoExecutor
|
||||||
oldExecutor?.closeAsync();
|
oldVideoExecutor?.closeAsync()
|
||||||
_audioExecutor = audioSource.getRequestExecutor()
|
_videoExecutor = videoSource.getRequestExecutor()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (videoSource != null && videoSource.hasRequestExecutor) {
|
if (audioSource != null) {
|
||||||
val oldExecutor = _videoExecutor;
|
val oldExecutor = _audioExecutor
|
||||||
oldExecutor?.closeAsync();
|
oldExecutor?.closeAsync()
|
||||||
_videoExecutor = videoSource.getRequestExecutor()
|
_audioExecutor = audioSource.getRequestExecutor()
|
||||||
|
} else if (hasAudioInDash && videoSource != null) {
|
||||||
|
val oldExecutor = _audioExecutor
|
||||||
|
oldExecutor?.closeAsync()
|
||||||
|
_audioExecutor = _videoExecutor
|
||||||
}
|
}
|
||||||
|
|
||||||
//TOOD: Else also handle the non request executor case, perhaps add ?url=$originalUrl to the query parameters, ... propagate this for all other flows also
|
//TOOD: Else also handle the non request executor case, perhaps add ?url=$originalUrl to the query parameters, ... propagate this for all other flows also
|
||||||
@@ -1388,7 +1406,7 @@ abstract class StateCasting {
|
|||||||
}.withHeader("Access-Control-Allow-Origin", "*"), true
|
}.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
).withTag("castDashRaw");
|
).withTag("castDashRaw");
|
||||||
}
|
}
|
||||||
if (audioSource != null) {
|
if (audioSource != null || (audioSource == null && hasAudioInDash)) {
|
||||||
_castServer.addHandlerWithAllowAllOptions(
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
HttpFunctionHandler("GET", audioPath) { httpContext ->
|
HttpFunctionHandler("GET", audioPath) { httpContext ->
|
||||||
val originalUrl = httpContext.query["url"]?.let { URLDecoder.decode(it, "UTF-8") } ?: return@HttpFunctionHandler
|
val originalUrl = httpContext.query["url"]?.let { URLDecoder.decode(it, "UTF-8") } ?: return@HttpFunctionHandler
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ import com.arthenica.ffmpegkit.FFmpegKit
|
|||||||
import com.arthenica.ffmpegkit.ReturnCode
|
import com.arthenica.ffmpegkit.ReturnCode
|
||||||
import com.arthenica.ffmpegkit.StatisticsCallback
|
import com.arthenica.ffmpegkit.StatisticsCallback
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
|
import com.futo.platformplayer.api.media.IPlatformClient
|
||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
import com.futo.platformplayer.api.media.models.modifier.IRequestModifier
|
import com.futo.platformplayer.api.media.models.modifier.IRequestModifier
|
||||||
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
||||||
@@ -40,10 +42,13 @@ import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManif
|
|||||||
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.JSSource
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.engine.exceptions.ScriptException
|
||||||
|
import com.futo.platformplayer.engine.exceptions.ScriptReloadRequiredException
|
||||||
import com.futo.platformplayer.exceptions.DownloadException
|
import com.futo.platformplayer.exceptions.DownloadException
|
||||||
import com.futo.platformplayer.helpers.FileHelper.Companion.sanitizeFileName
|
import com.futo.platformplayer.helpers.FileHelper.Companion.sanitizeFileName
|
||||||
import com.futo.platformplayer.helpers.VideoHelper
|
import com.futo.platformplayer.helpers.VideoHelper
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.others.Language
|
||||||
import com.futo.platformplayer.parsers.HLS
|
import com.futo.platformplayer.parsers.HLS
|
||||||
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||||
import com.futo.platformplayer.states.StateDownloads
|
import com.futo.platformplayer.states.StateDownloads
|
||||||
@@ -86,6 +91,9 @@ import kotlin.time.times
|
|||||||
class VideoDownload {
|
class VideoDownload {
|
||||||
var state: State = State.QUEUED;
|
var state: State = State.QUEUED;
|
||||||
|
|
||||||
|
@Contextual
|
||||||
|
@Transient
|
||||||
|
var plugin: IPlatformClient? = null;
|
||||||
var video: SerializedPlatformVideo? = null;
|
var video: SerializedPlatformVideo? = null;
|
||||||
var videoDetails: SerializedPlatformVideoDetails? = null;
|
var videoDetails: SerializedPlatformVideoDetails? = null;
|
||||||
|
|
||||||
@@ -101,6 +109,7 @@ class VideoDownload {
|
|||||||
|
|
||||||
var videoSource: VideoUrlSource?;
|
var videoSource: VideoUrlSource?;
|
||||||
var audioSource: AudioUrlSource?;
|
var audioSource: AudioUrlSource?;
|
||||||
|
var overrideResultAudioSource: IAudioSource? = null;
|
||||||
@Contextual
|
@Contextual
|
||||||
@Transient
|
@Transient
|
||||||
val videoSourceToUse: IVideoSource? get () = if(requiresLiveVideoSource) videoSourceLive as IVideoSource? else videoSource as IVideoSource?;
|
val videoSourceToUse: IVideoSource? get () = if(requiresLiveVideoSource) videoSourceLive as IVideoSource? else videoSource as IVideoSource?;
|
||||||
@@ -270,7 +279,7 @@ class VideoDownload {
|
|||||||
|
|
||||||
//Fetch full video object and determine source
|
//Fetch full video object and determine source
|
||||||
if(video != null && videoDetails == null) {
|
if(video != null && videoDetails == null) {
|
||||||
val original = StatePlatform.instance.getContentDetails(video!!.url).await();
|
val original = if (plugin != null) plugin!!.getContentDetails(video!!.url) else StatePlatform.instance.getContentDetails(video!!.url)?.await();
|
||||||
if(original !is IPlatformVideoDetails)
|
if(original !is IPlatformVideoDetails)
|
||||||
throw IllegalStateException("Original content is not media?");
|
throw IllegalStateException("Original content is not media?");
|
||||||
|
|
||||||
@@ -437,6 +446,11 @@ class VideoDownload {
|
|||||||
videoFileNameBase = "${videoDetails!!.id.value!!} [${actualVideoSource!!.width}x${actualVideoSource!!.height}]".sanitizeFileName();
|
videoFileNameBase = "${videoDetails!!.id.value!!} [${actualVideoSource!!.width}x${actualVideoSource!!.height}]".sanitizeFileName();
|
||||||
videoFileNameExt = videoContainerToExtension(actualVideoSource!!.container);
|
videoFileNameExt = videoContainerToExtension(actualVideoSource!!.container);
|
||||||
videoFilePath = File(downloadDir, videoFileName!!).absolutePath;
|
videoFilePath = File(downloadDir, videoFileName!!).absolutePath;
|
||||||
|
if(actualVideoSource is JSDashManifestRawSource && actualAudioSource == null) {
|
||||||
|
audioFileNameBase = "${videoDetails!!.id.value!!}-[unknown]".sanitizeFileName();
|
||||||
|
audioFileNameExt = videoAudioContainerToExtension(actualVideoSource!!.container);
|
||||||
|
audioFilePath = File(downloadDir, audioFileName!!).absolutePath;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if(actualAudioSource != null) {
|
if(actualAudioSource != null) {
|
||||||
audioFileNameBase = "${videoDetails!!.id.value!!} [${actualAudioSource!!.language}-${actualAudioSource!!.bitrate}]".sanitizeFileName();
|
audioFileNameBase = "${videoDetails!!.id.value!!} [${actualAudioSource!!.language}-${actualAudioSource!!.bitrate}]".sanitizeFileName();
|
||||||
@@ -490,7 +504,11 @@ class VideoDownload {
|
|||||||
else -> downloadFileSource("Video", client, if (actualVideoSource is JSSource) actualVideoSource else null, videoSource!!.getVideoUrl(), File(downloadDir, videoFileName!!), progressCallback)
|
else -> downloadFileSource("Video", client, if (actualVideoSource is JSSource) actualVideoSource else null, videoSource!!.getVideoUrl(), File(downloadDir, videoFileName!!), progressCallback)
|
||||||
}
|
}
|
||||||
else if(actualVideoSource is JSDashManifestRawSource) {
|
else if(actualVideoSource is JSDashManifestRawSource) {
|
||||||
videoFileSize = downloadDashFileSource("Video", client, actualVideoSource, File(downloadDir, videoFileName!!), progressCallback);
|
if(actualAudioSource == null)
|
||||||
|
videoFileSize = downloadDashFileSource("Video", client, actualVideoSource, File(downloadDir, videoFileName!!), progressCallback, 3,
|
||||||
|
File(downloadDir, audioFileName!!));
|
||||||
|
else
|
||||||
|
videoFileSize = downloadDashFileSource("Video", client, actualVideoSource, File(downloadDir, videoFileName!!), progressCallback, 1);
|
||||||
}
|
}
|
||||||
else throw NotImplementedError("NotImplemented video download: " + actualVideoSource.javaClass.name);
|
else throw NotImplementedError("NotImplemented video download: " + actualVideoSource.javaClass.name);
|
||||||
});
|
});
|
||||||
@@ -530,7 +548,7 @@ class VideoDownload {
|
|||||||
else -> downloadFileSource("Audio", client, if (actualAudioSource is JSSource) actualAudioSource else null, audioSource!!.getAudioUrl(), File(downloadDir, audioFileName!!), progressCallback)
|
else -> downloadFileSource("Audio", client, if (actualAudioSource is JSSource) actualAudioSource else null, audioSource!!.getAudioUrl(), File(downloadDir, audioFileName!!), progressCallback)
|
||||||
}
|
}
|
||||||
else if(actualAudioSource is JSDashManifestRawAudioSource) {
|
else if(actualAudioSource is JSDashManifestRawAudioSource) {
|
||||||
audioFileSize = downloadDashFileSource("Audio", client, actualAudioSource, File(downloadDir, audioFileName!!), progressCallback);
|
audioFileSize = downloadDashFileSource("Audio", client, actualAudioSource, File(downloadDir, audioFileName!!), progressCallback, 2);
|
||||||
}
|
}
|
||||||
else throw NotImplementedError("NotImplemented audio download: " + actualAudioSource.javaClass.name);
|
else throw NotImplementedError("NotImplemented audio download: " + actualAudioSource.javaClass.name);
|
||||||
});
|
});
|
||||||
@@ -589,38 +607,54 @@ class VideoDownload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun combineSegments(context: Context, segmentFiles: List<File>, targetFile: File) = withContext(Dispatchers.IO) {
|
private suspend fun combineSegments(context: Context, segmentFiles: List<File>, targetFile: File) = withContext(Dispatchers.IO) {
|
||||||
suspendCancellableCoroutine { continuation ->
|
require(segmentFiles.isNotEmpty()) { "segmentFiles must not be empty" }
|
||||||
val fileList = File(context.cacheDir, "fileList-${UUID.randomUUID()}.txt")
|
|
||||||
fileList.writeText(segmentFiles.joinToString("\n") { "file '${it.absolutePath}'" })
|
suspendCancellableCoroutine { continuation ->
|
||||||
|
val concatInput = buildString {
|
||||||
|
append("concat:")
|
||||||
|
append(
|
||||||
|
segmentFiles.joinToString("|") { file ->
|
||||||
|
file.absolutePath
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val cmd = "-i \"$concatInput\" -c copy \"${targetFile.absolutePath}\""
|
||||||
|
|
||||||
val cmd = "-f concat -safe 0 -i \"${fileList.absolutePath}\" -c copy \"${targetFile.absolutePath}\""
|
|
||||||
val statisticsCallback = StatisticsCallback { _ ->
|
val statisticsCallback = StatisticsCallback { _ ->
|
||||||
//TODO: Show progress?
|
//No callback
|
||||||
}
|
}
|
||||||
|
|
||||||
val executorService = Executors.newSingleThreadExecutor()
|
val executorService = Executors.newSingleThreadExecutor()
|
||||||
val session = FFmpegKit.executeAsync(cmd,
|
|
||||||
{ session ->
|
val session = FFmpegKit.executeAsync(
|
||||||
if (ReturnCode.isSuccess(session.returnCode)) {
|
cmd,
|
||||||
fileList.delete()
|
{ completedSession ->
|
||||||
|
executorService.shutdown()
|
||||||
|
|
||||||
|
if (ReturnCode.isSuccess(completedSession.returnCode)) {
|
||||||
continuation.resumeWith(Result.success(Unit))
|
continuation.resumeWith(Result.success(Unit))
|
||||||
} else {
|
} else {
|
||||||
val errorMessage = if (ReturnCode.isCancel(session.returnCode)) {
|
val errorMessage = if (ReturnCode.isCancel(completedSession.returnCode)) {
|
||||||
"Command cancelled"
|
"Command cancelled"
|
||||||
} else {
|
} else {
|
||||||
"Command failed with state '${session.state}' and return code ${session.returnCode}, stack trace ${session.failStackTrace}"
|
"Command failed with state '${completedSession.state}' " +
|
||||||
|
"and return code ${completedSession.returnCode}, " +
|
||||||
|
"stack trace ${completedSession.failStackTrace}"
|
||||||
}
|
}
|
||||||
fileList.delete()
|
|
||||||
continuation.resumeWithException(RuntimeException(errorMessage))
|
continuation.resumeWithException(RuntimeException(errorMessage))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ Logger.v(TAG, it.message) },
|
{ log ->
|
||||||
|
Logger.v(TAG, log.message)
|
||||||
|
},
|
||||||
statisticsCallback,
|
statisticsCallback,
|
||||||
executorService
|
executorService
|
||||||
)
|
)
|
||||||
|
|
||||||
continuation.invokeOnCancellation {
|
continuation.invokeOnCancellation {
|
||||||
session.cancel()
|
session.cancel()
|
||||||
|
executorService.shutdownNow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -856,14 +890,19 @@ class VideoDownload {
|
|||||||
return downloadedTotalLength
|
return downloadedTotalLength
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun downloadDashFileSource(name: String, client: ManagedHttpClient, source: IJSDashManifestRawSource, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long {
|
private fun downloadDashFileSource(name: String, client: ManagedHttpClient, source: IJSDashManifestRawSource, targetFile: File, onProgress: (Long, Long, Long) -> Unit, downloadType: Int = 0, targetFileAudio: File? = null): Long {
|
||||||
if(targetFile.exists())
|
if(targetFile.exists())
|
||||||
targetFile.delete();
|
targetFile.delete();
|
||||||
|
if(targetFileAudio?.exists() ?: false)
|
||||||
|
targetFileAudio.delete();
|
||||||
|
|
||||||
targetFile.createNewFile();
|
targetFile.createNewFile();
|
||||||
|
targetFileAudio?.createNewFile();
|
||||||
|
|
||||||
val sourceLength: Long?;
|
val sourceLength: Long?;
|
||||||
|
val sourceLengthAudio: Long?;
|
||||||
val fileStream = FileOutputStream(targetFile);
|
val fileStream = FileOutputStream(targetFile);
|
||||||
|
val fileStream2 = if(targetFileAudio != null) FileOutputStream(targetFileAudio) else null;
|
||||||
|
|
||||||
var executor: JSRequestExecutor? = null;
|
var executor: JSRequestExecutor? = null;
|
||||||
try{
|
try{
|
||||||
@@ -874,14 +913,27 @@ class VideoDownload {
|
|||||||
throw IllegalStateException("No manifest after generation");
|
throw IllegalStateException("No manifest after generation");
|
||||||
|
|
||||||
//TODO: Temporary naive assume single-sourced dash
|
//TODO: Temporary naive assume single-sourced dash
|
||||||
val foundTemplate = REGEX_DASH_TEMPLATE.find(manifest);
|
val foundTemplates = REGEX_DASH_TEMPLATE_WITH_MIME.findAll(manifest);
|
||||||
if(foundTemplate == null || foundTemplate.groupValues.size != 3)
|
val foundTemplate = when(downloadType) {
|
||||||
|
1 -> foundTemplates.find({ it.groupValues[1].contains("video/") });
|
||||||
|
2 -> foundTemplates.find({ it.groupValues[1].contains("audio/") });
|
||||||
|
else -> foundTemplates.find({ it.groupValues[1].contains("video/") });
|
||||||
|
}
|
||||||
|
if(foundTemplate == null || foundTemplate.groupValues.size != 4)
|
||||||
throw IllegalStateException("No SegmentTemplate found in manifest (unsupported dash?)");
|
throw IllegalStateException("No SegmentTemplate found in manifest (unsupported dash?)");
|
||||||
val foundTemplateUrl = foundTemplate.groupValues[1];
|
val foundTemplateUrl = foundTemplate.groupValues[2];
|
||||||
val foundCues = REGEX_DASH_CUE.findAll(foundTemplate.groupValues[2]);
|
val foundCues = REGEX_DASH_CUE.findAll(foundTemplate.groupValues[3]).toList();
|
||||||
if(foundCues.count() <= 0)
|
if(foundCues.count() <= 0)
|
||||||
throw IllegalStateException("No Cues found in manifest (unsupported dash?)");
|
throw IllegalStateException("No Cues found in manifest (unsupported dash?)");
|
||||||
|
|
||||||
|
val foundTemplate2 = if(downloadType == 3) foundTemplates.find({ it.groupValues[1].contains("audio/") }); else null;
|
||||||
|
val foundTemplateUrl2 = if(foundTemplate2 != null) foundTemplate2.groupValues[2] else null;
|
||||||
|
val foundCues2 = if(foundTemplate2 != null) REGEX_DASH_CUE.findAll(foundTemplate2.groupValues[3]).toList() else null;
|
||||||
|
val foundCues2Downloaded = hashSetOf<MatchResult>();
|
||||||
|
|
||||||
|
if(foundTemplate2 != null)
|
||||||
|
overrideResultAudioSource = LocalAudioSource((videoSource?.name)?.let { it + " [audio]" } ?: "audio", "", 0, 0, foundTemplate2.groupValues[1], REGEX_CODECS.find(foundTemplate2.groupValues[0])?.groupValues?.get(1) ?: "", Language.UNKNOWN);
|
||||||
|
|
||||||
executor = if(source is JSSource && source.hasRequestExecutor)
|
executor = if(source is JSSource && source.hasRequestExecutor)
|
||||||
source.getRequestExecutor();
|
source.getRequestExecutor();
|
||||||
else
|
else
|
||||||
@@ -896,13 +948,17 @@ class VideoDownload {
|
|||||||
Logger.i(TAG, "Download $name Dash, CueCount: " + foundCues.count().toString());
|
Logger.i(TAG, "Download $name Dash, CueCount: " + foundCues.count().toString());
|
||||||
|
|
||||||
var written: Long = 0;
|
var written: Long = 0;
|
||||||
|
var written2: Long = 0;
|
||||||
var indexCounter = 0;
|
var indexCounter = 0;
|
||||||
|
var indexCounter2 = 0;
|
||||||
onProgress(foundCues.count().toLong(), 0, 0);
|
onProgress(foundCues.count().toLong(), 0, 0);
|
||||||
|
val totalCues = foundCues.count().toLong() + (foundCues2?.count()?.toLong() ?: 0)
|
||||||
|
val lastCue = foundCues.lastOrNull();
|
||||||
for(cue in foundCues) {
|
for(cue in foundCues) {
|
||||||
val t = cue.groupValues[1];
|
val t = cue.groupValues[1];
|
||||||
val d = cue.groupValues[2];
|
val d = cue.groupValues[2];
|
||||||
|
|
||||||
|
Logger.i(TAG, "Downloading cue ${indexCounter}")
|
||||||
val url = foundTemplateUrl.replace("\$Number\$", (indexCounter).toString());
|
val url = foundTemplateUrl.replace("\$Number\$", (indexCounter).toString());
|
||||||
val modified = modifier?.modifyRequest(url, mapOf());
|
val modified = modifier?.modifyRequest(url, mapOf());
|
||||||
|
|
||||||
@@ -918,17 +974,60 @@ class VideoDownload {
|
|||||||
speedTracker.addWork(data.size.toLong());
|
speedTracker.addWork(data.size.toLong());
|
||||||
written += data.size;
|
written += data.size;
|
||||||
|
|
||||||
onProgress(foundCues.count().toLong(), indexCounter.toLong(), speedTracker.lastSpeed);
|
onProgress(totalCues, indexCounter.toLong() + indexCounter2.toLong(), speedTracker.lastSpeed);
|
||||||
|
|
||||||
|
|
||||||
indexCounter++;
|
indexCounter++;
|
||||||
|
|
||||||
|
if(foundCues2 != null && foundTemplateUrl2 != null && fileStream2 != null) {
|
||||||
|
val toDownload = if(lastCue != null && cue == lastCue)
|
||||||
|
foundCues2.filter { !foundCues2Downloaded.contains(it) }.toList() else
|
||||||
|
foundCues2.filter { !foundCues2Downloaded.contains(it) && (it.groupValues[1].toLong()) < t.toLong() }.toList();
|
||||||
|
Logger.i(TAG, "Downloading audio cues (${toDownload.size})")
|
||||||
|
for(cue2 in toDownload) {
|
||||||
|
val index2 = foundCues2.indexOf(cue2);
|
||||||
|
val t2 = cue2.groupValues[1];
|
||||||
|
val d2 = cue2.groupValues[2];
|
||||||
|
val url2 = foundTemplateUrl2!!.replace("\$Number\$", (index2).toString());
|
||||||
|
val modified2 = modifier?.modifyRequest(url, mapOf());
|
||||||
|
|
||||||
|
val data = if(executor != null)
|
||||||
|
executor.executeRequest("GET", modified2?.url ?: url2, null, modified2?.headers ?: mapOf());
|
||||||
|
else {
|
||||||
|
val resp = client.get(modified2?.url ?: url, modified2?.headers?.toMutableMap() ?: mutableMapOf());
|
||||||
|
if(!resp.isOk)
|
||||||
|
throw IllegalStateException("Dash request2 failed for index " + indexCounter.toString() + ", with code: " + resp.code.toString());
|
||||||
|
resp.body!!.bytes()
|
||||||
|
}
|
||||||
|
fileStream2.write(data, 0, data.size);
|
||||||
|
speedTracker.addWork(data.size.toLong());
|
||||||
|
written2 += data.size;
|
||||||
|
indexCounter2++;
|
||||||
|
|
||||||
|
foundCues2Downloaded.add(cue2);
|
||||||
|
onProgress(totalCues, indexCounter.toLong() + indexCounter2.toLong(), speedTracker.lastSpeed);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
sourceLength = written;
|
sourceLength = written;
|
||||||
|
sourceLengthAudio = written2;
|
||||||
|
|
||||||
Logger.i(TAG, "$name downloadSource Finished");
|
Logger.i(TAG, "$name downloadSource Finished");
|
||||||
}
|
}
|
||||||
|
catch(scriptEx: ScriptReloadRequiredException) {
|
||||||
|
if(targetFile.exists() ?: false)
|
||||||
|
targetFile.delete();
|
||||||
|
if(targetFileAudio?.exists() ?: false)
|
||||||
|
targetFileAudio.delete();
|
||||||
|
|
||||||
|
createNewPluginClient();
|
||||||
|
throw scriptEx;
|
||||||
|
}
|
||||||
catch(ioex: IOException) {
|
catch(ioex: IOException) {
|
||||||
if(targetFile.exists() ?: false)
|
if(targetFile.exists() ?: false)
|
||||||
targetFile.delete();
|
targetFile.delete();
|
||||||
|
if(targetFileAudio?.exists() ?: false)
|
||||||
|
targetFileAudio.delete();
|
||||||
if(ioex.message?.contains("ENOSPC") ?: false)
|
if(ioex.message?.contains("ENOSPC") ?: false)
|
||||||
throw Exception("Not enough space on device", ioex);
|
throw Exception("Not enough space on device", ioex);
|
||||||
else
|
else
|
||||||
@@ -937,14 +1036,37 @@ class VideoDownload {
|
|||||||
catch(ex: Throwable) {
|
catch(ex: Throwable) {
|
||||||
if(targetFile.exists() ?: false)
|
if(targetFile.exists() ?: false)
|
||||||
targetFile.delete();
|
targetFile.delete();
|
||||||
|
if(targetFileAudio?.exists() ?: false)
|
||||||
|
targetFileAudio.delete();
|
||||||
throw ex;
|
throw ex;
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
fileStream.close();
|
fileStream.close();
|
||||||
|
fileStream2?.close();
|
||||||
executor?.closeAsync()
|
executor?.closeAsync()
|
||||||
}
|
}
|
||||||
|
if(sourceLengthAudio != null && sourceLengthAudio > 0)
|
||||||
|
audioFileSize = sourceLengthAudio
|
||||||
return sourceLength!!;
|
return sourceLength!!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun createNewPluginClient() {
|
||||||
|
UIDialogs.appToast("Download creating new client at request of plugin");
|
||||||
|
cleanupPluginClient();
|
||||||
|
plugin = (video?.url ?: videoDetails?.url)?.let { StatePlatform.instance.getContentClient(it) }?.let { if(it is JSClient) it else null }?.getCopy(false, true);
|
||||||
|
plugin?.initialize();
|
||||||
|
}
|
||||||
|
fun cleanupPluginClient() {
|
||||||
|
val oldPlugin = plugin;
|
||||||
|
plugin = null;
|
||||||
|
try {
|
||||||
|
oldPlugin?.disable();
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to dispose download client: ${ex.message}" , ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun downloadFileSource(name: String, client: ManagedHttpClient, source: JSSource?, videoUrl: String, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long {
|
private fun downloadFileSource(name: String, client: ManagedHttpClient, source: JSSource?, videoUrl: String, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long {
|
||||||
if(targetFile.exists())
|
if(targetFile.exists())
|
||||||
targetFile.delete();
|
targetFile.delete();
|
||||||
@@ -1304,7 +1426,7 @@ class VideoDownload {
|
|||||||
throw IllegalStateException("Expected size [${videoFileSize}], but found ${expectedFile.length()}");
|
throw IllegalStateException("Expected size [${videoFileSize}], but found ${expectedFile.length()}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(audioSourceToUse != null) {
|
if(audioSourceToUse != null || (videoSourceToUse is IJSDashManifestRawSource)) {
|
||||||
if(audioFilePath == null)
|
if(audioFilePath == null)
|
||||||
throw IllegalStateException("Missing audio file name after download");
|
throw IllegalStateException("Missing audio file name after download");
|
||||||
val expectedFile = File(audioFilePath!!);
|
val expectedFile = File(audioFilePath!!);
|
||||||
@@ -1327,7 +1449,7 @@ class VideoDownload {
|
|||||||
Logger.i(TAG, "VideoDownload Complete [${name}]");
|
Logger.i(TAG, "VideoDownload Complete [${name}]");
|
||||||
val existing = StateDownloads.instance.getCachedVideo(id);
|
val existing = StateDownloads.instance.getCachedVideo(id);
|
||||||
val localVideoSource = videoFilePath?.let { LocalVideoSource.fromSource(videoSourceToUse!!, it, videoFileSize ?: 0, videoOverrideContainer) };
|
val localVideoSource = videoFilePath?.let { LocalVideoSource.fromSource(videoSourceToUse!!, it, videoFileSize ?: 0, videoOverrideContainer) };
|
||||||
val localAudioSource = audioFilePath?.let { LocalAudioSource.fromSource(audioSourceToUse!!, it, audioFileSize ?: 0, audioOverrideContainer) };
|
val localAudioSource = audioFilePath?.let { LocalAudioSource.fromSource(overrideResultAudioSource ?: audioSourceToUse!!, it, audioFileSize ?: 0, audioOverrideContainer) };
|
||||||
val localSubtitleSource = subtitleFilePath?.let { LocalSubtitleSource.fromSource(subtitleSource!!, it) };
|
val localSubtitleSource = subtitleFilePath?.let { LocalSubtitleSource.fromSource(subtitleSource!!, it) };
|
||||||
|
|
||||||
if(localVideoSource != null && videoSourceToUse != null && videoSourceToUse is IStreamMetaDataSource)
|
if(localVideoSource != null && videoSourceToUse != null && videoSourceToUse is IStreamMetaDataSource)
|
||||||
@@ -1369,6 +1491,10 @@ class VideoDownload {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun cleanup(){
|
||||||
|
cleanupPluginClient()
|
||||||
|
}
|
||||||
|
|
||||||
enum class State {
|
enum class State {
|
||||||
QUEUED,
|
QUEUED,
|
||||||
PREPARING,
|
PREPARING,
|
||||||
@@ -1392,6 +1518,8 @@ class VideoDownload {
|
|||||||
const val GROUP_WATCHLATER= "WatchLater";
|
const val GROUP_WATCHLATER= "WatchLater";
|
||||||
|
|
||||||
val REGEX_DASH_TEMPLATE = Regex("<SegmentTemplate .*?media=\"(.*?)\".*?>(.*?)<\\/SegmentTemplate>", RegexOption.DOT_MATCHES_ALL);
|
val REGEX_DASH_TEMPLATE = Regex("<SegmentTemplate .*?media=\"(.*?)\".*?>(.*?)<\\/SegmentTemplate>", RegexOption.DOT_MATCHES_ALL);
|
||||||
|
val REGEX_DASH_TEMPLATE_WITH_MIME = Regex("<Representation.*?mimeType=\\\"(.*?)\\\".*?>.*?<SegmentTemplate .*?media=\\\"(.*?)\\\".*?>(.*?)<\\/SegmentTemplate>", RegexOption.DOT_MATCHES_ALL);
|
||||||
|
val REGEX_CODECS = Regex("codecs=\\\"(.*?)\\\"")
|
||||||
val REGEX_DASH_CUE = Regex("<S .*?t=\"([0-9]*?)\".*?d=\"([0-9]*?)\".*?\\/>", RegexOption.DOT_MATCHES_ALL);
|
val REGEX_DASH_CUE = Regex("<S .*?t=\"([0-9]*?)\".*?d=\"([0-9]*?)\".*?\\/>", RegexOption.DOT_MATCHES_ALL);
|
||||||
|
|
||||||
fun videoContainerToExtension(container: String): String? {
|
fun videoContainerToExtension(container: String): String? {
|
||||||
@@ -1411,6 +1539,16 @@ class VideoDownload {
|
|||||||
return "video";//throw IllegalStateException("Unknown container: " + container)
|
return "video";//throw IllegalStateException("Unknown container: " + container)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO: Change usages of this to an accurate container instead of infering it.
|
||||||
|
fun videoAudioContainerToExtension(container: String): String? {
|
||||||
|
if (container.contains("video/mp4") || container == "application/vnd.apple.mpegurl")
|
||||||
|
return "mp4a";
|
||||||
|
else if (container.contains("video/webm"))
|
||||||
|
return "webm";
|
||||||
|
else
|
||||||
|
return "mp4a";//throw IllegalStateException("Unknown container: " + container)
|
||||||
|
}
|
||||||
|
|
||||||
fun audioContainerToExtension(container: String): String {
|
fun audioContainerToExtension(container: String): String {
|
||||||
if (container.contains("audio/mp4"))
|
if (container.contains("audio/mp4"))
|
||||||
return "mp4a";
|
return "mp4a";
|
||||||
|
|||||||
+9
-6
@@ -723,15 +723,17 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
val activeDevice = StateCasting.instance.activeDevice;
|
val activeDevice = StateCasting.instance.activeDevice;
|
||||||
if (activeDevice != null) {
|
if (activeDevice != null) {
|
||||||
handlePlayChanged(it);
|
handlePlayChanged(it);
|
||||||
|
|
||||||
val v = video;
|
|
||||||
if (!it && v != null && v.duration - activeDevice.time.toLong() < 2L) {
|
|
||||||
Log.i(TAG, "Next video (loop?)")
|
|
||||||
nextVideo();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
StateCasting.instance.onActiveDeviceMediaItemEnd.subscribe(this) {
|
||||||
|
val activeDevice = StateCasting.instance.activeDevice;
|
||||||
|
if (activeDevice != null) {
|
||||||
|
Log.i(TAG, "Next video (loop?)")
|
||||||
|
nextVideo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
StateCasting.instance.onActiveDeviceTimeChanged.subscribe(this) {
|
StateCasting.instance.onActiveDeviceTimeChanged.subscribe(this) {
|
||||||
if (_isCasting) {
|
if (_isCasting) {
|
||||||
setLastPositionMilliseconds((it * 1000.0).toLong(), true);
|
setLastPositionMilliseconds((it * 1000.0).toLong(), true);
|
||||||
@@ -1273,6 +1275,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
StateCasting.instance.onActiveDevicePlayChanged.remove(this);
|
StateCasting.instance.onActiveDevicePlayChanged.remove(this);
|
||||||
StateCasting.instance.onActiveDeviceTimeChanged.remove(this);
|
StateCasting.instance.onActiveDeviceTimeChanged.remove(this);
|
||||||
StateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this);
|
StateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this);
|
||||||
|
StateCasting.instance.onActiveDeviceMediaItemEnd.remove(this)
|
||||||
StateApp.instance.preventPictureInPicture.remove(this);
|
StateApp.instance.preventPictureInPicture.remove(this);
|
||||||
StatePlayer.instance.onQueueChanged.remove(this);
|
StatePlayer.instance.onQueueChanged.remove(this);
|
||||||
StatePlayer.instance.onVideoChanging.remove(this);
|
StatePlayer.instance.onVideoChanging.remove(this);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import com.futo.platformplayer.Settings
|
|||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.downloads.VideoDownload
|
import com.futo.platformplayer.downloads.VideoDownload
|
||||||
|
import com.futo.platformplayer.engine.exceptions.ScriptReloadRequiredException
|
||||||
import com.futo.platformplayer.exceptions.DownloadException
|
import com.futo.platformplayer.exceptions.DownloadException
|
||||||
import com.futo.platformplayer.getNowDiffMinutes
|
import com.futo.platformplayer.getNowDiffMinutes
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
@@ -169,6 +170,7 @@ class DownloadService : Service() {
|
|||||||
Thread.sleep(500);
|
Thread.sleep(500);
|
||||||
}
|
}
|
||||||
catch(ex: Throwable) {
|
catch(ex: Throwable) {
|
||||||
|
//if(ex is ScriptReloadRequiredException)
|
||||||
Logger.e(TAG, "Download failed", ex);
|
Logger.e(TAG, "Download failed", ex);
|
||||||
if(currentVideo.video == null && currentVideo.videoDetails == null) {
|
if(currentVideo.video == null && currentVideo.videoDetails == null) {
|
||||||
//Corrupt?
|
//Corrupt?
|
||||||
|
|||||||
@@ -179,6 +179,7 @@ class StateDownloads {
|
|||||||
|
|
||||||
fun removeDownload(download: VideoDownload) {
|
fun removeDownload(download: VideoDownload) {
|
||||||
download.isCancelled = true;
|
download.isCancelled = true;
|
||||||
|
download.cleanup();
|
||||||
_downloading.delete(download);
|
_downloading.delete(download);
|
||||||
onDownloadsChanged.emit();
|
onDownloadsChanged.emit();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import com.futo.platformplayer.*
|
|||||||
import com.futo.platformplayer.downloads.VideoDownload
|
import com.futo.platformplayer.downloads.VideoDownload
|
||||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.services.DownloadService
|
||||||
import com.futo.platformplayer.states.StateDownloads
|
import com.futo.platformplayer.states.StateDownloads
|
||||||
import com.futo.platformplayer.views.others.ProgressBar
|
import com.futo.platformplayer.views.others.ProgressBar
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -32,6 +33,7 @@ class ActiveDownloadItem: LinearLayout {
|
|||||||
private val _videoState: TextView;
|
private val _videoState: TextView;
|
||||||
|
|
||||||
private val _videoCancel: TextView;
|
private val _videoCancel: TextView;
|
||||||
|
private val _videoRetry: TextView;
|
||||||
|
|
||||||
private val _scope: CoroutineScope;
|
private val _scope: CoroutineScope;
|
||||||
|
|
||||||
@@ -51,13 +53,14 @@ class ActiveDownloadItem: LinearLayout {
|
|||||||
_videoSpeed = findViewById(R.id.download_video_speed);
|
_videoSpeed = findViewById(R.id.download_video_speed);
|
||||||
|
|
||||||
_videoCancel = findViewById(R.id.download_cancel);
|
_videoCancel = findViewById(R.id.download_cancel);
|
||||||
|
_videoRetry = findViewById(R.id.download_retry);
|
||||||
|
|
||||||
_videoName.text = download.name;
|
_videoName.text = download.name;
|
||||||
_videoDuration.text = download.videoEither.duration.toHumanTime(false);
|
_videoDuration.text = download.videoEither.duration.toHumanTime(false);
|
||||||
_videoAuthor.text = download.videoEither.author.name;
|
_videoAuthor.text = download.videoEither.author.name;
|
||||||
|
|
||||||
_videoState.setOnClickListener {
|
_videoState.setOnClickListener {
|
||||||
UIDialogs.toast(context, _videoState.text.toString(), false);
|
UIDialogs.appToast(_videoState.text.toString(), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
Glide.with(_videoImage)
|
Glide.with(_videoImage)
|
||||||
@@ -72,6 +75,12 @@ class ActiveDownloadItem: LinearLayout {
|
|||||||
StateDownloads.instance.removeDownload(_download);
|
StateDownloads.instance.removeDownload(_download);
|
||||||
StateDownloads.instance.preventPlaylistDownload(_download);
|
StateDownloads.instance.preventPlaylistDownload(_download);
|
||||||
};
|
};
|
||||||
|
_videoRetry.setOnClickListener {
|
||||||
|
download.changeState(VideoDownload.State.QUEUED);
|
||||||
|
DownloadService.getOrCreateService(context) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_download.onProgressChanged.subscribe(this) {
|
_download.onProgressChanged.subscribe(this) {
|
||||||
_scope.launch(Dispatchers.Main) {
|
_scope.launch(Dispatchers.Main) {
|
||||||
@@ -122,16 +131,19 @@ class ActiveDownloadItem: LinearLayout {
|
|||||||
VideoDownload.State.DOWNLOADING -> {
|
VideoDownload.State.DOWNLOADING -> {
|
||||||
_videoBar.visibility = VISIBLE;
|
_videoBar.visibility = VISIBLE;
|
||||||
_videoSpeed.visibility = VISIBLE;
|
_videoSpeed.visibility = VISIBLE;
|
||||||
|
_videoRetry.visibility = GONE;
|
||||||
};
|
};
|
||||||
VideoDownload.State.ERROR -> {
|
VideoDownload.State.ERROR -> {
|
||||||
_videoState.setTextColor(Color.RED);
|
_videoState.setTextColor(Color.RED);
|
||||||
_videoState.text = _download.error ?: context.getString(R.string.error);
|
_videoState.text = _download.error ?: context.getString(R.string.error);
|
||||||
_videoBar.visibility = GONE;
|
_videoBar.visibility = GONE;
|
||||||
_videoSpeed.visibility = GONE;
|
_videoSpeed.visibility = GONE;
|
||||||
|
_videoRetry.visibility = VISIBLE;
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
_videoBar.visibility = GONE;
|
_videoBar.visibility = GONE;
|
||||||
_videoSpeed.visibility = GONE;
|
_videoSpeed.visibility = GONE;
|
||||||
|
_videoRetry.visibility = GONE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,6 +118,21 @@
|
|||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
android:layout_marginEnd="10dp" />
|
android:layout_marginEnd="10dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/downloaded_author"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:textSize="9dp"
|
||||||
|
android:textColor="@color/gray_e0"
|
||||||
|
android:fontFamily="@font/inter_extra_light"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/downloaded_video_name"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
tools:text="ShortCircuit"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:layout_marginStart="10dp" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/download_cancel"
|
android:id="@+id/download_cancel"
|
||||||
android:layout_width="60dp"
|
android:layout_width="60dp"
|
||||||
@@ -130,20 +145,20 @@
|
|||||||
android:background="@drawable/background_small_button"
|
android:background="@drawable/background_small_button"
|
||||||
android:textAlignment="center"
|
android:textAlignment="center"
|
||||||
android:text="@string/cancel" />
|
android:text="@string/cancel" />
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/download_retry"
|
||||||
|
android:layout_width="60dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:padding="2dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintRight_toRightOf="@id/download_cancel"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/download_cancel"
|
||||||
|
android:textSize="10dp"
|
||||||
|
android:background="@drawable/background_small_button"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:text="@string/retry" />
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/downloaded_author"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:textSize="9dp"
|
|
||||||
android:textColor="@color/gray_e0"
|
|
||||||
android:fontFamily="@font/inter_extra_light"
|
|
||||||
tools:text="ShortCircuit"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:layout_marginStart="10dp" />
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|||||||
Submodule app/src/stable/assets/sources/bilibili updated: 17d7aef314...b153339c93
Submodule app/src/stable/assets/sources/crunchyroll updated: 534bded369...a1714790c5
Submodule app/src/stable/assets/sources/curiositystream updated: f6eb2463f5...1ebf5da236
Submodule app/src/stable/assets/sources/odysee updated: 98a8df5a60...1c7a8a4974
Submodule app/src/stable/assets/sources/rumble updated: d24fc4cf8e...3b51471010
Submodule app/src/stable/assets/sources/tedtalks updated: b9528e44c5...292e459eef
Submodule app/src/stable/assets/sources/twitch updated: e4cdb5a32e...cebdad37a3
Submodule app/src/stable/assets/sources/youtube updated: ec5359ae16...5e903fa569
Submodule app/src/unstable/assets/sources/bilibili updated: 17d7aef314...b153339c93
Submodule app/src/unstable/assets/sources/crunchyroll updated: 534bded369...a1714790c5
Submodule app/src/unstable/assets/sources/curiositystream updated: f6eb2463f5...1ebf5da236
Submodule app/src/unstable/assets/sources/odysee updated: 98a8df5a60...1c7a8a4974
Submodule app/src/unstable/assets/sources/rumble updated: d24fc4cf8e...3b51471010
Submodule app/src/unstable/assets/sources/tedtalks updated: b9528e44c5...292e459eef
Submodule app/src/unstable/assets/sources/twitch updated: e4cdb5a32e...cebdad37a3
Submodule app/src/unstable/assets/sources/youtube updated: ec5359ae16...5e903fa569
Reference in New Issue
Block a user