Compare commits

...

4 Commits

Author SHA1 Message Date
Kelvin 3f9477c246 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-07-16 20:18:54 +02:00
Kelvin 05ed1e188e Logging and refs 2024-07-16 20:18:46 +02:00
Koen f3d06e49f8 Added setting to always proxy requests for FCast. Added logging to print dash manifests. 2024-07-15 10:16:54 +02:00
Koen f9a4b68967 Updated submodules. 2024-07-14 15:39:57 +02:00
10 changed files with 83 additions and 20 deletions
@@ -525,6 +525,10 @@ class Settings : FragmentedStorageFileJson() {
@Serializable(with = FlexibleBooleanSerializer::class)
var keepScreenOn: Boolean = true;
@FormField(R.string.always_proxy_requests, FieldForm.TOGGLE, R.string.always_proxy_requests_description, 1)
@Serializable(with = FlexibleBooleanSerializer::class)
var alwaysProxyRequests: Boolean = false;
/*TODO: Should we have a different casting quality?
@FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3)
@DropdownFieldOptionsId(R.array.preferred_quality_array)
@@ -7,6 +7,7 @@ import android.os.Looper
import android.util.Base64
import android.util.Log
import com.futo.platformplayer.BuildConfig
import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.http.server.ManagedHttpServer
@@ -452,14 +453,22 @@ class StateCasting {
}
}
} else {
val proxyStreams = Settings.instance.casting.alwaysProxyRequests;
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
val id = UUID.randomUUID();
if (videoSource is IVideoUrlSource) {
val videoPath = "/video-${id}"
val videoUrl = if(proxyStreams) url + videoPath else videoSource.getVideoUrl();
Logger.i(TAG, "Casting as singular video");
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoSource.getVideoUrl(), resumePosition, video.duration.toDouble(), speed);
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoUrl, resumePosition, video.duration.toDouble(), speed);
} else if (audioSource is IAudioUrlSource) {
val audioPath = "/audio-${id}"
val audioUrl = if(proxyStreams) url + audioPath else audioSource.getAudioUrl();
Logger.i(TAG, "Casting as singular audio");
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioSource.getAudioUrl(), resumePosition, video.duration.toDouble(), speed);
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioUrl, resumePosition, video.duration.toDouble(), speed);
} else if(videoSource is IHLSManifestSource) {
if (ad is ChromecastCastingDevice) {
if (proxyStreams || ad is ChromecastCastingDevice) {
Logger.i(TAG, "Casting as proxied HLS");
castProxiedHls(video, videoSource.url, videoSource.codec, resumePosition, speed);
} else {
@@ -467,7 +476,7 @@ class StateCasting {
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoSource.url, resumePosition, video.duration.toDouble(), speed);
}
} else if(audioSource is IHLSManifestAudioSource) {
if (ad is ChromecastCastingDevice) {
if (proxyStreams || ad is ChromecastCastingDevice) {
Logger.i(TAG, "Casting as proxied audio HLS");
castProxiedHls(video, audioSource.url, audioSource.codec, resumePosition, speed);
} else {
@@ -667,8 +676,11 @@ class StateCasting {
val audioUrl = url + audioPath;
val subtitleUrl = url + subtitlePath;
val dashContent = DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitleUrl);
Logger.v(TAG) { "Dash manifest: $dashContent" };
_castServer.addHandlerWithAllowAllOptions(
HttpConstantHandler("GET", dashPath, DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitleUrl),
HttpConstantHandler("GET", dashPath, dashContent,
"application/dash+xml")
.withHeader("Access-Control-Allow-Origin", "*"), true
).withTag("cast");
@@ -699,13 +711,17 @@ class StateCasting {
private suspend fun castDashDirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
val ad = activeDevice ?: return listOf();
val proxyStreams = Settings.instance.casting.alwaysProxyRequests || ad !is FCastCastingDevice;
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
val id = UUID.randomUUID();
val subtitlePath = "/subtitle-${id}";
val videoUrl = videoSource?.getVideoUrl();
val audioUrl = audioSource?.getAudioUrl();
val videoPath = "/video-${id}"
val audioPath = "/audio-${id}"
val subtitlePath = "/subtitle-${id}"
val videoUrl = if(proxyStreams) url + videoPath else videoSource?.getVideoUrl();
val audioUrl = if(proxyStreams) url + audioPath else audioSource?.getAudioUrl();
val subtitlesUri = if (subtitleSource != null) withContext(Dispatchers.IO) {
return@withContext subtitleSource.getSubtitlesURI();
@@ -734,13 +750,28 @@ class StateCasting {
}
}
if (videoSource != null) {
_castServer.addHandlerWithAllowAllOptions(
HttpProxyHandler("GET", videoPath, videoSource.getVideoUrl(), true)
.withInjectedHost()
.withHeader("Access-Control-Allow-Origin", "*"), true
).withTag("cast");
}
if (audioSource != null) {
_castServer.addHandlerWithAllowAllOptions(
HttpProxyHandler("GET", audioPath, audioSource.getAudioUrl(), true)
.withInjectedHost()
.withHeader("Access-Control-Allow-Origin", "*"), true
).withTag("cast");
}
val content = DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitlesUrl);
Logger.i(TAG, "Direct dash cast to casting device (videoUrl: $videoUrl, audioUrl: $audioUrl).");
Logger.v(TAG) { "Dash manifest: $content" };
ad.loadContent("application/dash+xml", content, resumePosition, video.duration.toDouble(), speed);
return listOf(videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "");
}
return listOf(videoUrl ?: "", audioUrl ?: "", subtitlesUrl ?: "", videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "", subtitlesUri.toString()); }
private fun castProxiedHls(video: IPlatformVideoDetails, sourceUrl: String, codec: String?, resumePosition: Double, speed: Double?): List<String> {
_castServer.removeAllHandlers("castProxiedHlsMaster")
@@ -1044,7 +1075,7 @@ class StateCasting {
private suspend fun castDashIndirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
val ad = activeDevice ?: return listOf();
val proxyStreams = ad !is FCastCastingDevice;
val proxyStreams = Settings.instance.casting.alwaysProxyRequests || ad !is FCastCastingDevice;
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
val id = UUID.randomUUID();
@@ -1090,8 +1121,11 @@ class StateCasting {
}
}
val dashContent = DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitlesUrl);
Logger.v(TAG) { "Dash manifest: $dashContent" };
_castServer.addHandlerWithAllowAllOptions(
HttpConstantHandler("GET", dashPath, DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitlesUrl),
HttpConstantHandler("GET", dashPath, dashContent,
"application/dash+xml")
.withHeader("Access-Control-Allow-Origin", "*"), true
).withTag("cast");
@@ -127,7 +127,7 @@ class VideoHelper {
}
@OptIn(UnstableApi::class)
fun convertItagSourceToChunkedDashSource(videoSource: JSVideoUrlRangeSource) : MediaSource {
fun convertItagSourceToChunkedDashSource(videoSource: JSVideoUrlRangeSource) : Pair<MediaSource, String> {
val urlToUse = videoSource.getVideoUrl();
val manifestConfig = ProgressiveDashManifestCreator.fromVideoProgressiveStreamingUrl(urlToUse,
videoSource.duration * 1000,
@@ -145,10 +145,10 @@ class VideoHelper {
);
val manifest = DashManifestParser().parse(Uri.parse(""), manifestConfig.byteInputStream());
return DashMediaSource.Factory(ResolvingDataSource.Factory(videoSource.getHttpDataSourceFactory(), ResolvingDataSource.Resolver { dataSpec ->
return Pair(DashMediaSource.Factory(ResolvingDataSource.Factory(videoSource.getHttpDataSourceFactory(), ResolvingDataSource.Resolver { dataSpec ->
Logger.v("PLAYBACK", "Video REQ Range [" + dataSpec.position + "-" + (dataSpec.position + dataSpec.length) + "](" + dataSpec.length + ")", null);
return@Resolver dataSpec;
})).createMediaSource(manifest, MediaItem.Builder().setUri(Uri.parse(videoSource.getVideoUrl())).build())
})).createMediaSource(manifest, MediaItem.Builder().setUri(Uri.parse(videoSource.getVideoUrl())).build()), manifestConfig);
}
fun getMediaMetadata(media: IPlatformVideoDetails): MediaMetadata {
@@ -14,6 +14,7 @@ import androidx.media3.common.text.CueGroup
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DefaultDataSource
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.drm.DefaultDrmSessionManagerProvider
@@ -26,6 +27,7 @@ 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
@@ -36,17 +38,21 @@ 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.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
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.google.gson.Gson
import getHttpDataSourceFactory
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -68,6 +74,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
private set;
private var _lastVideoMediaSource: MediaSource? = null;
private var _lastGeneratedDash: String? = null;
private var _lastAudioMediaSource: MediaSource? = null;
private var _lastSubtitleMediaSource: MediaSource? = null;
private var _shouldPlaybackRestartOnConnectivity: Boolean = false;
@@ -375,6 +382,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
private fun swapSourceInternal(videoSource: IVideoSource?) {
_lastGeneratedDash = null;
when(videoSource) {
is LocalVideoSource -> swapVideoSourceLocal(videoSource);
is JSVideoUrlRangeSource -> swapVideoSourceUrlRange(videoSource);
@@ -415,7 +423,9 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
if(videoSource.hasItag) {
//Temporary workaround for Youtube
try {
_lastVideoMediaSource = VideoHelper.convertItagSourceToChunkedDashSource(videoSource);
val results = VideoHelper.convertItagSourceToChunkedDashSource(videoSource);
_lastGeneratedDash = results.second;
_lastVideoMediaSource = results.first;
return;
}
//If it fails to create the dash workaround, fallback to standard progressive
@@ -635,6 +645,16 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
when (error.errorCode) {
PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS -> {
if(error.cause is HttpDataSource.InvalidResponseCodeException) {
val cause = error.cause as HttpDataSource.InvalidResponseCodeException
Logger.v(TAG, null) {
"ERROR BAD HTTP ${cause.responseCode},\n" +
"Video Source: ${V8RemoteObject.gsonStandard.toJson(lastVideoSource)}\n" +
"Audio Source: ${V8RemoteObject.gsonStandard.toJson(lastAudioSource)}\n" +
"Dash: ${_lastGeneratedDash}"
};
}
onDatasourceError.emit(error);
}
//PlaybackException.ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED,
@@ -25,6 +25,7 @@ import androidx.media3.datasource.HttpDataSource;
import androidx.media3.datasource.HttpUtil;
import androidx.media3.datasource.TransferListener;
import com.futo.platformplayer.logging.Logger;
import com.google.common.base.Predicate;
import com.google.common.collect.ForwardingMap;
import com.google.common.collect.ImmutableMap;
@@ -582,6 +583,8 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
requestHeaders = result.getHeaders();
}
Logger.Companion.v("JSHttpDataSource", "DataSource REQ: " + requestUrl, null);
HttpURLConnection connection = openConnection(new URL(requestUrl));
connection.setConnectTimeout(connectTimeoutMillis);
connection.setReadTimeout(readTimeoutMillis);
+2
View File
@@ -66,6 +66,8 @@
<string name="enabled">Enabled</string>
<string name="keep_screen_on">Keep screen on</string>
<string name="keep_screen_on_while_casting">Keep screen on while casting</string>
<string name="always_proxy_requests">Always proxy requests</string>
<string name="always_proxy_requests_description">Always proxy requests when casting data through the device.</string>
<string name="discover">Discover</string>
<string name="find_new_video_sources_to_add">Find new video sources to add</string>
<string name="these_sources_have_been_disabled">These sources have been disabled</string>