mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-05-24 18:42:10 +02:00
Added support for playback rate changing when casting.
This commit is contained in:
parent
d3ab8ecf3a
commit
14d579eb1b
@ -1,12 +1,17 @@
|
|||||||
package com.futo.platformplayer.casting
|
package com.futo.platformplayer.casting
|
||||||
|
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import com.futo.platformplayer.logging.Logger
|
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.getConnectedSocket
|
import com.futo.platformplayer.getConnectedSocket
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.CastingDeviceInfo
|
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||||
import com.futo.platformplayer.toInetAddress
|
import com.futo.platformplayer.toInetAddress
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@ -18,7 +23,7 @@ class AirPlayCastingDevice : CastingDevice {
|
|||||||
override var usedRemoteAddress: InetAddress? = null;
|
override var usedRemoteAddress: InetAddress? = null;
|
||||||
override var localAddress: InetAddress? = null;
|
override var localAddress: InetAddress? = null;
|
||||||
override val canSetVolume: Boolean get() = false;
|
override val canSetVolume: Boolean get() = false;
|
||||||
override val canSetSpeed: Boolean get() = false; //TODO: Implement playback speed for AirPlay
|
override val canSetSpeed: Boolean get() = true;
|
||||||
|
|
||||||
var addresses: Array<InetAddress>? = null;
|
var addresses: Array<InetAddress>? = null;
|
||||||
var port: Int = 0;
|
var port: Int = 0;
|
||||||
@ -59,6 +64,10 @@ class AirPlayCastingDevice : CastingDevice {
|
|||||||
} else {
|
} else {
|
||||||
post("play", "text/parameters", "Content-Location: $contentId\r\nStart-Position: 0");
|
post("play", "text/parameters", "Content-Location: $contentId\r\nStart-Position: 0");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (speed != null) {
|
||||||
|
changeSpeed(speed)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun loadContent(contentType: String, content: String, resumePosition: Double, duration: Double, speed: Double?) {
|
override fun loadContent(contentType: String, content: String, resumePosition: Double, duration: Double, speed: Double?) {
|
||||||
@ -186,6 +195,11 @@ class AirPlayCastingDevice : CastingDevice {
|
|||||||
_scopeIO = null;
|
_scopeIO = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun changeSpeed(speed: Double) {
|
||||||
|
this.speed = speed
|
||||||
|
post("rate?value=$speed")
|
||||||
|
}
|
||||||
|
|
||||||
override fun getDeviceInfo(): CastingDeviceInfo {
|
override fun getDeviceInfo(): CastingDeviceInfo {
|
||||||
return CastingDeviceInfo(name!!, CastProtocolType.AIRPLAY, addresses!!.filter { a -> a.hostAddress != null }.map { a -> a.hostAddress!! }.toTypedArray(), port);
|
return CastingDeviceInfo(name!!, CastProtocolType.AIRPLAY, addresses!!.filter { a -> a.hostAddress != null }.map { a -> a.hostAddress!! }.toTypedArray(), port);
|
||||||
}
|
}
|
||||||
|
@ -81,7 +81,7 @@ abstract class CastingDevice {
|
|||||||
var speed: Double = 1.0
|
var speed: Double = 1.0
|
||||||
set(value) {
|
set(value) {
|
||||||
val changed = value != field;
|
val changed = value != field;
|
||||||
speed = value;
|
field = value;
|
||||||
if (changed) {
|
if (changed) {
|
||||||
onSpeedChanged.emit(value);
|
onSpeedChanged.emit(value);
|
||||||
}
|
}
|
||||||
|
@ -3,14 +3,24 @@ package com.futo.platformplayer.casting
|
|||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.casting.models.*
|
import com.futo.platformplayer.casting.models.FCastPlayMessage
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.casting.models.FCastPlaybackErrorMessage
|
||||||
|
import com.futo.platformplayer.casting.models.FCastPlaybackUpdateMessage
|
||||||
|
import com.futo.platformplayer.casting.models.FCastSeekMessage
|
||||||
|
import com.futo.platformplayer.casting.models.FCastSetSpeedMessage
|
||||||
|
import com.futo.platformplayer.casting.models.FCastSetVolumeMessage
|
||||||
|
import com.futo.platformplayer.casting.models.FCastVersionMessage
|
||||||
|
import com.futo.platformplayer.casting.models.FCastVolumeUpdateMessage
|
||||||
import com.futo.platformplayer.getConnectedSocket
|
import com.futo.platformplayer.getConnectedSocket
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.CastingDeviceInfo
|
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||||
import com.futo.platformplayer.toHexString
|
import com.futo.platformplayer.toHexString
|
||||||
import com.futo.platformplayer.toInetAddress
|
import com.futo.platformplayer.toInetAddress
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.io.DataInputStream
|
import java.io.DataInputStream
|
||||||
@ -89,6 +99,8 @@ class FCastCastingDevice : CastingDevice {
|
|||||||
time = resumePosition,
|
time = resumePosition,
|
||||||
speed = speed
|
speed = speed
|
||||||
));
|
));
|
||||||
|
|
||||||
|
this.speed = speed ?: 1.0
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun loadContent(contentType: String, content: String, resumePosition: Double, duration: Double, speed: Double?) {
|
override fun loadContent(contentType: String, content: String, resumePosition: Double, duration: Double, speed: Double?) {
|
||||||
@ -110,6 +122,8 @@ class FCastCastingDevice : CastingDevice {
|
|||||||
time = resumePosition,
|
time = resumePosition,
|
||||||
speed = speed
|
speed = speed
|
||||||
));
|
));
|
||||||
|
|
||||||
|
this.speed = speed ?: 1.0
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun changeVolume(volume: Double) {
|
override fun changeVolume(volume: Double) {
|
||||||
@ -122,12 +136,12 @@ class FCastCastingDevice : CastingDevice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun changeSpeed(speed: Double) {
|
override fun changeSpeed(speed: Double) {
|
||||||
if (invokeInIOScopeIfRequired({ changeSpeed(volume) })) {
|
if (invokeInIOScopeIfRequired({ changeSpeed(speed) })) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.speed = speed
|
this.speed = speed
|
||||||
sendMessage(Opcode.SET_SPEED, FCastSetSpeedMessage(volume))
|
sendMessage(Opcode.SET_SPEED, FCastSetSpeedMessage(speed))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun seekVideo(timeSeconds: Double) {
|
override fun seekVideo(timeSeconds: Double) {
|
||||||
|
@ -262,6 +262,7 @@ class StateCasting {
|
|||||||
try {
|
try {
|
||||||
jmDNS.removeServiceListener("_googlecast._tcp.local.", _chromecastServiceListener);
|
jmDNS.removeServiceListener("_googlecast._tcp.local.", _chromecastServiceListener);
|
||||||
jmDNS.removeServiceListener("_airplay._tcp", _airPlayServiceListener);
|
jmDNS.removeServiceListener("_airplay._tcp", _airPlayServiceListener);
|
||||||
|
jmDNS.removeServiceListener("_fastcast._tcp.local.", _fastCastServiceListener);
|
||||||
|
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
jmDNS.removeServiceTypeListener(_serviceTypeListener);
|
jmDNS.removeServiceTypeListener(_serviceTypeListener);
|
||||||
@ -382,7 +383,7 @@ class StateCasting {
|
|||||||
action();
|
action();
|
||||||
}
|
}
|
||||||
|
|
||||||
fun castIfAvailable(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: ISubtitleSource?, ms: Long = -1): Boolean {
|
fun castIfAvailable(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: ISubtitleSource?, ms: Long = -1, speed: Double?): Boolean {
|
||||||
val ad = activeDevice ?: return false;
|
val ad = activeDevice ?: return false;
|
||||||
if (ad.connectionState != CastConnectionState.CONNECTED) {
|
if (ad.connectionState != CastConnectionState.CONNECTED) {
|
||||||
return false;
|
return false;
|
||||||
@ -403,23 +404,23 @@ class StateCasting {
|
|||||||
if (videoSource is LocalVideoSource || audioSource is LocalAudioSource || subtitleSource is LocalSubtitleSource) {
|
if (videoSource is LocalVideoSource || audioSource is LocalAudioSource || subtitleSource is LocalSubtitleSource) {
|
||||||
if (ad is AirPlayCastingDevice) {
|
if (ad is AirPlayCastingDevice) {
|
||||||
Logger.i(TAG, "Casting as local HLS");
|
Logger.i(TAG, "Casting as local HLS");
|
||||||
castLocalHls(video, videoSource as LocalVideoSource?, audioSource as LocalAudioSource?, subtitleSource as LocalSubtitleSource?, resumePosition);
|
castLocalHls(video, videoSource as LocalVideoSource?, audioSource as LocalAudioSource?, subtitleSource as LocalSubtitleSource?, resumePosition, speed);
|
||||||
} else {
|
} else {
|
||||||
Logger.i(TAG, "Casting as local DASH");
|
Logger.i(TAG, "Casting as local DASH");
|
||||||
castLocalDash(video, videoSource as LocalVideoSource?, audioSource as LocalAudioSource?, subtitleSource as LocalSubtitleSource?, resumePosition);
|
castLocalDash(video, videoSource as LocalVideoSource?, audioSource as LocalAudioSource?, subtitleSource as LocalSubtitleSource?, resumePosition, speed);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
StateApp.instance.scope.launch(Dispatchers.IO) {
|
StateApp.instance.scope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
if (ad is FCastCastingDevice) {
|
if (ad is FCastCastingDevice) {
|
||||||
Logger.i(TAG, "Casting as DASH direct");
|
Logger.i(TAG, "Casting as DASH direct");
|
||||||
castDashDirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition);
|
castDashDirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
|
||||||
} else if (ad is AirPlayCastingDevice) {
|
} else if (ad is AirPlayCastingDevice) {
|
||||||
Logger.i(TAG, "Casting as HLS indirect");
|
Logger.i(TAG, "Casting as HLS indirect");
|
||||||
castHlsIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition);
|
castHlsIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
|
||||||
} else {
|
} else {
|
||||||
Logger.i(TAG, "Casting as DASH indirect");
|
Logger.i(TAG, "Casting as DASH indirect");
|
||||||
castDashIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition);
|
castDashIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to start casting DASH videoSource=${videoSource} audioSource=${audioSource}.", e);
|
Logger.e(TAG, "Failed to start casting DASH videoSource=${videoSource} audioSource=${audioSource}.", e);
|
||||||
@ -429,32 +430,32 @@ class StateCasting {
|
|||||||
} else {
|
} else {
|
||||||
if (videoSource is IVideoUrlSource) {
|
if (videoSource is IVideoUrlSource) {
|
||||||
Logger.i(TAG, "Casting as singular video");
|
Logger.i(TAG, "Casting as singular video");
|
||||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoSource.getVideoUrl(), resumePosition, video.duration.toDouble(), null);
|
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoSource.getVideoUrl(), resumePosition, video.duration.toDouble(), speed);
|
||||||
} else if (audioSource is IAudioUrlSource) {
|
} else if (audioSource is IAudioUrlSource) {
|
||||||
Logger.i(TAG, "Casting as singular audio");
|
Logger.i(TAG, "Casting as singular audio");
|
||||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioSource.getAudioUrl(), resumePosition, video.duration.toDouble(), null);
|
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioSource.getAudioUrl(), resumePosition, video.duration.toDouble(), speed);
|
||||||
} else if(videoSource is IHLSManifestSource) {
|
} else if(videoSource is IHLSManifestSource) {
|
||||||
if (ad is ChromecastCastingDevice) {
|
if (ad is ChromecastCastingDevice) {
|
||||||
Logger.i(TAG, "Casting as proxied HLS");
|
Logger.i(TAG, "Casting as proxied HLS");
|
||||||
castProxiedHls(video, videoSource.url, videoSource.codec, resumePosition);
|
castProxiedHls(video, videoSource.url, videoSource.codec, resumePosition, speed);
|
||||||
} else {
|
} else {
|
||||||
Logger.i(TAG, "Casting as non-proxied HLS");
|
Logger.i(TAG, "Casting as non-proxied HLS");
|
||||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoSource.url, resumePosition, video.duration.toDouble(), null);
|
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoSource.url, resumePosition, video.duration.toDouble(), speed);
|
||||||
}
|
}
|
||||||
} else if(audioSource is IHLSManifestAudioSource) {
|
} else if(audioSource is IHLSManifestAudioSource) {
|
||||||
if (ad is ChromecastCastingDevice) {
|
if (ad is ChromecastCastingDevice) {
|
||||||
Logger.i(TAG, "Casting as proxied audio HLS");
|
Logger.i(TAG, "Casting as proxied audio HLS");
|
||||||
castProxiedHls(video, audioSource.url, audioSource.codec, resumePosition);
|
castProxiedHls(video, audioSource.url, audioSource.codec, resumePosition, speed);
|
||||||
} else {
|
} else {
|
||||||
Logger.i(TAG, "Casting as non-proxied audio HLS");
|
Logger.i(TAG, "Casting as non-proxied audio HLS");
|
||||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioSource.url, resumePosition, video.duration.toDouble(), null);
|
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioSource.url, resumePosition, video.duration.toDouble(), speed);
|
||||||
}
|
}
|
||||||
} else if (videoSource is LocalVideoSource) {
|
} else if (videoSource is LocalVideoSource) {
|
||||||
Logger.i(TAG, "Casting as local video");
|
Logger.i(TAG, "Casting as local video");
|
||||||
castLocalVideo(video, videoSource, resumePosition);
|
castLocalVideo(video, videoSource, resumePosition, speed);
|
||||||
} else if (audioSource is LocalAudioSource) {
|
} else if (audioSource is LocalAudioSource) {
|
||||||
Logger.i(TAG, "Casting as local audio");
|
Logger.i(TAG, "Casting as local audio");
|
||||||
castLocalAudio(video, audioSource, resumePosition);
|
castLocalAudio(video, audioSource, resumePosition, speed);
|
||||||
} else {
|
} else {
|
||||||
var str = listOf(
|
var str = listOf(
|
||||||
if(videoSource != null) "Video: ${videoSource::class.java.simpleName}" else null,
|
if(videoSource != null) "Video: ${videoSource::class.java.simpleName}" else null,
|
||||||
@ -492,15 +493,7 @@ class StateCasting {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun castVideoIndirect() {
|
private fun castLocalVideo(video: IPlatformVideoDetails, videoSource: LocalVideoSource, resumePosition: Double, speed: Double?) : List<String> {
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun castAudioIndirect() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun castLocalVideo(video: IPlatformVideoDetails, videoSource: LocalVideoSource, resumePosition: Double) : List<String> {
|
|
||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
|
|
||||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
||||||
@ -514,12 +507,12 @@ class StateCasting {
|
|||||||
).withTag("cast");
|
).withTag("cast");
|
||||||
|
|
||||||
Logger.i(TAG, "Casting local video (videoUrl: $videoUrl).");
|
Logger.i(TAG, "Casting local video (videoUrl: $videoUrl).");
|
||||||
ad.loadVideo("BUFFERED", videoSource.container, videoUrl, resumePosition, video.duration.toDouble(), null);
|
ad.loadVideo("BUFFERED", videoSource.container, videoUrl, resumePosition, video.duration.toDouble(), speed);
|
||||||
|
|
||||||
return listOf(videoUrl);
|
return listOf(videoUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun castLocalAudio(video: IPlatformVideoDetails, audioSource: LocalAudioSource, resumePosition: Double) : List<String> {
|
private fun castLocalAudio(video: IPlatformVideoDetails, audioSource: LocalAudioSource, resumePosition: Double, speed: Double?) : List<String> {
|
||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
|
|
||||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
||||||
@ -533,12 +526,12 @@ class StateCasting {
|
|||||||
).withTag("cast");
|
).withTag("cast");
|
||||||
|
|
||||||
Logger.i(TAG, "Casting local audio (audioUrl: $audioUrl).");
|
Logger.i(TAG, "Casting local audio (audioUrl: $audioUrl).");
|
||||||
ad.loadVideo("BUFFERED", audioSource.container, audioUrl, resumePosition, video.duration.toDouble(), null);
|
ad.loadVideo("BUFFERED", audioSource.container, audioUrl, resumePosition, video.duration.toDouble(), speed);
|
||||||
|
|
||||||
return listOf(audioUrl);
|
return listOf(audioUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun castLocalHls(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double): List<String> {
|
private fun castLocalHls(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double, speed: Double?): List<String> {
|
||||||
val ad = activeDevice ?: return listOf()
|
val ad = activeDevice ?: return listOf()
|
||||||
|
|
||||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}"
|
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}"
|
||||||
@ -629,12 +622,12 @@ class StateCasting {
|
|||||||
).withTag("castLocalHls")
|
).withTag("castLocalHls")
|
||||||
|
|
||||||
Logger.i(TAG, "added new castLocalHls handlers (hlsPath: $hlsPath, videoPath: $videoPath, audioPath: $audioPath, subtitlePath: $subtitlePath).")
|
Logger.i(TAG, "added new castLocalHls handlers (hlsPath: $hlsPath, videoPath: $videoPath, audioPath: $audioPath, subtitlePath: $subtitlePath).")
|
||||||
ad.loadVideo("BUFFERED", "application/vnd.apple.mpegurl", hlsUrl, resumePosition, video.duration.toDouble(), null)
|
ad.loadVideo("BUFFERED", "application/vnd.apple.mpegurl", hlsUrl, resumePosition, video.duration.toDouble(), speed)
|
||||||
|
|
||||||
return listOf(hlsUrl, videoUrl, audioUrl, subtitleUrl)
|
return listOf(hlsUrl, videoUrl, audioUrl, subtitleUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun castLocalDash(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double) : List<String> {
|
private fun castLocalDash(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
|
|
||||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
||||||
@ -675,12 +668,12 @@ class StateCasting {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Logger.i(TAG, "added new castLocalDash handlers (dashPath: $dashPath, videoPath: $videoPath, audioPath: $audioPath, subtitlePath: $subtitlePath).");
|
Logger.i(TAG, "added new castLocalDash handlers (dashPath: $dashPath, videoPath: $videoPath, audioPath: $audioPath, subtitlePath: $subtitlePath).");
|
||||||
ad.loadVideo("BUFFERED", "application/dash+xml", dashUrl, resumePosition, video.duration.toDouble(), null);
|
ad.loadVideo("BUFFERED", "application/dash+xml", dashUrl, resumePosition, video.duration.toDouble(), speed);
|
||||||
|
|
||||||
return listOf(dashUrl, videoUrl, audioUrl, subtitleUrl);
|
return listOf(dashUrl, videoUrl, audioUrl, subtitleUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun castDashDirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double) : List<String> {
|
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 ad = activeDevice ?: return listOf();
|
||||||
|
|
||||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
||||||
@ -720,12 +713,12 @@ class StateCasting {
|
|||||||
val content = DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitlesUrl);
|
val content = DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitlesUrl);
|
||||||
|
|
||||||
Logger.i(TAG, "Direct dash cast to casting device (videoUrl: $videoUrl, audioUrl: $audioUrl).");
|
Logger.i(TAG, "Direct dash cast to casting device (videoUrl: $videoUrl, audioUrl: $audioUrl).");
|
||||||
ad.loadContent("application/dash+xml", content, resumePosition, video.duration.toDouble(), null);
|
ad.loadContent("application/dash+xml", content, resumePosition, video.duration.toDouble(), speed);
|
||||||
|
|
||||||
return listOf(videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "");
|
return listOf(videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "");
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun castProxiedHls(video: IPlatformVideoDetails, sourceUrl: String, codec: String?, resumePosition: Double): List<String> {
|
private fun castProxiedHls(video: IPlatformVideoDetails, sourceUrl: String, codec: String?, resumePosition: Double, speed: Double?): List<String> {
|
||||||
_castServer.removeAllHandlers("castProxiedHlsMaster")
|
_castServer.removeAllHandlers("castProxiedHlsMaster")
|
||||||
|
|
||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
@ -846,7 +839,7 @@ class StateCasting {
|
|||||||
|
|
||||||
//ChromeCast is sometimes funky with resume position 0
|
//ChromeCast is sometimes funky with resume position 0
|
||||||
val hackfixResumePosition = if (ad is ChromecastCastingDevice && !video.isLive && resumePosition == 0.0) 0.1 else resumePosition;
|
val hackfixResumePosition = if (ad is ChromecastCastingDevice && !video.isLive && resumePosition == 0.0) 0.1 else resumePosition;
|
||||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", "application/vnd.apple.mpegurl", hlsUrl, hackfixResumePosition, video.duration.toDouble(), null);
|
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", "application/vnd.apple.mpegurl", hlsUrl, hackfixResumePosition, video.duration.toDouble(), speed);
|
||||||
|
|
||||||
return listOf(hlsUrl);
|
return listOf(hlsUrl);
|
||||||
}
|
}
|
||||||
@ -897,7 +890,7 @@ class StateCasting {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun castHlsIndirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double) : List<String> {
|
private suspend fun castHlsIndirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
@ -1020,12 +1013,12 @@ class StateCasting {
|
|||||||
).withTag("castHlsIndirectMaster")
|
).withTag("castHlsIndirectMaster")
|
||||||
|
|
||||||
Logger.i(TAG, "added new castHls handlers (hlsPath: $hlsPath).");
|
Logger.i(TAG, "added new castHls handlers (hlsPath: $hlsPath).");
|
||||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", "application/vnd.apple.mpegurl", hlsUrl, resumePosition, video.duration.toDouble(), null);
|
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", "application/vnd.apple.mpegurl", hlsUrl, resumePosition, video.duration.toDouble(), speed);
|
||||||
|
|
||||||
return listOf(hlsUrl, videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "", subtitlesUri.toString());
|
return listOf(hlsUrl, videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "", subtitlesUri.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun castDashIndirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double) : List<String> {
|
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 ad = activeDevice ?: return listOf();
|
||||||
val proxyStreams = ad !is FCastCastingDevice;
|
val proxyStreams = ad !is FCastCastingDevice;
|
||||||
|
|
||||||
@ -1095,7 +1088,7 @@ class StateCasting {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Logger.i(TAG, "added new castDash handlers (dashPath: $dashPath, videoPath: $videoPath, audioPath: $audioPath).");
|
Logger.i(TAG, "added new castDash handlers (dashPath: $dashPath, videoPath: $videoPath, audioPath: $audioPath).");
|
||||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", "application/dash+xml", dashUrl, resumePosition, video.duration.toDouble(), null);
|
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", "application/dash+xml", dashUrl, resumePosition, video.duration.toDouble(), speed);
|
||||||
|
|
||||||
return listOf(dashUrl, videoUrl ?: "", audioUrl ?: "", subtitlesUrl ?: "", videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "", subtitlesUri.toString());
|
return listOf(dashUrl, videoUrl ?: "", audioUrl ?: "", subtitlesUrl ?: "", videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "", subtitlesUri.toString());
|
||||||
}
|
}
|
||||||
|
@ -1468,7 +1468,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_player.seekTo(resumePositionMs);
|
_player.seekTo(resumePositionMs);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
loadCurrentVideoCast(video, videoSource, audioSource, subtitleSource, resumePositionMs);
|
loadCurrentVideoCast(video, videoSource, audioSource, subtitleSource, resumePositionMs, Settings.instance.playback.getDefaultPlaybackSpeed().toDouble());
|
||||||
|
|
||||||
_lastVideoSource = videoSource;
|
_lastVideoSource = videoSource;
|
||||||
_lastAudioSource = audioSource;
|
_lastAudioSource = audioSource;
|
||||||
@ -1482,14 +1482,13 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
UIDialogs.showGeneralErrorDialog(context, context.getString(R.string.failed_to_load_media), ex);
|
UIDialogs.showGeneralErrorDialog(context, context.getString(R.string.failed_to_load_media), ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private fun loadCurrentVideoCast(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: ISubtitleSource?, resumePositionMs: Long) {
|
private fun loadCurrentVideoCast(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: ISubtitleSource?, resumePositionMs: Long, speed: Double?) {
|
||||||
Logger.i(TAG, "loadCurrentVideoCast(video=$video, videoSource=$videoSource, audioSource=$audioSource, resumePositionMs=$resumePositionMs)")
|
Logger.i(TAG, "loadCurrentVideoCast(video=$video, videoSource=$videoSource, audioSource=$audioSource, resumePositionMs=$resumePositionMs)")
|
||||||
|
|
||||||
if(StateCasting.instance.castIfAvailable(context.contentResolver, video, videoSource, audioSource, subtitleSource, resumePositionMs)) {
|
if(StateCasting.instance.castIfAvailable(context.contentResolver, video, videoSource, audioSource, subtitleSource, resumePositionMs, speed)) {
|
||||||
_cast.setVideoDetails(video, resumePositionMs / 1000);
|
_cast.setVideoDetails(video, resumePositionMs / 1000);
|
||||||
setCastEnabled(true);
|
setCastEnabled(true);
|
||||||
}
|
} else throw IllegalStateException("Disconnected cast during loading");
|
||||||
else throw IllegalStateException("Disconnected cast during loading");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//Events
|
//Events
|
||||||
@ -1576,6 +1575,11 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_overlay_quality_selector?.selectOption("video", _lastVideoSource);
|
_overlay_quality_selector?.selectOption("video", _lastVideoSource);
|
||||||
_overlay_quality_selector?.selectOption("audio", _lastAudioSource);
|
_overlay_quality_selector?.selectOption("audio", _lastAudioSource);
|
||||||
_overlay_quality_selector?.selectOption("subtitles", _lastSubtitleSource);
|
_overlay_quality_selector?.selectOption("subtitles", _lastSubtitleSource);
|
||||||
|
val currentPlaybackRate = (if (_isCasting) StateCasting.instance.activeDevice?.speed else _player.getPlaybackRate()) ?: 1.0
|
||||||
|
_overlay_quality_selector?.groupItems?.firstOrNull { it is SlideUpMenuButtonList && it.id == "playback_rate" }?.let {
|
||||||
|
(it as SlideUpMenuButtonList).setSelected(currentPlaybackRate.toString())
|
||||||
|
};
|
||||||
|
|
||||||
_overlay_quality_selector?.show();
|
_overlay_quality_selector?.show();
|
||||||
_slideUpOverlay = _overlay_quality_selector;
|
_slideUpOverlay = _overlay_quality_selector;
|
||||||
}
|
}
|
||||||
@ -1656,18 +1660,26 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
?.filter { it.container == bestAudioContainer }
|
?.filter { it.container == bestAudioContainer }
|
||||||
?.toList() ?: listOf();
|
?.toList() ?: listOf();
|
||||||
|
|
||||||
|
val canSetSpeed = !_isCasting || StateCasting.instance.activeDevice?.canSetSpeed == true
|
||||||
|
val currentPlaybackRate = if (_isCasting) StateCasting.instance.activeDevice?.speed else _player.getPlaybackRate()
|
||||||
_overlay_quality_selector = SlideUpMenuOverlay(this.context, _overlay_quality_container, context.getString(
|
_overlay_quality_selector = SlideUpMenuOverlay(this.context, _overlay_quality_container, context.getString(
|
||||||
R.string.quality), null, true,
|
R.string.quality), null, true,
|
||||||
if (!_isCasting) SlideUpMenuTitle(this.context).apply { setTitle(context.getString(R.string.playback_rate)) } else null,
|
if (canSetSpeed) SlideUpMenuTitle(this.context).apply { setTitle(context.getString(R.string.playback_rate)) } else null,
|
||||||
if (!_isCasting) SlideUpMenuButtonList(this.context).apply {
|
if (canSetSpeed) SlideUpMenuButtonList(this.context, null, "playback_rate").apply {
|
||||||
setButtons(listOf("0.25", "0.5", "0.75", "1.0", "1.25", "1.5", "1.75", "2.0", "2.25"), _player.getPlaybackRate().toString());
|
setButtons(listOf("0.25", "0.5", "0.75", "1.0", "1.25", "1.5", "1.75", "2.0", "2.25"), currentPlaybackRate!!.toString());
|
||||||
onClick.subscribe { v ->
|
onClick.subscribe { v ->
|
||||||
if (_isCasting) {
|
if (_isCasting) {
|
||||||
return@subscribe;
|
val ad = StateCasting.instance.activeDevice ?: return@subscribe
|
||||||
}
|
if (!ad.canSetSpeed) {
|
||||||
|
return@subscribe
|
||||||
|
}
|
||||||
|
|
||||||
_player.setPlaybackRate(v.toFloat());
|
ad.changeSpeed(v.toDouble())
|
||||||
setSelected(v);
|
setSelected(v);
|
||||||
|
} else {
|
||||||
|
_player.setPlaybackRate(v.toFloat());
|
||||||
|
setSelected(v);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
} else null,
|
} else null,
|
||||||
|
|
||||||
@ -1829,7 +1841,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
|
|
||||||
val d = StateCasting.instance.activeDevice;
|
val d = StateCasting.instance.activeDevice;
|
||||||
if (d != null && d.connectionState == CastConnectionState.CONNECTED)
|
if (d != null && d.connectionState == CastConnectionState.CONNECTED)
|
||||||
StateCasting.instance.castIfAvailable(context.contentResolver, video, videoSource, _lastAudioSource, _lastSubtitleSource, (d.expectedCurrentTime * 1000.0).toLong());
|
StateCasting.instance.castIfAvailable(context.contentResolver, video, videoSource, _lastAudioSource, _lastSubtitleSource, (d.expectedCurrentTime * 1000.0).toLong(), d.speed);
|
||||||
else if(!_player.swapSources(videoSource, _lastAudioSource, true, true, true))
|
else if(!_player.swapSources(videoSource, _lastAudioSource, true, true, true))
|
||||||
_player.hideControls(false); //TODO: Disable player?
|
_player.hideControls(false); //TODO: Disable player?
|
||||||
|
|
||||||
@ -1844,7 +1856,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
|
|
||||||
val d = StateCasting.instance.activeDevice;
|
val d = StateCasting.instance.activeDevice;
|
||||||
if (d != null && d.connectionState == CastConnectionState.CONNECTED)
|
if (d != null && d.connectionState == CastConnectionState.CONNECTED)
|
||||||
StateCasting.instance.castIfAvailable(context.contentResolver, video, _lastVideoSource, audioSource, _lastSubtitleSource, (d.expectedCurrentTime * 1000.0).toLong());
|
StateCasting.instance.castIfAvailable(context.contentResolver, video, _lastVideoSource, audioSource, _lastSubtitleSource, (d.expectedCurrentTime * 1000.0).toLong(), d.speed);
|
||||||
else(!_player.swapSources(_lastVideoSource, audioSource, true, true, true))
|
else(!_player.swapSources(_lastVideoSource, audioSource, true, true, true))
|
||||||
_player.hideControls(false); //TODO: Disable player?
|
_player.hideControls(false); //TODO: Disable player?
|
||||||
|
|
||||||
@ -1860,7 +1872,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
|
|
||||||
val d = StateCasting.instance.activeDevice;
|
val d = StateCasting.instance.activeDevice;
|
||||||
if (d != null && d.connectionState == CastConnectionState.CONNECTED)
|
if (d != null && d.connectionState == CastConnectionState.CONNECTED)
|
||||||
StateCasting.instance.castIfAvailable(context.contentResolver, video, _lastVideoSource, _lastAudioSource, toSet, (d.expectedCurrentTime * 1000.0).toLong());
|
StateCasting.instance.castIfAvailable(context.contentResolver, video, _lastVideoSource, _lastAudioSource, toSet, (d.expectedCurrentTime * 1000.0).toLong(), d.speed);
|
||||||
else
|
else
|
||||||
_player.swapSubtitles(fragment.lifecycleScope, toSet);
|
_player.swapSubtitles(fragment.lifecycleScope, toSet);
|
||||||
|
|
||||||
|
@ -18,8 +18,11 @@ class SlideUpMenuButtonList : LinearLayout {
|
|||||||
val onClick = Event1<String>();
|
val onClick = Event1<String>();
|
||||||
val buttons: HashMap<String, LinearLayout> = hashMapOf();
|
val buttons: HashMap<String, LinearLayout> = hashMapOf();
|
||||||
var _activeText: String? = null;
|
var _activeText: String? = null;
|
||||||
|
val id: String?
|
||||||
|
|
||||||
|
constructor(context: Context, attrs: AttributeSet? = null, id: String? = null): super(context, attrs) {
|
||||||
|
this.id = id
|
||||||
|
|
||||||
constructor(context: Context, attrs: AttributeSet? = null): super(context, attrs) {
|
|
||||||
LayoutInflater.from(context).inflate(R.layout.overlay_slide_up_menu_button_list, this, true);
|
LayoutInflater.from(context).inflate(R.layout.overlay_slide_up_menu_button_list, this, true);
|
||||||
|
|
||||||
_root = findViewById(R.id.root);
|
_root = findViewById(R.id.root);
|
||||||
|
@ -26,7 +26,7 @@ class SlideUpMenuOverlay : RelativeLayout {
|
|||||||
private lateinit var _viewContainer: LinearLayout;
|
private lateinit var _viewContainer: LinearLayout;
|
||||||
private var _animated: Boolean = true;
|
private var _animated: Boolean = true;
|
||||||
|
|
||||||
private var _groupItems: List<View>;
|
var groupItems: List<View>;
|
||||||
|
|
||||||
var isVisible = false
|
var isVisible = false
|
||||||
private set;
|
private set;
|
||||||
@ -36,7 +36,7 @@ class SlideUpMenuOverlay : RelativeLayout {
|
|||||||
|
|
||||||
constructor(context: Context, attrs: AttributeSet? = null): super(context, attrs) {
|
constructor(context: Context, attrs: AttributeSet? = null): super(context, attrs) {
|
||||||
init(false, null);
|
init(false, null);
|
||||||
_groupItems = listOf();
|
groupItems = listOf();
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(context: Context, parent: ViewGroup, titleText: String, okText: String?, animated: Boolean, items: List<View>, hideButtons: Boolean = false): super(context){
|
constructor(context: Context, parent: ViewGroup, titleText: String, okText: String?, animated: Boolean, items: List<View>, hideButtons: Boolean = false): super(context){
|
||||||
@ -47,7 +47,7 @@ class SlideUpMenuOverlay : RelativeLayout {
|
|||||||
_container!!.addView(this);
|
_container!!.addView(this);
|
||||||
}
|
}
|
||||||
_textTitle.text = titleText;
|
_textTitle.text = titleText;
|
||||||
_groupItems = items;
|
groupItems = items;
|
||||||
|
|
||||||
if(hideButtons) {
|
if(hideButtons) {
|
||||||
_textCancel.visibility = GONE;
|
_textCancel.visibility = GONE;
|
||||||
@ -74,7 +74,7 @@ class SlideUpMenuOverlay : RelativeLayout {
|
|||||||
item.setParentClickListener { hide() };
|
item.setParentClickListener { hide() };
|
||||||
}
|
}
|
||||||
|
|
||||||
_groupItems = items;
|
groupItems = items;
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun init(animated: Boolean, okText: String?){
|
private fun init(animated: Boolean, okText: String?){
|
||||||
@ -116,12 +116,12 @@ class SlideUpMenuOverlay : RelativeLayout {
|
|||||||
|
|
||||||
fun selectOption(groupTag: Any?, itemTag: Any?, multiSelect: Boolean = false, toggle: Boolean = false): Boolean {
|
fun selectOption(groupTag: Any?, itemTag: Any?, multiSelect: Boolean = false, toggle: Boolean = false): Boolean {
|
||||||
var didSelect = false;
|
var didSelect = false;
|
||||||
for(view in _groupItems) {
|
for(view in groupItems) {
|
||||||
if(view is SlideUpMenuGroup && view.groupTag == groupTag)
|
if(view is SlideUpMenuGroup && view.groupTag == groupTag)
|
||||||
didSelect = didSelect || view.selectItem(itemTag);
|
didSelect = didSelect || view.selectItem(itemTag);
|
||||||
}
|
}
|
||||||
if(groupTag == null)
|
if(groupTag == null)
|
||||||
for(item in _groupItems)
|
for(item in groupItems)
|
||||||
if(item is SlideUpMenuItem) {
|
if(item is SlideUpMenuItem) {
|
||||||
if(multiSelect) {
|
if(multiSelect) {
|
||||||
if(item.itemTag == itemTag)
|
if(item.itemTag == itemTag)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user