Merge branch 'media3-migration' into 'master'

Media3 migration.

See merge request videostreaming/grayjay!10
This commit is contained in:
Koen 2023-12-13 12:57:43 +00:00
commit 0432f06eb3
34 changed files with 467 additions and 272 deletions

View file

@ -172,15 +172,16 @@ dependencies {
implementation("com.caoccao.javet:javet-android:2.2.1")
//Exoplayer
implementation 'com.google.android.exoplayer:exoplayer-core:2.19.1'
implementation 'com.google.android.exoplayer:exoplayer-dash:2.19.1'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.19.1'
implementation 'com.google.android.exoplayer:exoplayer-hls:2.19.1'
implementation 'com.google.android.exoplayer:exoplayer-rtsp:2.19.1'
implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.19.1'
implementation 'com.google.android.exoplayer:exoplayer-transformer:2.19.1'
implementation 'androidx.media3:media3-exoplayer:1.2.0'
implementation 'androidx.media3:media3-exoplayer-dash:1.2.0'
implementation 'androidx.media3:media3-ui:1.2.0'
implementation 'androidx.media3:media3-exoplayer-hls:1.2.0'
implementation 'androidx.media3:media3-exoplayer-rtsp:1.2.0'
implementation 'androidx.media3:media3-exoplayer-smoothstreaming:1.2.0'
implementation 'androidx.media3:media3-transformer:1.2.0'
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.5'
implementation 'androidx.navigation:navigation-ui-ktx:2.7.5'
implementation 'androidx.media:media:1.7.0'
//Other
implementation 'org.jmdns:jmdns:3.5.1'

View file

@ -6,28 +6,41 @@ import android.content.Intent
import android.net.Uri
import android.webkit.CookieManager
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.activities.*
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.activities.ManageTabsActivity
import com.futo.platformplayer.activities.PolycentricHomeActivity
import com.futo.platformplayer.activities.PolycentricProfileActivity
import com.futo.platformplayer.activities.SettingsActivity
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.serializers.FlexibleBooleanSerializer
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
import com.futo.platformplayer.states.*
import com.futo.platformplayer.states.StateAnnouncement
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateBackup
import com.futo.platformplayer.states.StateCache
import com.futo.platformplayer.states.StateMeta
import com.futo.platformplayer.states.StatePayment
import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.states.StateUpdate
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.FragmentedStorageFileJson
import com.futo.platformplayer.views.FeedStyle
import com.futo.platformplayer.views.fields.DropdownFieldOptionsId
import com.futo.platformplayer.views.fields.FormField
import com.futo.platformplayer.views.fields.FieldForm
import com.futo.platformplayer.views.fields.FormField
import com.futo.platformplayer.views.fields.FormFieldButton
import com.futo.platformplayer.views.fields.FormFieldWarning
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.*
import kotlinx.serialization.json.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.io.File
import java.time.OffsetDateTime
@ -428,6 +441,9 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.restart_after_connectivity_loss, FieldForm.DROPDOWN, R.string.restart_playback_when_gaining_connectivity_after_a_loss, 12)
@DropdownFieldOptionsId(R.array.restart_playback_after_loss)
var restartPlaybackAfterConnectivityLoss: Int = 1;
@FormField(R.string.full_screen_portrait, FieldForm.TOGGLE, R.string.allow_full_screen_portrait, 13)
var fullscreenPortrait: Boolean = false;
}
@FormField(R.string.comments, "group", R.string.comments_description, 6)

View file

@ -1,7 +1,7 @@
@file:Suppress("DEPRECATION")
package com.futo.platformplayer.api.media.platforms.js.models.sources
import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.datasource.HttpDataSource
import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
@ -11,8 +11,6 @@ import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.orNull
import com.futo.platformplayer.views.video.datasources.JSHttpDataSource
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource
import com.google.android.exoplayer2.upstream.HttpDataSource
abstract class JSSource {
protected val _config: IV8PluginConfig;

View file

@ -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<InetAddress>? = 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);
}

View file

@ -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);
}

View file

@ -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) {
@ -247,7 +261,8 @@ class FCastCastingDevice : CastingDevice {
val buffer = ByteArray(4096);
Logger.i(TAG, "Started receiving.");
while (_scopeIO?.isActive == true) {
var exceptionOccurred = false;
while (_scopeIO?.isActive == true && !exceptionOccurred) {
try {
val inputStream = _inputStream ?: break;
Log.d(TAG, "Receiving next packet...");
@ -275,20 +290,25 @@ class FCastCastingDevice : CastingDevice {
}
try {
handleMessage(Opcode.values().first { it.value == opcode }, json);
handleMessage(Opcode.entries.first { it.value == opcode }, json);
} catch (e:Throwable) {
Logger.w(TAG, "Failed to handle message.", e);
}
} catch (e: java.net.SocketException) {
Logger.e(TAG, "Socket exception while receiving.", e);
break;
exceptionOccurred = true;
} catch (e: Throwable) {
Logger.e(TAG, "Exception while receiving.", e);
break;
exceptionOccurred = true;
}
}
_socket?.close();
Logger.i(TAG, "Socket disconnected.");
try {
_socket?.close();
Logger.i(TAG, "Socket disconnected.");
} catch (e: Throwable) {
Logger.e(TAG, "Failed to close socket.", e)
}
connectionState = CastConnectionState.CONNECTING;
Thread.sleep(3000);

View file

@ -5,6 +5,7 @@ import android.content.Context
import android.net.Uri
import android.os.Looper
import android.util.Base64
import android.util.Log
import com.futo.platformplayer.BuildConfig
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.http.ManagedHttpClient
@ -67,6 +68,7 @@ class StateCasting {
val onActiveDeviceTimeChanged = Event1<Double>();
var activeDevice: CastingDevice? = null;
private val _client = ManagedHttpClient();
var _resumeCastingDevice: CastingDeviceInfo? = null;
val isCasting: Boolean get() = activeDevice != null;
@ -182,28 +184,42 @@ class StateCasting {
val networkConfig = Json.decodeFromString<FCastNetworkConfig>(json)
val tcpService = networkConfig.services.first { v -> v.type == 0 }
addRememberedDevice(CastingDeviceInfo(
val foundInfo = addRememberedDevice(CastingDeviceInfo(
name = networkConfig.name,
type = CastProtocolType.FCAST,
addresses = networkConfig.addresses.toTypedArray(),
port = tcpService.port
))
UIDialogs.toast(context,"FCast device '${networkConfig.name}' added")
connectDevice(deviceFromCastingDeviceInfo(foundInfo))
}
fun onStop() {
val ad = activeDevice ?: return;
_resumeCastingDevice = ad.getDeviceInfo()
Log.i(TAG, "_resumeCastingDevice set to '${ad.name}'")
Logger.i(TAG, "Stopping active device because of onStop.");
ad.stop();
}
fun onResume() {
val resumeCastingDevice = _resumeCastingDevice
if (resumeCastingDevice != null) {
connectDevice(deviceFromCastingDeviceInfo(resumeCastingDevice))
_resumeCastingDevice = null
Log.i(TAG, "_resumeCastingDevice set to null onResume")
}
}
@Synchronized
fun start(context: Context) {
if (_started)
return;
_started = true;
Log.i(TAG, "_resumeCastingDevice set null start")
_resumeCastingDevice = null;
Logger.i(TAG, "CastingService starting...");
rememberedDevices.clear();
@ -246,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);
@ -335,15 +352,20 @@ class StateCasting {
Logger.i(TAG, "Connect to device ${device.name}");
}
fun addRememberedDevice(deviceInfo: CastingDeviceInfo) {
fun addRememberedDevice(deviceInfo: CastingDeviceInfo): CastingDeviceInfo {
val device = deviceFromCastingDeviceInfo(deviceInfo);
addRememberedDevice(device);
return addRememberedDevice(device);
}
fun addRememberedDevice(device: CastingDevice) {
if (_storage.addDevice(device.getDeviceInfo())) {
fun addRememberedDevice(device: CastingDevice): CastingDeviceInfo {
val deviceInfo = device.getDeviceInfo()
val foundInfo = _storage.addDevice(deviceInfo)
if (foundInfo == deviceInfo) {
rememberedDevices.add(device);
return foundInfo;
}
return foundInfo;
}
fun removeRememberedDevice(device: CastingDevice) {
@ -361,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;
@ -382,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);
@ -408,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,
@ -471,15 +493,7 @@ class StateCasting {
return true;
}
private fun castVideoIndirect() {
}
private fun castAudioIndirect() {
}
private fun castLocalVideo(video: IPlatformVideoDetails, videoSource: LocalVideoSource, resumePosition: Double) : List<String> {
private fun castLocalVideo(video: IPlatformVideoDetails, videoSource: LocalVideoSource, resumePosition: Double, speed: Double?) : List<String> {
val ad = activeDevice ?: return listOf();
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
@ -493,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<String> {
private fun castLocalAudio(video: IPlatformVideoDetails, audioSource: LocalAudioSource, resumePosition: Double, speed: Double?) : List<String> {
val ad = activeDevice ?: return listOf();
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
@ -512,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<String> {
private fun castLocalHls(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double, speed: Double?): List<String> {
val ad = activeDevice ?: return listOf()
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}"
@ -608,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<String> {
private fun castLocalDash(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
val ad = activeDevice ?: return listOf();
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
@ -654,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<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 url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
@ -699,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<String> {
private fun castProxiedHls(video: IPlatformVideoDetails, sourceUrl: String, codec: String?, resumePosition: Double, speed: Double?): List<String> {
_castServer.removeAllHandlers("castProxiedHlsMaster")
val ad = activeDevice ?: return listOf();
@ -825,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);
}
@ -876,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 url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
val id = UUID.randomUUID();
@ -999,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<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 proxyStreams = ad !is FCastCastingDevice;
@ -1074,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());
}

View file

@ -67,7 +67,7 @@ class VideoDownload {
val videoEither: IPlatformVideo get() = videoDetails ?: video ?: throw IllegalStateException("Missing video?");
val id: PlatformID get() = videoEither.id
val name: String get() = videoEither.name;
val thumbnail: String? get() = videoDetails?.thumbnails?.getHQThumbnail() ?: video?.thumbnails?.getHQThumbnail();
val thumbnail: String? get() = videoDetails?.thumbnails?.getHQThumbnail();
var targetPixelCount: Long? = null;
var targetBitrate: Long? = null;

View file

@ -108,17 +108,22 @@ class VideoDetailFragment : MainFragment {
if(Settings.instance.other.bypassRotationPrevention && orientation == OrientationManager.Orientation.PORTRAIT)
changeOrientation(OrientationManager.Orientation.PORTRAIT);
if(lastOrientation == newOrientation)
return;
activity?.let {
if (isFullscreen) {
if(newOrientation == OrientationManager.Orientation.REVERSED_LANDSCAPE && it.requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)
changeOrientation(OrientationManager.Orientation.REVERSED_LANDSCAPE);
else if(newOrientation == OrientationManager.Orientation.LANDSCAPE && it.requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE)
changeOrientation(OrientationManager.Orientation.LANDSCAPE);
else if(Settings.instance.playback.isAutoRotate() && (newOrientation == OrientationManager.Orientation.PORTRAIT || newOrientation == OrientationManager.Orientation.REVERSED_PORTRAIT)) {
_viewDetail?.setFullscreen(false);
if (Settings.instance.playback.fullscreenPortrait) {
changeOrientation(newOrientation);
} else {
if(newOrientation == OrientationManager.Orientation.REVERSED_LANDSCAPE && it.requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)
changeOrientation(OrientationManager.Orientation.REVERSED_LANDSCAPE);
else if(newOrientation == OrientationManager.Orientation.LANDSCAPE && it.requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE)
changeOrientation(OrientationManager.Orientation.LANDSCAPE);
else if(Settings.instance.playback.isAutoRotate() && (newOrientation == OrientationManager.Orientation.PORTRAIT || newOrientation == OrientationManager.Orientation.REVERSED_PORTRAIT)) {
_viewDetail?.setFullscreen(false);
}
}
}
else {
@ -326,6 +331,8 @@ class VideoDetailFragment : MainFragment {
Logger.i(TAG, "Real orientation on boot ${realOrientation}, lastOrientation: ${lastOrientation}");
if(realOrientation != lastOrientation)
onOrientationChanged(realOrientation);
StateCasting.instance.onResume();
}
override fun onPause() {
super.onPause();
@ -403,10 +410,14 @@ class VideoDetailFragment : MainFragment {
private fun onFullscreenChanged(fullscreen : Boolean) {
activity?.let {
if (fullscreen) {
var orient = lastOrientation;
if(orient == OrientationManager.Orientation.PORTRAIT || orient == OrientationManager.Orientation.REVERSED_PORTRAIT)
orient = OrientationManager.Orientation.LANDSCAPE;
changeOrientation(orient);
if (Settings.instance.playback.fullscreenPortrait) {
changeOrientation(lastOrientation);
} else {
var orient = lastOrientation;
if(orient == OrientationManager.Orientation.PORTRAIT || orient == OrientationManager.Orientation.REVERSED_PORTRAIT)
orient = OrientationManager.Orientation.LANDSCAPE;
changeOrientation(orient);
}
}
else
changeOrientation(OrientationManager.Orientation.PORTRAIT);

View file

@ -1,5 +1,3 @@
@file:Suppress("DEPRECATION")
package com.futo.platformplayer.fragment.mainactivity.main
import android.app.PictureInPictureParams
@ -32,6 +30,12 @@ import android.widget.LinearLayout
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.C
import androidx.media3.common.Format
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.HttpDataSource
import androidx.media3.ui.PlayerControlView
import androidx.media3.ui.TimeBar
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
@ -138,11 +142,6 @@ import com.futo.polycentric.core.ContentType
import com.futo.polycentric.core.Models
import com.futo.polycentric.core.Opinion
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.Format
import com.google.android.exoplayer2.ui.PlayerControlView
import com.google.android.exoplayer2.ui.TimeBar
import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCodeException
import com.google.protobuf.ByteString
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -155,7 +154,6 @@ import java.time.OffsetDateTime
import kotlin.math.abs
import kotlin.math.roundToLong
class VideoDetailView : ConstraintLayout {
private val TAG = "VideoDetailView"
@ -303,7 +301,7 @@ class VideoDetailView : ConstraintLayout {
Pair(0, 10) //around live, try every 10 seconds
);
@androidx.annotation.OptIn(UnstableApi::class)
constructor(context: Context, attrs : AttributeSet? = null) : super(context, attrs) {
inflate(context, R.layout.fragview_video_detail, this);
@ -312,7 +310,7 @@ class VideoDetailView : ConstraintLayout {
_cast = findViewById(R.id.videodetail_cast);
_player = findViewById(R.id.videodetail_player);
_playerProgress = findViewById(R.id.videodetail_progress);
_timeBar = _playerProgress.findViewById(com.google.android.exoplayer2.ui.R.id.exo_progress);
_timeBar = _playerProgress.findViewById(androidx.media3.ui.R.id.exo_progress);
_title = findViewById(R.id.videodetail_title);
_subTitle = findViewById(R.id.videodetail_meta);
_platform = findViewById(R.id.videodetail_platform);
@ -514,6 +512,8 @@ class VideoDetailView : ConstraintLayout {
_player.onDatasourceError.subscribe(::onDataSourceError);
_player.onNext.subscribe { nextVideo(true, true, true) };
_player.onPrevious.subscribe { prevVideo(true) };
_cast.onPrevious.subscribe { prevVideo(true) };
_cast.onNext.subscribe { nextVideo(true, true, true) };
_minimize_controls_play.setOnClickListener { handlePlay(); };
_minimize_controls_pause.setOnClickListener { handlePause(); };
@ -576,7 +576,7 @@ class VideoDetailView : ConstraintLayout {
_playerProgress.player = _player.exoPlayer?.player;
_playerProgress.setProgressUpdateListener { position, _ ->
StatePlayer.instance.updateMediaSessionPlaybackState(_player.exoPlayer?.getPlaybackStateCompat() ?: PlaybackStateCompat.STATE_NONE, position);
}
};
StatePlayer.instance.onQueueChanged.subscribe(this) {
if(!_destroyed) {
@ -1361,11 +1361,9 @@ class VideoDetailView : ConstraintLayout {
}
}
StatePlayer.instance.startOrUpdateMediaSession(context, video);
StatePlayer.instance.setCurrentlyPlaying(video);
if(video.isLive && video.live != null) {
loadLiveChat(video);
}
@ -1472,7 +1470,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;
@ -1486,17 +1484,17 @@ 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
@androidx.annotation.OptIn(UnstableApi::class)
private fun onSourceChanged(videoSource: IVideoSource?, audioSource: IAudioSource?, resume: Boolean){
Logger.i(TAG, "onSourceChanged(videoSource=$videoSource, audioSource=$audioSource, resume=$resume)")
@ -1532,7 +1530,7 @@ class VideoDetailView : ConstraintLayout {
private var _didTriggerDatasourceError = false;
private fun onDataSourceError(exception: Throwable) {
Logger.e(TAG, "onDataSourceError", exception);
if(exception.cause != null && exception.cause is InvalidResponseCodeException && (exception.cause!! as InvalidResponseCodeException).responseCode == 403) {
if(exception.cause != null && exception.cause is HttpDataSource.InvalidResponseCodeException && (exception.cause!! as HttpDataSource.InvalidResponseCodeException).responseCode == 403) {
val currentVideo = video
if(currentVideo == null || currentVideo !is IPluginSourced)
return;
@ -1579,6 +1577,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;
}
@ -1611,6 +1614,7 @@ class VideoDetailView : ConstraintLayout {
val v = video ?: return;
updateQualitySourcesOverlay(v, videoLocal, liveStreamVideoFormats, liveStreamAudioFormats);
}
@androidx.annotation.OptIn(UnstableApi::class)
private fun updateQualitySourcesOverlay(videoDetails: IPlatformVideoDetails?, videoLocal: VideoLocal? = null, liveStreamVideoFormats: List<Format>? = null, liveStreamAudioFormats: List<Format>? = null) {
Logger.i(TAG, "updateQualitySourcesOverlay");
@ -1658,18 +1662,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,
@ -1730,7 +1742,7 @@ class VideoDetailView : ConstraintLayout {
{ handleSelectAudioTrack(it) });
}.toList().toTypedArray())
else null,
if(video?.subtitles?.isNotEmpty() ?: false && video != null)
if(video?.subtitles?.isNotEmpty() == true)
SlideUpMenuGroup(this.context, context.getString(R.string.subtitles), "subtitles",
*video.subtitles
.map {
@ -1831,7 +1843,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?
@ -1846,7 +1858,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?
@ -1862,7 +1874,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);

View file

@ -1,8 +1,14 @@
@file:Suppress("DEPRECATION")
package com.futo.platformplayer.helpers
import android.net.Uri
import androidx.annotation.OptIn
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.ResolvingDataSource
import androidx.media3.exoplayer.dash.DashMediaSource
import androidx.media3.exoplayer.dash.manifest.DashManifestParser
import androidx.media3.exoplayer.source.MediaSource
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
@ -16,11 +22,6 @@ import com.futo.platformplayer.api.media.platforms.js.models.sources.JSAudioUrlR
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSVideoUrlRangeSource
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.others.Language
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.source.MediaSource
import com.google.android.exoplayer2.source.dash.DashMediaSource
import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser
import com.google.android.exoplayer2.upstream.ResolvingDataSource
import kotlin.math.abs
class VideoHelper {
@ -123,7 +124,7 @@ class VideoHelper {
return bestSource;
}
@Suppress("DEPRECATION")
@OptIn(UnstableApi::class)
fun convertItagSourceToChunkedDashSource(videoSource: JSVideoUrlRangeSource) : MediaSource {
val urlToUse = videoSource.getVideoUrl();
val manifestConfig = ProgressiveDashManifestCreator.fromVideoProgressiveStreamingUrl(urlToUse,
@ -142,14 +143,25 @@ class VideoHelper {
);
val manifest = DashManifestParser().parse(Uri.parse(""), manifestConfig.byteInputStream());
return 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())
}
@Suppress("DEPRECATION")
fun getMediaMetadata(media: IPlatformVideoDetails): MediaMetadata {
val builder = MediaMetadata.Builder()
.setArtist(media.author.name)
.setTitle(media.name)
media.thumbnails.getHQThumbnail()?.let {
builder.setArtworkUri(Uri.parse(it))
}
return builder.build()
}
@OptIn(UnstableApi::class)
fun convertItagSourceToChunkedDashSource(audioSource: JSAudioUrlRangeSource) : MediaSource {
val manifestConfig = ProgressiveDashManifestCreator.fromAudioProgressiveStreamingUrl(audioSource.getAudioUrl(),
audioSource.duration?.times(1000) ?: 0,

View file

@ -17,7 +17,6 @@ class InstallReceiver : BroadcastReceiver() {
val onReceiveResult = Event1<String?>();
}
@Suppress("DEPRECATION")
override fun onReceive(context: Context, intent: Intent) {
val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1);
Logger.i(TAG, "Received status $status.");

View file

@ -55,6 +55,7 @@ class MediaPlaybackService : Service() {
private var _hasFocus: Boolean = false;
private var _focusRequest: AudioFocusRequest? = null;
private var _audioFocusLossTime_ms: Long? = null
private var _playbackState = PlaybackStateCompat.STATE_NONE;
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Logger.v(TAG, "onStartCommand");
@ -250,13 +251,13 @@ class MediaPlaybackService : Service() {
.setSilent(true)
.setContentIntent(PendingIntent.getActivity(this, 5, bringUpIntent, PendingIntent.FLAG_IMMUTABLE))
.setStyle(if(hasQueue)
androidx.media.app.NotificationCompat.MediaStyle()
.setMediaSession(session.sessionToken)
.setShowActionsInCompactView(0, 1, 2)
else
androidx.media.app.NotificationCompat.MediaStyle()
.setMediaSession(session.sessionToken)
.setShowActionsInCompactView(0))
androidx.media.app.NotificationCompat.MediaStyle()
.setMediaSession(session.sessionToken)
.setShowActionsInCompactView(0, 1, 2)
else
androidx.media.app.NotificationCompat.MediaStyle()
.setMediaSession(session.sessionToken)
.setShowActionsInCompactView(0))
.setDeleteIntent(deleteIntent)
.setChannelId(channel.id)
@ -306,10 +307,8 @@ class MediaPlaybackService : Service() {
Logger.i(TAG, "Updating notification bitmap=${if (bitmap != null) "yes" else "no."} channelId=${channel.id} icon=${icon} video=${video?.name ?: ""} playWhenReady=${playWhenReady} session.sessionToken=${session.sessionToken}");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// For API 29 and above
startForeground(MEDIA_NOTIF_ID, notif, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
} else {
// For API 28 and below
startForeground(MEDIA_NOTIF_ID, notif);
}
@ -319,19 +318,21 @@ class MediaPlaybackService : Service() {
fun updateMediaSessionPlaybackState(state: Int, pos: Long) {
_mediaSession?.setPlaybackState(
PlaybackStateCompat.Builder()
.setActions(
PlaybackStateCompat.ACTION_SEEK_TO or
PlaybackStateCompat.ACTION_PLAY or
PlaybackStateCompat.ACTION_PAUSE or
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or
PlaybackStateCompat.ACTION_SKIP_TO_NEXT or
PlaybackStateCompat.ACTION_PLAY_PAUSE
)
.setState(state, pos, 1f, SystemClock.elapsedRealtime())
.build());
.setActions(
PlaybackStateCompat.ACTION_SEEK_TO or
PlaybackStateCompat.ACTION_PLAY or
PlaybackStateCompat.ACTION_PAUSE or
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or
PlaybackStateCompat.ACTION_SKIP_TO_NEXT or
PlaybackStateCompat.ACTION_PLAY_PAUSE
)
.setState(state, pos, 1f, SystemClock.elapsedRealtime())
.build());
if(_focusRequest == null)
setAudioFocus();
_playbackState = state;
}
//TODO: (TBD) This code probably more fitting inside FutoVideoPlayer, as this service is generally only used for global events
@ -379,14 +380,26 @@ class MediaPlaybackService : Service() {
}
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
MediaControlReceiver.onPauseReceived.emit();
_audioFocusLossTime_ms = System.currentTimeMillis()
if (_playbackState != PlaybackStateCompat.STATE_PAUSED &&
_playbackState != PlaybackStateCompat.STATE_STOPPED &&
_playbackState != PlaybackStateCompat.STATE_NONE &&
_playbackState != PlaybackStateCompat.STATE_ERROR) {
_audioFocusLossTime_ms = System.currentTimeMillis()
}
Log.i(TAG, "Audio focus transient loss");
}
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
Log.i(TAG, "Audio focus transient loss, can duck");
}
AudioManager.AUDIOFOCUS_LOSS -> {
_audioFocusLossTime_ms = System.currentTimeMillis()
if (_playbackState != PlaybackStateCompat.STATE_PAUSED &&
_playbackState != PlaybackStateCompat.STATE_STOPPED &&
_playbackState != PlaybackStateCompat.STATE_NONE &&
_playbackState != PlaybackStateCompat.STATE_ERROR) {
_audioFocusLossTime_ms = System.currentTimeMillis()
}
_hasFocus = false;
MediaControlReceiver.onPauseReceived.emit();
Log.i(TAG, "Audio focus lost");

View file

@ -1,8 +1,12 @@
@file:Suppress("DEPRECATION")
package com.futo.platformplayer.states
import android.content.Context
import androidx.annotation.OptIn
import androidx.media3.common.C
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.DefaultLoadControl
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.upstream.DefaultAllocator
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
@ -13,10 +17,6 @@ import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.services.MediaPlaybackService
import com.futo.platformplayer.video.PlayerManager
import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.DefaultLoadControl
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.upstream.DefaultAllocator
import kotlin.random.Random
/***
@ -557,21 +557,23 @@ class StatePlayer {
}
//Player Initialization
fun getPlayerOrCreate(context : Context) : PlayerManager {
fun getPlayerOrCreate(context: Context) : PlayerManager {
if(_exoplayer == null) {
val player = createExoPlayer(context);
_exoplayer = PlayerManager(player);
}
return _exoplayer!!;
}
fun getThumbnailPlayerOrCreate(context : Context) : PlayerManager {
fun getThumbnailPlayerOrCreate(context: Context) : PlayerManager {
if(_thumbnailExoPlayer == null) {
val player = createExoPlayer(context);
_thumbnailExoPlayer = PlayerManager(player);
}
return _thumbnailExoPlayer!!;
}
private fun createExoPlayer(context : Context) : ExoPlayer {
@OptIn(UnstableApi::class)
private fun createExoPlayer(context : Context): ExoPlayer {
return ExoPlayer.Builder(context)
.setLoadControl(
DefaultLoadControl.Builder()
@ -589,7 +591,6 @@ class StatePlayer {
.build();
}
fun dispose(){
val player = _exoplayer;
val thumbPlayer = _thumbnailExoPlayer;

View file

@ -20,10 +20,11 @@ class CastingDeviceInfoStorage : FragmentedStorageFileJson() {
}
@Synchronized
fun addDevice(castingDeviceInfo: CastingDeviceInfo): Boolean {
if (deviceInfos.any { d -> d.name == castingDeviceInfo.name }) {
fun addDevice(castingDeviceInfo: CastingDeviceInfo): CastingDeviceInfo {
val foundDeviceInfo = deviceInfos.firstOrNull { d -> d.name == castingDeviceInfo.name }
if (foundDeviceInfo != null) {
Logger.i("CastingDeviceInfoStorage", "Device '${castingDeviceInfo.name}' already existed in device storage.")
return false;
return foundDeviceInfo;
}
if (deviceInfos.size >= 5) {
@ -32,7 +33,7 @@ class CastingDeviceInfoStorage : FragmentedStorageFileJson() {
deviceInfos.add(castingDeviceInfo);
save();
return true;
return castingDeviceInfo;
}
@Synchronized

View file

@ -1,15 +1,13 @@
@file:Suppress("DEPRECATION")
package com.futo.platformplayer.video
import android.media.session.PlaybackState
import android.support.v4.media.session.PlaybackStateCompat
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.ui.StyledPlayerView
import androidx.media3.common.Player
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.PlayerView
class PlayerManager {
private var _currentView: StyledPlayerView? = null;
private var _currentView: PlayerView? = null;
private val _stateMap = HashMap<String, PlayerState>();
private var _currentState: PlayerState? = null;
val currentState: PlayerState get() {
@ -25,6 +23,7 @@ class PlayerManager {
this.player = exoPlayer;
}
fun getPlaybackStateCompat() : Int {
return when(player.playbackState) {
ExoPlayer.STATE_READY -> if(player.playWhenReady) PlaybackStateCompat.STATE_PLAYING else PlaybackStateCompat.STATE_PAUSED;
@ -34,7 +33,7 @@ class PlayerManager {
}
@Synchronized
fun attach(view: StyledPlayerView, stateName: String) {
fun attach(view: PlayerView, stateName: String) {
if(view != _currentView) {
_currentView?.player = null;
switchState(stateName);

View file

@ -1,5 +1,3 @@
@file:Suppress("DEPRECATION")
package com.futo.platformplayer.views.casting
import android.content.Context
@ -13,7 +11,11 @@ import android.widget.FrameLayout
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.OptIn
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.media3.common.util.UnstableApi
import androidx.media3.ui.DefaultTimeBar
import androidx.media3.ui.TimeBar
import com.bumptech.glide.Glide
import com.futo.platformplayer.R
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
@ -23,9 +25,6 @@ import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.toHumanTime
import com.futo.platformplayer.views.behavior.GestureControlView
import com.google.android.exoplayer2.ui.DefaultTimeBar
import com.google.android.exoplayer2.ui.TimeBar
import com.google.android.exoplayer2.ui.TimeBar.OnScrubListener
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@ -39,6 +38,8 @@ class CastView : ConstraintLayout {
private val _buttonSettings: ImageButton;
private val _buttonLoop: ImageButton;
private val _buttonPlay: ImageButton;
private val _buttonPrevious: ImageButton;
private val _buttonNext: ImageButton;
private val _buttonPause: ImageButton;
private val _buttonCast: CastButton;
private val _textPosition: TextView;
@ -53,7 +54,10 @@ class CastView : ConstraintLayout {
val onMinimizeClick = Event0();
val onSettingsClick = Event0();
val onPrevious = Event0();
val onNext = Event0();
@OptIn(UnstableApi::class)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
LayoutInflater.from(context).inflate(R.layout.view_cast, this, true);
@ -62,6 +66,8 @@ class CastView : ConstraintLayout {
_buttonSettings = findViewById(R.id.button_settings);
_buttonLoop = findViewById(R.id.button_loop);
_buttonPlay = findViewById(R.id.button_play);
_buttonPrevious = findViewById(R.id.button_previous);
_buttonNext = findViewById(R.id.button_next);
_buttonPause = findViewById(R.id.button_pause);
_buttonCast = findViewById(R.id.button_cast);
_textPosition = findViewById(R.id.text_position);
@ -83,7 +89,7 @@ class CastView : ConstraintLayout {
}
_buttonLoop.setImageResource(if(StatePlayer.instance.loopVideo) R.drawable.ic_loop_active else R.drawable.ic_loop);
_timeBar.addListener(object : OnScrubListener {
_timeBar.addListener(object : TimeBar.OnScrubListener {
override fun onScrubStart(timeBar: TimeBar, position: Long) {
StateCasting.instance.videoSeekTo(position.toDouble());
}
@ -105,6 +111,29 @@ class CastView : ConstraintLayout {
if (!isInEditMode) {
setIsPlaying(false);
}
StatePlayer.instance.onQueueChanged.subscribe(this) {
CoroutineScope(Dispatchers.Main).launch(Dispatchers.Main) {
setLoopVisible(!StatePlayer.instance.hasQueue)
updateNextPrevious();
}
}
StatePlayer.instance.onVideoChanging.subscribe(this) {
CoroutineScope(Dispatchers.Main).launch(Dispatchers.Main) {
updateNextPrevious();
}
}
updateNextPrevious();
_buttonPrevious.setOnClickListener { onPrevious.emit() };
_buttonNext.setOnClickListener { onNext.emit() };
}
private fun updateNextPrevious() {
val vidPrev = StatePlayer.instance.getPrevQueueItem(true);
val vidNext = StatePlayer.instance.getNextQueueItem(true);
_buttonNext.visibility = if (vidNext != null) View.VISIBLE else View.GONE
_buttonPrevious.visibility = if (vidPrev != null) View.VISIBLE else View.GONE
}
fun stopTimeJob() {
@ -150,7 +179,6 @@ class CastView : ConstraintLayout {
}
val position = StateCasting.instance.activeDevice?.expectedCurrentTime?.times(1000.0)?.toLong();
if(StatePlayer.instance.hasMediaSession()) {
StatePlayer.instance.updateMediaSession(null);
StatePlayer.instance.updateMediaSessionPlaybackState(getPlaybackStateCompat(), (position ?: 0));
@ -183,6 +211,7 @@ class CastView : ConstraintLayout {
}
}
@OptIn(UnstableApi::class)
fun setVideoDetails(video: IPlatformVideoDetails, position: Long) {
Glide.with(_thumbnail)
.load(video.thumbnails.getHQThumbnail())
@ -194,6 +223,7 @@ class CastView : ConstraintLayout {
_timeBar.setDuration(video.duration);
}
@OptIn(UnstableApi::class)
fun setTime(ms: Long) {
_textPosition.text = ms.toHumanTime(true);
_timeBar.setPosition(ms / 1000);

View file

@ -18,8 +18,11 @@ class SlideUpMenuButtonList : LinearLayout {
val onClick = Event1<String>();
val buttons: HashMap<String, LinearLayout> = 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);

View file

@ -26,7 +26,7 @@ class SlideUpMenuOverlay : RelativeLayout {
private lateinit var _viewContainer: LinearLayout;
private var _animated: Boolean = true;
private var _groupItems: List<View>;
var groupItems: List<View>;
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<View>, 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)

View file

@ -1,5 +1,3 @@
@file:Suppress("DEPRECATION")
package com.futo.platformplayer.views.video
import android.content.Context
@ -9,6 +7,10 @@ import android.view.View
import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.TextView
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.media3.ui.PlayerControlView
import androidx.media3.ui.PlayerView
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
@ -17,8 +19,6 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.helpers.VideoHelper
import com.futo.platformplayer.toHumanTime
import com.futo.platformplayer.video.PlayerManager
import com.google.android.exoplayer2.ui.PlayerControlView
import com.google.android.exoplayer2.ui.StyledPlayerView
class FutoThumbnailPlayer : FutoVideoPlayerBase {
@ -28,7 +28,7 @@ class FutoThumbnailPlayer : FutoVideoPlayerBase {
}
//Views
private val videoView : StyledPlayerView;
private val videoView : PlayerView;
private val videoControls : PlayerControlView;
private val buttonMute : ImageButton;
private val buttonUnMute : ImageButton;
@ -41,7 +41,8 @@ class FutoThumbnailPlayer : FutoVideoPlayerBase {
private val _evMuteChanged = mutableListOf<(FutoThumbnailPlayer, Boolean)->Unit>();
constructor(context : Context, attrs: AttributeSet? = null) : super(PLAYER_STATE_NAME, context, attrs) {
@OptIn(UnstableApi::class)
constructor(context: Context, attrs: AttributeSet? = null) : super(PLAYER_STATE_NAME, context, attrs) {
LayoutInflater.from(context).inflate(R.layout.thumbnail_video_view, this, true);
videoView = findViewById(R.id.video_player);
@ -70,7 +71,7 @@ class FutoThumbnailPlayer : FutoVideoPlayerBase {
}
}
fun setLive(live : Boolean) {
fun setLive(live: Boolean) {
if(live) {
containerDuration.visibility = GONE;
containerLive.visibility = VISIBLE;
@ -81,7 +82,8 @@ class FutoThumbnailPlayer : FutoVideoPlayerBase {
}
}
fun setPlayer(player : PlayerManager?){
@OptIn(UnstableApi::class)
fun setPlayer(player: PlayerManager?){
changePlayer(player);
player?.attach(videoView, PLAYER_STATE_NAME);
videoControls.player = player?.player;

View file

@ -1,5 +1,3 @@
@file:Suppress("DEPRECATION")
package com.futo.platformplayer.views.video
import android.content.Context
@ -15,6 +13,7 @@ import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.FrameLayout
import android.widget.ImageButton
import android.widget.TextView
import androidx.annotation.OptIn
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.setMargins
import com.futo.platformplayer.R
@ -31,13 +30,14 @@ import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.views.behavior.GestureControlView
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.PlaybackParameters
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout
import com.google.android.exoplayer2.ui.PlayerControlView
import com.google.android.exoplayer2.ui.StyledPlayerView
import com.google.android.exoplayer2.ui.TimeBar
import com.google.android.exoplayer2.video.VideoSize
import androidx.media3.common.PlaybackParameters
import androidx.media3.common.VideoSize
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.AspectRatioFrameLayout
import androidx.media3.ui.PlayerControlView
import androidx.media3.ui.PlayerView
import androidx.media3.ui.TimeBar
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -58,7 +58,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
//Views
private val _root: ConstraintLayout;
private val _videoView: StyledPlayerView;
private val _videoView: PlayerView;
val videoControls: PlayerControlView;
private val _videoControls_fullscreen: PlayerControlView;
@ -127,6 +127,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
val onVideoClicked = Event0();
val onTimeBarChanged = Event2<Long, Long>();
@OptIn(UnstableApi::class)
constructor(context: Context, attrs: AttributeSet? = null) : super(PLAYER_STATE_NAME, context, attrs) {
LayoutInflater.from(context).inflate(R.layout.video_view, this, true);
_root = findViewById(R.id.videoview_root);
@ -139,8 +140,8 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
_control_rotate_lock = videoControls.findViewById(R.id.exo_rotate_lock);
_control_loop = videoControls.findViewById(R.id.exo_loop);
_control_cast = videoControls.findViewById(R.id.exo_cast);
_control_play = videoControls.findViewById(com.google.android.exoplayer2.ui.R.id.exo_play);
_time_bar = videoControls.findViewById(com.google.android.exoplayer2.ui.R.id.exo_progress);
_control_play = videoControls.findViewById(androidx.media3.ui.R.id.exo_play);
_time_bar = videoControls.findViewById(androidx.media3.ui.R.id.exo_progress);
_control_chapter = videoControls.findViewById(R.id.text_chapter_current);
_buttonNext = videoControls.findViewById(R.id.button_next);
_buttonPrevious = videoControls.findViewById(R.id.button_previous);
@ -152,9 +153,9 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
_control_rotate_lock_fullscreen = _videoControls_fullscreen.findViewById(R.id.exo_rotate_lock);
_control_loop_fullscreen = videoControls.findViewById(R.id.exo_loop);
_control_cast_fullscreen = _videoControls_fullscreen.findViewById(R.id.exo_cast);
_control_play_fullscreen = videoControls.findViewById(com.google.android.exoplayer2.ui.R.id.exo_play);
_control_play_fullscreen = videoControls.findViewById(androidx.media3.ui.R.id.exo_play);
_control_chapter_fullscreen = _videoControls_fullscreen.findViewById(R.id.text_chapter_current);
_time_bar_fullscreen = _videoControls_fullscreen.findViewById(com.google.android.exoplayer2.ui.R.id.exo_progress);
_time_bar_fullscreen = _videoControls_fullscreen.findViewById(androidx.media3.ui.R.id.exo_progress);
_buttonPrevious_fullscreen = _videoControls_fullscreen.findViewById(R.id.button_previous);
_buttonNext_fullscreen = _videoControls_fullscreen.findViewById(R.id.button_next);
@ -404,14 +405,15 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
return false;
}
@OptIn(UnstableApi::class)
fun setArtwork(drawable: Drawable?) {
if (drawable != null) {
_videoView.defaultArtwork = drawable;
_videoView.artworkDisplayMode = StyledPlayerView.ARTWORK_DISPLAY_MODE_FILL;
_videoView.artworkDisplayMode = PlayerView.ARTWORK_DISPLAY_MODE_FILL;
fitOrFill(isFullScreen);
} else {
_videoView.defaultArtwork = null;
_videoView.artworkDisplayMode = StyledPlayerView.ARTWORK_DISPLAY_MODE_OFF;
_videoView.artworkDisplayMode = PlayerView.ARTWORK_DISPLAY_MODE_OFF;
}
}
@ -436,6 +438,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
return exoPlayer?.player?.playbackParameters?.speed ?: 1.0f;
}
@OptIn(UnstableApi::class)
fun setFullScreen(fullScreen: Boolean) {
if (isFullScreen == fullScreen) {
return;
@ -538,6 +541,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
}
//Sizing
@OptIn(UnstableApi::class)
fun fitHeight(videoSize : VideoSize? = null){
Logger.i(TAG, "Video Fit Height");
if(_originalBottomMargin != 0) {

View file

@ -1,11 +1,27 @@
@file:Suppress("DEPRECATION")
package com.futo.platformplayer.views.video
import android.content.Context
import android.net.Uri
import android.util.AttributeSet
import android.widget.RelativeLayout
import androidx.annotation.OptIn
import androidx.media3.common.C
import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import androidx.media3.common.VideoSize
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.exoplayer.ExoPlayer
import androidx.media3.exoplayer.dash.DashMediaSource
import androidx.media3.exoplayer.hls.HlsMediaSource
import androidx.media3.exoplayer.source.MediaSource
import androidx.media3.exoplayer.source.MergingMediaSource
import androidx.media3.exoplayer.source.ProgressiveMediaSource
import androidx.media3.exoplayer.source.SingleSampleMediaSource
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
@ -28,22 +44,6 @@ 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.android.exoplayer2.C
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.PlaybackException
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.source.MediaSource
import com.google.android.exoplayer2.source.MergingMediaSource
import com.google.android.exoplayer2.source.ProgressiveMediaSource
import com.google.android.exoplayer2.source.SingleSampleMediaSource
import com.google.android.exoplayer2.source.dash.DashMediaSource
import com.google.android.exoplayer2.source.hls.HlsMediaSource
import com.google.android.exoplayer2.text.CueGroup
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
import com.google.android.exoplayer2.upstream.DefaultDataSource
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource
import com.google.android.exoplayer2.video.VideoSize
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -251,6 +251,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
_targetTrackAudioBitrate = bitrate;
updateTrackSelector();
}
@OptIn(UnstableApi::class)
private fun updateTrackSelector() {
var builder = DefaultTrackSelector.Parameters.Builder(context);
if(_targetTrackVideoHeight > 0) {
@ -298,6 +299,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
return loadSelectedSources(play, resume);
}
@OptIn(UnstableApi::class)
fun swapSubtitles(scope: CoroutineScope, subtitles: ISubtitleSource?) {
if(subtitles == null)
clearSubtitles();
@ -369,6 +371,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
}
//Video loads
@OptIn(UnstableApi::class)
private fun swapVideoSourceLocal(videoSource: LocalVideoSource) {
Logger.i(TAG, "Loading VideoSource [Local]");
val file = File(videoSource.filePath);
@ -377,14 +380,13 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
_lastVideoMediaSource = ProgressiveMediaSource.Factory(DefaultDataSource.Factory(context))
.createMediaSource(MediaItem.fromUri(Uri.fromFile(file)));
}
@OptIn(UnstableApi::class)
private fun swapVideoSourceUrlRange(videoSource: JSVideoUrlRangeSource) {
Logger.i(TAG, "Loading JSVideoUrlRangeSource");
if(videoSource.hasItag) {
//Temporary workaround for Youtube
try {
_lastVideoMediaSource = VideoHelper.convertItagSourceToChunkedDashSource(videoSource);
if(_lastVideoMediaSource == null)
throw java.lang.IllegalStateException("Dash manifest workaround failed");
return;
}
//If it fails to create the dash workaround, fallback to standard progressive
@ -397,18 +399,21 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
}
else throw IllegalArgumentException("source without itag data...");
}
@OptIn(UnstableApi::class)
private fun swapVideoSourceUrl(videoSource: IVideoUrlSource) {
Logger.i(TAG, "Loading VideoSource [Url]");
_lastVideoMediaSource = ProgressiveMediaSource.Factory(DefaultHttpDataSource.Factory()
.setUserAgent(DEFAULT_USER_AGENT))
.createMediaSource(MediaItem.fromUri(videoSource.getVideoUrl()));
}
@OptIn(UnstableApi::class)
private fun swapVideoSourceDash(videoSource: IDashManifestSource) {
Logger.i(TAG, "Loading VideoSource [Dash]");
_lastVideoMediaSource = DashMediaSource.Factory(DefaultHttpDataSource.Factory()
.setUserAgent(DEFAULT_USER_AGENT))
.createMediaSource(MediaItem.fromUri(videoSource.url))
}
@OptIn(UnstableApi::class)
private fun swapVideoSourceHLS(videoSource: IHLSManifestSource) {
Logger.i(TAG, "Loading VideoSource [HLS]");
_lastVideoMediaSource = HlsMediaSource.Factory(DefaultHttpDataSource.Factory()
@ -416,7 +421,9 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
.createMediaSource(MediaItem.fromUri(videoSource.url));
}
//Audio loads
@OptIn(UnstableApi::class)
private fun swapAudioSourceLocal(audioSource: LocalAudioSource) {
Logger.i(TAG, "Loading AudioSource [Local]");
val file = File(audioSource.filePath);
@ -425,6 +432,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
_lastAudioMediaSource = ProgressiveMediaSource.Factory(DefaultDataSource.Factory(context))
.createMediaSource(MediaItem.fromUri(Uri.fromFile(file)));
}
@OptIn(UnstableApi::class)
private fun swapAudioSourceUrlRange(audioSource: JSAudioUrlRangeSource) {
Logger.i(TAG, "Loading JSAudioUrlRangeSource");
if(audioSource.hasItag) {
@ -444,12 +452,14 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
}
else throw IllegalArgumentException("source without itag data...")
}
@OptIn(UnstableApi::class)
private fun swapAudioSourceUrl(audioSource: IAudioUrlSource) {
Logger.i(TAG, "Loading AudioSource [Url]");
_lastAudioMediaSource = ProgressiveMediaSource.Factory(DefaultHttpDataSource.Factory()
.setUserAgent(DEFAULT_USER_AGENT))
.createMediaSource(MediaItem.fromUri(audioSource.getAudioUrl()));
}
@OptIn(UnstableApi::class)
private fun swapAudioSourceHLS(audioSource: IHLSManifestAudioSource) {
Logger.i(TAG, "Loading AudioSource [HLS]");
_lastAudioMediaSource = HlsMediaSource.Factory(DefaultHttpDataSource.Factory()
@ -479,6 +489,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
return VideoHelper.selectBestAudioSource(video.video, PREFERED_AUDIO_CONTAINERS, preferredLanguage);
}
@OptIn(UnstableApi::class)
private fun loadSelectedSources(play: Boolean, resume: Boolean): Boolean {
val sourceVideo = if(!isAudioMode || _lastAudioMediaSource == null) _lastVideoMediaSource else null;
val sourceAudio = _lastAudioMediaSource;
@ -506,11 +517,9 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
return true;
}
@OptIn(UnstableApi::class)
private fun reloadMediaSource(play: Boolean = false, resume: Boolean = true) {
val player = exoPlayer
if (player == null)
return;
val player = exoPlayer ?: return
val positionBefore = player.player.currentPosition;
if(_mediaSource != null) {
player.player.setMediaSource(_mediaSource!!);
@ -564,7 +573,10 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
//PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE,
PlaybackException.ERROR_CODE_IO_UNSPECIFIED -> {
_shouldPlaybackRestartOnConnectivity = true;
_connectivityLossTime_ms = System.currentTimeMillis()
if (playing) {
_connectivityLossTime_ms = System.currentTimeMillis()
}
Logger.i(TAG, "IO error, set _shouldPlaybackRestartOnConnectivity=true _connectivityLossTime_ms=$_connectivityLossTime_ms");
}
}

View file

@ -1,26 +1,28 @@
package com.futo.platformplayer.views.video.datasources;
import static com.google.android.exoplayer2.upstream.HttpUtil.buildRangeRequestHeader;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Util.castNonNull;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Util.castNonNull;
import static androidx.media3.datasource.HttpUtil.buildRangeRequestHeader;
import static java.lang.Math.min;
import android.net.Uri;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestModifier;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.PlaybackException;
import com.google.android.exoplayer2.upstream.BaseDataSource;
import com.google.android.exoplayer2.upstream.DataSourceException;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.DataSpec.HttpMethod;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.HttpUtil;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
import androidx.media3.common.C;
import androidx.media3.common.PlaybackException;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.datasource.BaseDataSource;
import androidx.media3.datasource.DataSourceException;
import androidx.media3.datasource.DataSpec;
import androidx.media3.datasource.HttpDataSource;
import androidx.media3.datasource.HttpUtil;
import androidx.media3.datasource.TransferListener;
import com.google.common.base.Predicate;
import com.google.common.collect.ForwardingMap;
import com.google.common.collect.ImmutableMap;
@ -45,6 +47,7 @@ import java.util.zip.GZIPInputStream;
* Based on the default ExoPlayer DefaultHttpDataSource
*/
@UnstableApi
public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
public static final class Factory implements HttpDataSource.Factory {
@ -142,7 +145,7 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
/**
* Sets a content type {@link Predicate}. If a content type is rejected by the predicate then a
* {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link
* JSHttpDataSource#open(com.google.android.exoplayer2.upstream.DataSpec)}.
* JSHttpDataSource#open(androidx.media3.datasource.DataSpec)}.
*
* <p>The default is {@code null}.
*
@ -160,7 +163,7 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
*
* <p>The default is {@code null}.
*
* <p>See {@link com.google.android.exoplayer2.upstream.DataSource#addTransferListener(TransferListener)}.
* <p>See {@link androidx.media3.datasource.DataSource#addTransferListener(TransferListener)}.
*
* @param transferListener The listener that will be used.
* @return This factory.
@ -367,12 +370,12 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
if (dataSpec.length != C.LENGTH_UNSET) {
bytesToRead = dataSpec.length;
} else {
long contentLength =
HttpUtil.getContentLength(
connection.getHeaderField(HttpHeaders.CONTENT_LENGTH),
connection.getHeaderField(HttpHeaders.CONTENT_RANGE));
bytesToRead =
contentLength != C.LENGTH_UNSET ? (contentLength - bytesToSkip) : C.LENGTH_UNSET;
long contentLength = HttpUtil.getContentLength(
connection.getHeaderField(HttpHeaders.CONTENT_LENGTH),
connection.getHeaderField(HttpHeaders.CONTENT_RANGE)
);
bytesToRead = contentLength != C.LENGTH_UNSET ? (contentLength - bytesToSkip) : C.LENGTH_UNSET;
}
} else {
// Gzip is enabled. If the server opts to use gzip then the content length in the response
@ -457,7 +460,7 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
/** Establishes a connection, following redirects to do so where permitted. */
private HttpURLConnection makeConnection(DataSpec dataSpec) throws IOException {
URL url = new URL(dataSpec.uri.toString());
@HttpMethod int httpMethod = dataSpec.httpMethod;
@DataSpec.HttpMethod int httpMethod = dataSpec.httpMethod;
@Nullable byte[] httpBody = dataSpec.httpBody;
long position = dataSpec.position;
long length = dataSpec.length;
@ -543,7 +546,7 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
*/
private HttpURLConnection makeConnection(
URL url,
@HttpMethod int httpMethod,
@DataSpec.HttpMethod int httpMethod,
@Nullable byte[] httpBody,
long position,
long length,

View file

@ -39,7 +39,7 @@
android:elevation="4dp"
android:layout_marginBottom="6dp" />
<com.google.android.exoplayer2.ui.PlayerControlView
<androidx.media3.ui.PlayerControlView
android:id="@+id/videodetail_progress"
android:layout_width="match_parent"
android:layout_height="12dp"

View file

@ -103,7 +103,7 @@
android:textStyle="normal" />
</LinearLayout>
<com.google.android.exoplayer2.ui.DefaultTimeBar
<androidx.media3.ui.DefaultTimeBar
android:id="@id/exo_progress"
android:layout_width="match_parent"
android:layout_height="16dp"

View file

@ -4,7 +4,7 @@
android:layout_height="wrap_content"
android:background="@color/transparent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.google.android.exoplayer2.ui.StyledPlayerView
<androidx.media3.ui.PlayerView
android:id="@+id/video_player"
android:layout_width="match_parent"
android:layout_height="match_parent"
@ -15,7 +15,7 @@
app:resize_mode="fit"
app:show_buffering="when_playing"
android:layout_marginBottom="6dp" />
<com.google.android.exoplayer2.ui.PlayerControlView
<androidx.media3.ui.PlayerControlView
android:id="@+id/video_player_controller"
android:layout_width="match_parent"
android:layout_height="match_parent"

View file

@ -198,12 +198,12 @@
</TextView>
<com.google.android.exoplayer2.ui.SubtitleView
<androidx.media3.ui.SubtitleView
android:id="@id/exo_subtitles"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.google.android.exoplayer2.ui.DefaultTimeBar
<androidx.media3.ui.DefaultTimeBar
android:id="@id/exo_progress"
android:layout_width="match_parent"
android:layout_height="16dp"

View file

@ -16,7 +16,7 @@
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent">
<com.google.android.exoplayer2.ui.DefaultTimeBar
<androidx.media3.ui.DefaultTimeBar
android:id="@id/exo_progress"
android:layout_width="match_parent"
android:layout_height="12dp"

View file

@ -227,7 +227,7 @@
</TextView>
<com.google.android.exoplayer2.ui.DefaultTimeBar
<androidx.media3.ui.DefaultTimeBar
android:id="@id/exo_progress"
android:layout_width="match_parent"
android:layout_height="12dp"

View file

@ -6,7 +6,7 @@
android:id="@+id/videoview_root"
android:background="@color/transparent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.google.android.exoplayer2.ui.StyledPlayerView
<androidx.media3.ui.PlayerView
android:id="@+id/video_player"
android:layout_width="match_parent"
android:layout_height="match_parent"
@ -16,7 +16,7 @@
app:show_buffering="always"
android:layout_marginBottom="6dp" />
<!--
<com.google.android.exoplayer2.ui.PlayerControlView
<androidx.media3.ui.PlayerControlView
android:id="@+id/video_player_bar"
android:layout_width="match_parent"
android:layout_height="match_parent"
@ -48,7 +48,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.exoplayer2.ui.PlayerControlView
<androidx.media3.ui.PlayerControlView
android:id="@+id/video_player_controller"
android:layout_width="match_parent"
android:layout_height="match_parent"
@ -56,7 +56,7 @@
android:layout_marginRight="-6dp"
app:show_timeout="-1"
app:controller_layout_id="@layout/video_player_ui" />
<com.google.android.exoplayer2.ui.PlayerControlView
<androidx.media3.ui.PlayerControlView
android:id="@+id/video_player_controller_fullscreen"
android:layout_width="match_parent"
android:layout_height="match_parent"

View file

@ -73,6 +73,19 @@
app:srcCompat="@drawable/ic_settings" />
</LinearLayout>
<ImageButton
android:id="@id/button_previous"
android:layout_width="60dp"
android:layout_height="60dp"
android:scaleType="centerCrop"
android:clickable="true"
android:layout_marginRight="40dp"
android:padding="5dp"
app:srcCompat="@drawable/ic_skip_previous"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toLeftOf="@id/button_play"
app:layout_constraintBottom_toBottomOf="parent" />
<ImageButton
android:id="@+id/button_play"
android:layout_width="60dp"
@ -85,6 +98,20 @@
android:clickable="true"
app:srcCompat="@drawable/ic_play_white_nopad" />
<ImageButton
android:id="@id/button_next"
android:layout_width="60dp"
android:layout_height="60dp"
android:clickable="true"
android:scaleType="centerCrop"
android:padding="5dp"
android:layout_marginLeft="40dp"
app:srcCompat="@drawable/ic_skip_next"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toRightOf="@id/button_play"
app:layout_constraintBottom_toBottomOf="parent" />
<ImageButton
android:id="@+id/button_pause"
android:layout_width="60dp"
@ -143,7 +170,7 @@
app:layout_constraintTop_toTopOf="@id/text_position"
app:layout_constraintBottom_toBottomOf="@id/text_position"/>
<com.google.android.exoplayer2.ui.DefaultTimeBar
<androidx.media3.ui.DefaultTimeBar
android:id="@+id/time_progress"
android:layout_width="match_parent"
android:layout_height="16dp"

View file

@ -342,6 +342,8 @@
<string name="give_feedback_on_the_application">Give feedback on the application</string>
<string name="info">Info</string>
<string name="live_chat_webview">Live Chat Webview</string>
<string name="full_screen_portrait">Fullscreen portrait</string>
<string name="allow_full_screen_portrait">Allow fullscreen portrait</string>
<string name="background_switch_audio">Switch to Audio in Background</string>
<string name="background_switch_audio_description">Optimize bandwidth usage by switching to audio-only stream in background if available, may cause stutter</string>
<string name="preview_feed_items">Preview Feed Items</string>
@ -712,6 +714,7 @@
<string name="fcast_technical_documentation">FCast Technical Documentation</string>
<string name="login_to_view_your_comments">Login to view your comments</string>
<string name="polycentric_is_disabled">Polycentric is disabled</string>
<string name="play_pause">Play Pause</string>
<string-array name="home_screen_array">
<item>Recommendations</item>
<item>Subscriptions</item>

@ -1 +1 @@
Subproject commit fc5d17e19067efc0d28192b43de31f9bc499d288
Subproject commit d41cc8e848891ef8e949e6d49384b754e7c305c7

@ -1 +1 @@
Subproject commit 128b03c5911d414ad230afd75c35c6be5b1e87db
Subproject commit d41cc8e848891ef8e949e6d49384b754e7c305c7