diff --git a/app/src/main/java/com/futo/platformplayer/casting/AirPlayCastingDevice.kt b/app/src/main/java/com/futo/platformplayer/casting/AirPlayCastingDevice.kt index 93de4b05..f390012e 100644 --- a/app/src/main/java/com/futo/platformplayer/casting/AirPlayCastingDevice.kt +++ b/app/src/main/java/com/futo/platformplayer/casting/AirPlayCastingDevice.kt @@ -1,12 +1,17 @@ package com.futo.platformplayer.casting import android.os.Looper -import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.getConnectedSocket +import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.models.CastingDeviceInfo 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.util.UUID @@ -18,7 +23,7 @@ class AirPlayCastingDevice : CastingDevice { override var usedRemoteAddress: InetAddress? = null; override var localAddress: InetAddress? = null; 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? = null; var port: Int = 0; @@ -59,6 +64,10 @@ class AirPlayCastingDevice : CastingDevice { } else { 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?) { @@ -186,6 +195,11 @@ class AirPlayCastingDevice : CastingDevice { _scopeIO = null; } + override fun changeSpeed(speed: Double) { + this.speed = speed + post("rate?value=$speed") + } + override fun getDeviceInfo(): CastingDeviceInfo { return CastingDeviceInfo(name!!, CastProtocolType.AIRPLAY, addresses!!.filter { a -> a.hostAddress != null }.map { a -> a.hostAddress!! }.toTypedArray(), port); } diff --git a/app/src/main/java/com/futo/platformplayer/casting/CastingDevice.kt b/app/src/main/java/com/futo/platformplayer/casting/CastingDevice.kt index f7e0438f..beeeecc5 100644 --- a/app/src/main/java/com/futo/platformplayer/casting/CastingDevice.kt +++ b/app/src/main/java/com/futo/platformplayer/casting/CastingDevice.kt @@ -81,7 +81,7 @@ abstract class CastingDevice { var speed: Double = 1.0 set(value) { val changed = value != field; - speed = value; + field = value; if (changed) { onSpeedChanged.emit(value); } diff --git a/app/src/main/java/com/futo/platformplayer/casting/FCastCastingDevice.kt b/app/src/main/java/com/futo/platformplayer/casting/FCastCastingDevice.kt index 2a6d51c3..e276458e 100644 --- a/app/src/main/java/com/futo/platformplayer/casting/FCastCastingDevice.kt +++ b/app/src/main/java/com/futo/platformplayer/casting/FCastCastingDevice.kt @@ -3,14 +3,24 @@ package com.futo.platformplayer.casting import android.os.Looper import android.util.Log import com.futo.platformplayer.UIDialogs -import com.futo.platformplayer.casting.models.* -import com.futo.platformplayer.logging.Logger +import com.futo.platformplayer.casting.models.FCastPlayMessage +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.logging.Logger import com.futo.platformplayer.models.CastingDeviceInfo import com.futo.platformplayer.toHexString import com.futo.platformplayer.toInetAddress -import kotlinx.coroutines.* -import kotlinx.serialization.decodeFromString +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import java.io.DataInputStream @@ -89,6 +99,8 @@ class FCastCastingDevice : CastingDevice { time = resumePosition, speed = speed )); + + this.speed = speed ?: 1.0 } override fun loadContent(contentType: String, content: String, resumePosition: Double, duration: Double, speed: Double?) { @@ -110,6 +122,8 @@ class FCastCastingDevice : CastingDevice { time = resumePosition, speed = speed )); + + this.speed = speed ?: 1.0 } override fun changeVolume(volume: Double) { @@ -122,12 +136,12 @@ class FCastCastingDevice : CastingDevice { } override fun changeSpeed(speed: Double) { - if (invokeInIOScopeIfRequired({ changeSpeed(volume) })) { + if (invokeInIOScopeIfRequired({ changeSpeed(speed) })) { return; } this.speed = speed - sendMessage(Opcode.SET_SPEED, FCastSetSpeedMessage(volume)) + sendMessage(Opcode.SET_SPEED, FCastSetSpeedMessage(speed)) } override fun seekVideo(timeSeconds: Double) { diff --git a/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt b/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt index c400b5a2..685ccdd0 100644 --- a/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt +++ b/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt @@ -262,6 +262,7 @@ class StateCasting { try { jmDNS.removeServiceListener("_googlecast._tcp.local.", _chromecastServiceListener); jmDNS.removeServiceListener("_airplay._tcp", _airPlayServiceListener); + jmDNS.removeServiceListener("_fastcast._tcp.local.", _fastCastServiceListener); if (BuildConfig.DEBUG) { jmDNS.removeServiceTypeListener(_serviceTypeListener); @@ -382,7 +383,7 @@ class StateCasting { 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; if (ad.connectionState != CastConnectionState.CONNECTED) { return false; @@ -403,23 +404,23 @@ class StateCasting { if (videoSource is LocalVideoSource || audioSource is LocalAudioSource || subtitleSource is LocalSubtitleSource) { if (ad is AirPlayCastingDevice) { 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 { 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 { StateApp.instance.scope.launch(Dispatchers.IO) { try { if (ad is FCastCastingDevice) { 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) { 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 { 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) { Logger.e(TAG, "Failed to start casting DASH videoSource=${videoSource} audioSource=${audioSource}.", e); @@ -429,32 +430,32 @@ class StateCasting { } else { if (videoSource is IVideoUrlSource) { 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) { 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) { if (ad is ChromecastCastingDevice) { Logger.i(TAG, "Casting as proxied HLS"); - castProxiedHls(video, videoSource.url, videoSource.codec, resumePosition); + castProxiedHls(video, videoSource.url, videoSource.codec, resumePosition, speed); } else { 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) { if (ad is ChromecastCastingDevice) { Logger.i(TAG, "Casting as proxied audio HLS"); - castProxiedHls(video, audioSource.url, audioSource.codec, resumePosition); + castProxiedHls(video, audioSource.url, audioSource.codec, resumePosition, speed); } else { 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) { Logger.i(TAG, "Casting as local video"); - castLocalVideo(video, videoSource, resumePosition); + castLocalVideo(video, videoSource, resumePosition, speed); } else if (audioSource is LocalAudioSource) { Logger.i(TAG, "Casting as local audio"); - castLocalAudio(video, audioSource, resumePosition); + castLocalAudio(video, audioSource, resumePosition, speed); } else { var str = listOf( if(videoSource != null) "Video: ${videoSource::class.java.simpleName}" else null, @@ -492,15 +493,7 @@ class StateCasting { return true; } - private fun castVideoIndirect() { - - } - - private fun castAudioIndirect() { - - } - - private fun castLocalVideo(video: IPlatformVideoDetails, videoSource: LocalVideoSource, resumePosition: Double) : List { + private fun castLocalVideo(video: IPlatformVideoDetails, videoSource: LocalVideoSource, resumePosition: Double, speed: Double?) : List { val ad = activeDevice ?: return listOf(); val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}"; @@ -514,12 +507,12 @@ class StateCasting { ).withTag("cast"); 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); } - private fun castLocalAudio(video: IPlatformVideoDetails, audioSource: LocalAudioSource, resumePosition: Double) : List { + private fun castLocalAudio(video: IPlatformVideoDetails, audioSource: LocalAudioSource, resumePosition: Double, speed: Double?) : List { val ad = activeDevice ?: return listOf(); val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}"; @@ -533,12 +526,12 @@ class StateCasting { ).withTag("cast"); 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); } - private fun castLocalHls(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double): List { + private fun castLocalHls(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double, speed: Double?): List { val ad = activeDevice ?: return listOf() val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}" @@ -629,12 +622,12 @@ class StateCasting { ).withTag("castLocalHls") 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) } - private fun castLocalDash(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double) : List { + private fun castLocalDash(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double, speed: Double?) : List { val ad = activeDevice ?: return listOf(); 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)."); - 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); } - private suspend fun castDashDirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double) : List { + private suspend fun castDashDirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List { val ad = activeDevice ?: return listOf(); 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); 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() ?: ""); } - private fun castProxiedHls(video: IPlatformVideoDetails, sourceUrl: String, codec: String?, resumePosition: Double): List { + private fun castProxiedHls(video: IPlatformVideoDetails, sourceUrl: String, codec: String?, resumePosition: Double, speed: Double?): List { _castServer.removeAllHandlers("castProxiedHlsMaster") val ad = activeDevice ?: return listOf(); @@ -846,7 +839,7 @@ class StateCasting { //ChromeCast is sometimes funky with resume position 0 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); } @@ -897,7 +890,7 @@ class StateCasting { } } - private suspend fun castHlsIndirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double) : List { + private suspend fun castHlsIndirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List { val ad = activeDevice ?: return listOf(); val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}"; val id = UUID.randomUUID(); @@ -1020,12 +1013,12 @@ class StateCasting { ).withTag("castHlsIndirectMaster") 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()); } - private suspend fun castDashIndirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double) : List { + private suspend fun castDashIndirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List { val ad = activeDevice ?: return listOf(); val proxyStreams = ad !is FCastCastingDevice; @@ -1095,7 +1088,7 @@ class StateCasting { } 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()); } diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt index 66f01215..da1066ff 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt @@ -1468,7 +1468,7 @@ class VideoDetailView : ConstraintLayout { _player.seekTo(resumePositionMs); } else - loadCurrentVideoCast(video, videoSource, audioSource, subtitleSource, resumePositionMs); + loadCurrentVideoCast(video, videoSource, audioSource, subtitleSource, resumePositionMs, Settings.instance.playback.getDefaultPlaybackSpeed().toDouble()); _lastVideoSource = videoSource; _lastAudioSource = audioSource; @@ -1482,14 +1482,13 @@ class VideoDetailView : ConstraintLayout { 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)") - 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); setCastEnabled(true); - } - else throw IllegalStateException("Disconnected cast during loading"); + } else throw IllegalStateException("Disconnected cast during loading"); } //Events @@ -1576,6 +1575,11 @@ class VideoDetailView : ConstraintLayout { _overlay_quality_selector?.selectOption("video", _lastVideoSource); _overlay_quality_selector?.selectOption("audio", _lastAudioSource); _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(); _slideUpOverlay = _overlay_quality_selector; } @@ -1656,18 +1660,26 @@ class VideoDetailView : ConstraintLayout { ?.filter { it.container == bestAudioContainer } ?.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( R.string.quality), null, true, - if (!_isCasting) SlideUpMenuTitle(this.context).apply { setTitle(context.getString(R.string.playback_rate)) } else null, - if (!_isCasting) SlideUpMenuButtonList(this.context).apply { - setButtons(listOf("0.25", "0.5", "0.75", "1.0", "1.25", "1.5", "1.75", "2.0", "2.25"), _player.getPlaybackRate().toString()); + if (canSetSpeed) SlideUpMenuTitle(this.context).apply { setTitle(context.getString(R.string.playback_rate)) } else null, + 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"), currentPlaybackRate!!.toString()); onClick.subscribe { v -> if (_isCasting) { - return@subscribe; - } + val ad = StateCasting.instance.activeDevice ?: return@subscribe + if (!ad.canSetSpeed) { + return@subscribe + } - _player.setPlaybackRate(v.toFloat()); - setSelected(v); + ad.changeSpeed(v.toDouble()) + setSelected(v); + } else { + _player.setPlaybackRate(v.toFloat()); + setSelected(v); + } }; } else null, @@ -1829,7 +1841,7 @@ class VideoDetailView : ConstraintLayout { val d = StateCasting.instance.activeDevice; 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)) _player.hideControls(false); //TODO: Disable player? @@ -1844,7 +1856,7 @@ class VideoDetailView : ConstraintLayout { val d = StateCasting.instance.activeDevice; 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)) _player.hideControls(false); //TODO: Disable player? @@ -1860,7 +1872,7 @@ class VideoDetailView : ConstraintLayout { val d = StateCasting.instance.activeDevice; 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 _player.swapSubtitles(fragment.lifecycleScope, toSet); diff --git a/app/src/main/java/com/futo/platformplayer/views/overlays/slideup/SlideUpMenuButtonList.kt b/app/src/main/java/com/futo/platformplayer/views/overlays/slideup/SlideUpMenuButtonList.kt index f5a404fc..78031ec0 100644 --- a/app/src/main/java/com/futo/platformplayer/views/overlays/slideup/SlideUpMenuButtonList.kt +++ b/app/src/main/java/com/futo/platformplayer/views/overlays/slideup/SlideUpMenuButtonList.kt @@ -18,8 +18,11 @@ class SlideUpMenuButtonList : LinearLayout { val onClick = Event1(); val buttons: HashMap = hashMapOf(); 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); _root = findViewById(R.id.root); diff --git a/app/src/main/java/com/futo/platformplayer/views/overlays/slideup/SlideUpMenuOverlay.kt b/app/src/main/java/com/futo/platformplayer/views/overlays/slideup/SlideUpMenuOverlay.kt index 157c13a6..89cf1359 100644 --- a/app/src/main/java/com/futo/platformplayer/views/overlays/slideup/SlideUpMenuOverlay.kt +++ b/app/src/main/java/com/futo/platformplayer/views/overlays/slideup/SlideUpMenuOverlay.kt @@ -26,7 +26,7 @@ class SlideUpMenuOverlay : RelativeLayout { private lateinit var _viewContainer: LinearLayout; private var _animated: Boolean = true; - private var _groupItems: List; + var groupItems: List; var isVisible = false private set; @@ -36,7 +36,7 @@ class SlideUpMenuOverlay : RelativeLayout { constructor(context: Context, attrs: AttributeSet? = null): super(context, attrs) { init(false, null); - _groupItems = listOf(); + groupItems = listOf(); } constructor(context: Context, parent: ViewGroup, titleText: String, okText: String?, animated: Boolean, items: List, hideButtons: Boolean = false): super(context){ @@ -47,7 +47,7 @@ class SlideUpMenuOverlay : RelativeLayout { _container!!.addView(this); } _textTitle.text = titleText; - _groupItems = items; + groupItems = items; if(hideButtons) { _textCancel.visibility = GONE; @@ -74,7 +74,7 @@ class SlideUpMenuOverlay : RelativeLayout { item.setParentClickListener { hide() }; } - _groupItems = items; + groupItems = items; } 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 { var didSelect = false; - for(view in _groupItems) { + for(view in groupItems) { if(view is SlideUpMenuGroup && view.groupTag == groupTag) didSelect = didSelect || view.selectItem(itemTag); } if(groupTag == null) - for(item in _groupItems) + for(item in groupItems) if(item is SlideUpMenuItem) { if(multiSelect) { if(item.itemTag == itemTag)