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") implementation("com.caoccao.javet:javet-android:2.2.1")
//Exoplayer //Exoplayer
implementation 'com.google.android.exoplayer:exoplayer-core:2.19.1' implementation 'androidx.media3:media3-exoplayer:1.2.0'
implementation 'com.google.android.exoplayer:exoplayer-dash:2.19.1' implementation 'androidx.media3:media3-exoplayer-dash:1.2.0'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.19.1' implementation 'androidx.media3:media3-ui:1.2.0'
implementation 'com.google.android.exoplayer:exoplayer-hls:2.19.1' implementation 'androidx.media3:media3-exoplayer-hls:1.2.0'
implementation 'com.google.android.exoplayer:exoplayer-rtsp:2.19.1' implementation 'androidx.media3:media3-exoplayer-rtsp:1.2.0'
implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.19.1' implementation 'androidx.media3:media3-exoplayer-smoothstreaming:1.2.0'
implementation 'com.google.android.exoplayer:exoplayer-transformer:2.19.1' implementation 'androidx.media3:media3-transformer:1.2.0'
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.5' implementation 'androidx.navigation:navigation-fragment-ktx:2.7.5'
implementation 'androidx.navigation:navigation-ui-ktx:2.7.5' implementation 'androidx.navigation:navigation-ui-ktx:2.7.5'
implementation 'androidx.media:media:1.7.0'
//Other //Other
implementation 'org.jmdns:jmdns:3.5.1' implementation 'org.jmdns:jmdns:3.5.1'

View File

@ -6,28 +6,41 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import android.webkit.CookieManager import android.webkit.CookieManager
import androidx.lifecycle.lifecycleScope 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.api.http.ManagedHttpClient
import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.serializers.FlexibleBooleanSerializer import com.futo.platformplayer.serializers.FlexibleBooleanSerializer
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer 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.FragmentedStorage
import com.futo.platformplayer.stores.FragmentedStorageFileJson import com.futo.platformplayer.stores.FragmentedStorageFileJson
import com.futo.platformplayer.views.FeedStyle import com.futo.platformplayer.views.FeedStyle
import com.futo.platformplayer.views.fields.DropdownFieldOptionsId 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.FieldForm
import com.futo.platformplayer.views.fields.FormField
import com.futo.platformplayer.views.fields.FormFieldButton import com.futo.platformplayer.views.fields.FormFieldButton
import com.futo.platformplayer.views.fields.FormFieldWarning import com.futo.platformplayer.views.fields.FormFieldWarning
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.* import kotlinx.serialization.Serializable
import kotlinx.serialization.json.* import kotlinx.serialization.Transient
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.io.File import java.io.File
import java.time.OffsetDateTime 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) @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) @DropdownFieldOptionsId(R.array.restart_playback_after_loss)
var restartPlaybackAfterConnectivityLoss: Int = 1; 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) @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 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.V8Value
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource 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.engine.V8Plugin
import com.futo.platformplayer.orNull import com.futo.platformplayer.orNull
import com.futo.platformplayer.views.video.datasources.JSHttpDataSource 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 { abstract class JSSource {
protected val _config: IV8PluginConfig; protected val _config: IV8PluginConfig;

View File

@ -1,12 +1,17 @@
package com.futo.platformplayer.casting package com.futo.platformplayer.casting
import android.os.Looper import android.os.Looper
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.getConnectedSocket import com.futo.platformplayer.getConnectedSocket
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.CastingDeviceInfo import com.futo.platformplayer.models.CastingDeviceInfo
import com.futo.platformplayer.toInetAddress import com.futo.platformplayer.toInetAddress
import kotlinx.coroutines.* import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import java.net.InetAddress import java.net.InetAddress
import java.util.UUID import java.util.UUID
@ -18,7 +23,7 @@ class AirPlayCastingDevice : CastingDevice {
override var usedRemoteAddress: InetAddress? = null; override var usedRemoteAddress: InetAddress? = null;
override var localAddress: InetAddress? = null; override var localAddress: InetAddress? = null;
override val canSetVolume: Boolean get() = false; override val canSetVolume: Boolean get() = false;
override val canSetSpeed: Boolean get() = false; //TODO: Implement playback speed for AirPlay override val canSetSpeed: Boolean get() = true;
var addresses: Array<InetAddress>? = null; var addresses: Array<InetAddress>? = null;
var port: Int = 0; var port: Int = 0;
@ -59,6 +64,10 @@ class AirPlayCastingDevice : CastingDevice {
} else { } else {
post("play", "text/parameters", "Content-Location: $contentId\r\nStart-Position: 0"); post("play", "text/parameters", "Content-Location: $contentId\r\nStart-Position: 0");
} }
if (speed != null) {
changeSpeed(speed)
}
} }
override fun loadContent(contentType: String, content: String, resumePosition: Double, duration: Double, speed: Double?) { override fun loadContent(contentType: String, content: String, resumePosition: Double, duration: Double, speed: Double?) {
@ -186,6 +195,11 @@ class AirPlayCastingDevice : CastingDevice {
_scopeIO = null; _scopeIO = null;
} }
override fun changeSpeed(speed: Double) {
this.speed = speed
post("rate?value=$speed")
}
override fun getDeviceInfo(): CastingDeviceInfo { override fun getDeviceInfo(): CastingDeviceInfo {
return CastingDeviceInfo(name!!, CastProtocolType.AIRPLAY, addresses!!.filter { a -> a.hostAddress != null }.map { a -> a.hostAddress!! }.toTypedArray(), port); return CastingDeviceInfo(name!!, CastProtocolType.AIRPLAY, addresses!!.filter { a -> a.hostAddress != null }.map { a -> a.hostAddress!! }.toTypedArray(), port);
} }

View File

@ -81,7 +81,7 @@ abstract class CastingDevice {
var speed: Double = 1.0 var speed: Double = 1.0
set(value) { set(value) {
val changed = value != field; val changed = value != field;
speed = value; field = value;
if (changed) { if (changed) {
onSpeedChanged.emit(value); onSpeedChanged.emit(value);
} }

View File

@ -3,14 +3,24 @@ package com.futo.platformplayer.casting
import android.os.Looper import android.os.Looper
import android.util.Log import android.util.Log
import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.casting.models.* import com.futo.platformplayer.casting.models.FCastPlayMessage
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.casting.models.FCastPlaybackErrorMessage
import com.futo.platformplayer.casting.models.FCastPlaybackUpdateMessage
import com.futo.platformplayer.casting.models.FCastSeekMessage
import com.futo.platformplayer.casting.models.FCastSetSpeedMessage
import com.futo.platformplayer.casting.models.FCastSetVolumeMessage
import com.futo.platformplayer.casting.models.FCastVersionMessage
import com.futo.platformplayer.casting.models.FCastVolumeUpdateMessage
import com.futo.platformplayer.getConnectedSocket import com.futo.platformplayer.getConnectedSocket
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.CastingDeviceInfo import com.futo.platformplayer.models.CastingDeviceInfo
import com.futo.platformplayer.toHexString import com.futo.platformplayer.toHexString
import com.futo.platformplayer.toInetAddress import com.futo.platformplayer.toInetAddress
import kotlinx.coroutines.* import kotlinx.coroutines.CoroutineScope
import kotlinx.serialization.decodeFromString import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.io.DataInputStream import java.io.DataInputStream
@ -89,6 +99,8 @@ class FCastCastingDevice : CastingDevice {
time = resumePosition, time = resumePosition,
speed = speed speed = speed
)); ));
this.speed = speed ?: 1.0
} }
override fun loadContent(contentType: String, content: String, resumePosition: Double, duration: Double, speed: Double?) { override fun loadContent(contentType: String, content: String, resumePosition: Double, duration: Double, speed: Double?) {
@ -110,6 +122,8 @@ class FCastCastingDevice : CastingDevice {
time = resumePosition, time = resumePosition,
speed = speed speed = speed
)); ));
this.speed = speed ?: 1.0
} }
override fun changeVolume(volume: Double) { override fun changeVolume(volume: Double) {
@ -122,12 +136,12 @@ class FCastCastingDevice : CastingDevice {
} }
override fun changeSpeed(speed: Double) { override fun changeSpeed(speed: Double) {
if (invokeInIOScopeIfRequired({ changeSpeed(volume) })) { if (invokeInIOScopeIfRequired({ changeSpeed(speed) })) {
return; return;
} }
this.speed = speed this.speed = speed
sendMessage(Opcode.SET_SPEED, FCastSetSpeedMessage(volume)) sendMessage(Opcode.SET_SPEED, FCastSetSpeedMessage(speed))
} }
override fun seekVideo(timeSeconds: Double) { override fun seekVideo(timeSeconds: Double) {
@ -247,7 +261,8 @@ class FCastCastingDevice : CastingDevice {
val buffer = ByteArray(4096); val buffer = ByteArray(4096);
Logger.i(TAG, "Started receiving."); Logger.i(TAG, "Started receiving.");
while (_scopeIO?.isActive == true) { var exceptionOccurred = false;
while (_scopeIO?.isActive == true && !exceptionOccurred) {
try { try {
val inputStream = _inputStream ?: break; val inputStream = _inputStream ?: break;
Log.d(TAG, "Receiving next packet..."); Log.d(TAG, "Receiving next packet...");
@ -275,20 +290,25 @@ class FCastCastingDevice : CastingDevice {
} }
try { try {
handleMessage(Opcode.values().first { it.value == opcode }, json); handleMessage(Opcode.entries.first { it.value == opcode }, json);
} catch (e:Throwable) { } catch (e:Throwable) {
Logger.w(TAG, "Failed to handle message.", e); Logger.w(TAG, "Failed to handle message.", e);
} }
} catch (e: java.net.SocketException) { } catch (e: java.net.SocketException) {
Logger.e(TAG, "Socket exception while receiving.", e); Logger.e(TAG, "Socket exception while receiving.", e);
break; exceptionOccurred = true;
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.e(TAG, "Exception while receiving.", e); 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; connectionState = CastConnectionState.CONNECTING;
Thread.sleep(3000); Thread.sleep(3000);

View File

@ -5,6 +5,7 @@ import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.Looper import android.os.Looper
import android.util.Base64 import android.util.Base64
import android.util.Log
import com.futo.platformplayer.BuildConfig import com.futo.platformplayer.BuildConfig
import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
@ -67,6 +68,7 @@ class StateCasting {
val onActiveDeviceTimeChanged = Event1<Double>(); val onActiveDeviceTimeChanged = Event1<Double>();
var activeDevice: CastingDevice? = null; var activeDevice: CastingDevice? = null;
private val _client = ManagedHttpClient(); private val _client = ManagedHttpClient();
var _resumeCastingDevice: CastingDeviceInfo? = null;
val isCasting: Boolean get() = activeDevice != null; val isCasting: Boolean get() = activeDevice != null;
@ -182,28 +184,42 @@ class StateCasting {
val networkConfig = Json.decodeFromString<FCastNetworkConfig>(json) val networkConfig = Json.decodeFromString<FCastNetworkConfig>(json)
val tcpService = networkConfig.services.first { v -> v.type == 0 } val tcpService = networkConfig.services.first { v -> v.type == 0 }
addRememberedDevice(CastingDeviceInfo( val foundInfo = addRememberedDevice(CastingDeviceInfo(
name = networkConfig.name, name = networkConfig.name,
type = CastProtocolType.FCAST, type = CastProtocolType.FCAST,
addresses = networkConfig.addresses.toTypedArray(), addresses = networkConfig.addresses.toTypedArray(),
port = tcpService.port port = tcpService.port
)) ))
UIDialogs.toast(context,"FCast device '${networkConfig.name}' added") connectDevice(deviceFromCastingDeviceInfo(foundInfo))
} }
fun onStop() { fun onStop() {
val ad = activeDevice ?: return; val ad = activeDevice ?: return;
_resumeCastingDevice = ad.getDeviceInfo()
Log.i(TAG, "_resumeCastingDevice set to '${ad.name}'")
Logger.i(TAG, "Stopping active device because of onStop."); Logger.i(TAG, "Stopping active device because of onStop.");
ad.stop(); ad.stop();
} }
fun onResume() {
val resumeCastingDevice = _resumeCastingDevice
if (resumeCastingDevice != null) {
connectDevice(deviceFromCastingDeviceInfo(resumeCastingDevice))
_resumeCastingDevice = null
Log.i(TAG, "_resumeCastingDevice set to null onResume")
}
}
@Synchronized @Synchronized
fun start(context: Context) { fun start(context: Context) {
if (_started) if (_started)
return; return;
_started = true; _started = true;
Log.i(TAG, "_resumeCastingDevice set null start")
_resumeCastingDevice = null;
Logger.i(TAG, "CastingService starting..."); Logger.i(TAG, "CastingService starting...");
rememberedDevices.clear(); rememberedDevices.clear();
@ -246,6 +262,7 @@ class StateCasting {
try { try {
jmDNS.removeServiceListener("_googlecast._tcp.local.", _chromecastServiceListener); jmDNS.removeServiceListener("_googlecast._tcp.local.", _chromecastServiceListener);
jmDNS.removeServiceListener("_airplay._tcp", _airPlayServiceListener); jmDNS.removeServiceListener("_airplay._tcp", _airPlayServiceListener);
jmDNS.removeServiceListener("_fastcast._tcp.local.", _fastCastServiceListener);
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
jmDNS.removeServiceTypeListener(_serviceTypeListener); jmDNS.removeServiceTypeListener(_serviceTypeListener);
@ -335,15 +352,20 @@ class StateCasting {
Logger.i(TAG, "Connect to device ${device.name}"); Logger.i(TAG, "Connect to device ${device.name}");
} }
fun addRememberedDevice(deviceInfo: CastingDeviceInfo) { fun addRememberedDevice(deviceInfo: CastingDeviceInfo): CastingDeviceInfo {
val device = deviceFromCastingDeviceInfo(deviceInfo); val device = deviceFromCastingDeviceInfo(deviceInfo);
addRememberedDevice(device); return addRememberedDevice(device);
} }
fun addRememberedDevice(device: CastingDevice) { fun addRememberedDevice(device: CastingDevice): CastingDeviceInfo {
if (_storage.addDevice(device.getDeviceInfo())) { val deviceInfo = device.getDeviceInfo()
val foundInfo = _storage.addDevice(deviceInfo)
if (foundInfo == deviceInfo) {
rememberedDevices.add(device); rememberedDevices.add(device);
return foundInfo;
} }
return foundInfo;
} }
fun removeRememberedDevice(device: CastingDevice) { fun removeRememberedDevice(device: CastingDevice) {
@ -361,7 +383,7 @@ class StateCasting {
action(); action();
} }
fun castIfAvailable(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: ISubtitleSource?, ms: Long = -1): Boolean { fun castIfAvailable(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: ISubtitleSource?, ms: Long = -1, speed: Double?): Boolean {
val ad = activeDevice ?: return false; val ad = activeDevice ?: return false;
if (ad.connectionState != CastConnectionState.CONNECTED) { if (ad.connectionState != CastConnectionState.CONNECTED) {
return false; return false;
@ -382,23 +404,23 @@ class StateCasting {
if (videoSource is LocalVideoSource || audioSource is LocalAudioSource || subtitleSource is LocalSubtitleSource) { if (videoSource is LocalVideoSource || audioSource is LocalAudioSource || subtitleSource is LocalSubtitleSource) {
if (ad is AirPlayCastingDevice) { if (ad is AirPlayCastingDevice) {
Logger.i(TAG, "Casting as local HLS"); Logger.i(TAG, "Casting as local HLS");
castLocalHls(video, videoSource as LocalVideoSource?, audioSource as LocalAudioSource?, subtitleSource as LocalSubtitleSource?, resumePosition); castLocalHls(video, videoSource as LocalVideoSource?, audioSource as LocalAudioSource?, subtitleSource as LocalSubtitleSource?, resumePosition, speed);
} else { } else {
Logger.i(TAG, "Casting as local DASH"); Logger.i(TAG, "Casting as local DASH");
castLocalDash(video, videoSource as LocalVideoSource?, audioSource as LocalAudioSource?, subtitleSource as LocalSubtitleSource?, resumePosition); castLocalDash(video, videoSource as LocalVideoSource?, audioSource as LocalAudioSource?, subtitleSource as LocalSubtitleSource?, resumePosition, speed);
} }
} else { } else {
StateApp.instance.scope.launch(Dispatchers.IO) { StateApp.instance.scope.launch(Dispatchers.IO) {
try { try {
if (ad is FCastCastingDevice) { if (ad is FCastCastingDevice) {
Logger.i(TAG, "Casting as DASH direct"); Logger.i(TAG, "Casting as DASH direct");
castDashDirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition); castDashDirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
} else if (ad is AirPlayCastingDevice) { } else if (ad is AirPlayCastingDevice) {
Logger.i(TAG, "Casting as HLS indirect"); Logger.i(TAG, "Casting as HLS indirect");
castHlsIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition); castHlsIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
} else { } else {
Logger.i(TAG, "Casting as DASH indirect"); Logger.i(TAG, "Casting as DASH indirect");
castDashIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition); castDashIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
} }
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.e(TAG, "Failed to start casting DASH videoSource=${videoSource} audioSource=${audioSource}.", e); Logger.e(TAG, "Failed to start casting DASH videoSource=${videoSource} audioSource=${audioSource}.", e);
@ -408,32 +430,32 @@ class StateCasting {
} else { } else {
if (videoSource is IVideoUrlSource) { if (videoSource is IVideoUrlSource) {
Logger.i(TAG, "Casting as singular video"); Logger.i(TAG, "Casting as singular video");
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoSource.getVideoUrl(), resumePosition, video.duration.toDouble(), null); ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoSource.getVideoUrl(), resumePosition, video.duration.toDouble(), speed);
} else if (audioSource is IAudioUrlSource) { } else if (audioSource is IAudioUrlSource) {
Logger.i(TAG, "Casting as singular audio"); Logger.i(TAG, "Casting as singular audio");
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioSource.getAudioUrl(), resumePosition, video.duration.toDouble(), null); ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioSource.getAudioUrl(), resumePosition, video.duration.toDouble(), speed);
} else if(videoSource is IHLSManifestSource) { } else if(videoSource is IHLSManifestSource) {
if (ad is ChromecastCastingDevice) { if (ad is ChromecastCastingDevice) {
Logger.i(TAG, "Casting as proxied HLS"); Logger.i(TAG, "Casting as proxied HLS");
castProxiedHls(video, videoSource.url, videoSource.codec, resumePosition); castProxiedHls(video, videoSource.url, videoSource.codec, resumePosition, speed);
} else { } else {
Logger.i(TAG, "Casting as non-proxied HLS"); Logger.i(TAG, "Casting as non-proxied HLS");
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoSource.url, resumePosition, video.duration.toDouble(), null); ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoSource.url, resumePosition, video.duration.toDouble(), speed);
} }
} else if(audioSource is IHLSManifestAudioSource) { } else if(audioSource is IHLSManifestAudioSource) {
if (ad is ChromecastCastingDevice) { if (ad is ChromecastCastingDevice) {
Logger.i(TAG, "Casting as proxied audio HLS"); Logger.i(TAG, "Casting as proxied audio HLS");
castProxiedHls(video, audioSource.url, audioSource.codec, resumePosition); castProxiedHls(video, audioSource.url, audioSource.codec, resumePosition, speed);
} else { } else {
Logger.i(TAG, "Casting as non-proxied audio HLS"); Logger.i(TAG, "Casting as non-proxied audio HLS");
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioSource.url, resumePosition, video.duration.toDouble(), null); ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioSource.url, resumePosition, video.duration.toDouble(), speed);
} }
} else if (videoSource is LocalVideoSource) { } else if (videoSource is LocalVideoSource) {
Logger.i(TAG, "Casting as local video"); Logger.i(TAG, "Casting as local video");
castLocalVideo(video, videoSource, resumePosition); castLocalVideo(video, videoSource, resumePosition, speed);
} else if (audioSource is LocalAudioSource) { } else if (audioSource is LocalAudioSource) {
Logger.i(TAG, "Casting as local audio"); Logger.i(TAG, "Casting as local audio");
castLocalAudio(video, audioSource, resumePosition); castLocalAudio(video, audioSource, resumePosition, speed);
} else { } else {
var str = listOf( var str = listOf(
if(videoSource != null) "Video: ${videoSource::class.java.simpleName}" else null, if(videoSource != null) "Video: ${videoSource::class.java.simpleName}" else null,
@ -471,15 +493,7 @@ class StateCasting {
return true; return true;
} }
private fun castVideoIndirect() { private fun castLocalVideo(video: IPlatformVideoDetails, videoSource: LocalVideoSource, resumePosition: Double, speed: Double?) : List<String> {
}
private fun castAudioIndirect() {
}
private fun castLocalVideo(video: IPlatformVideoDetails, videoSource: LocalVideoSource, resumePosition: Double) : List<String> {
val ad = activeDevice ?: return listOf(); val ad = activeDevice ?: return listOf();
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}"; val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
@ -493,12 +507,12 @@ class StateCasting {
).withTag("cast"); ).withTag("cast");
Logger.i(TAG, "Casting local video (videoUrl: $videoUrl)."); Logger.i(TAG, "Casting local video (videoUrl: $videoUrl).");
ad.loadVideo("BUFFERED", videoSource.container, videoUrl, resumePosition, video.duration.toDouble(), null); ad.loadVideo("BUFFERED", videoSource.container, videoUrl, resumePosition, video.duration.toDouble(), speed);
return listOf(videoUrl); return listOf(videoUrl);
} }
private fun castLocalAudio(video: IPlatformVideoDetails, audioSource: LocalAudioSource, resumePosition: Double) : List<String> { private fun castLocalAudio(video: IPlatformVideoDetails, audioSource: LocalAudioSource, resumePosition: Double, speed: Double?) : List<String> {
val ad = activeDevice ?: return listOf(); val ad = activeDevice ?: return listOf();
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}"; val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
@ -512,12 +526,12 @@ class StateCasting {
).withTag("cast"); ).withTag("cast");
Logger.i(TAG, "Casting local audio (audioUrl: $audioUrl)."); Logger.i(TAG, "Casting local audio (audioUrl: $audioUrl).");
ad.loadVideo("BUFFERED", audioSource.container, audioUrl, resumePosition, video.duration.toDouble(), null); ad.loadVideo("BUFFERED", audioSource.container, audioUrl, resumePosition, video.duration.toDouble(), speed);
return listOf(audioUrl); return listOf(audioUrl);
} }
private fun castLocalHls(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double): List<String> { private fun castLocalHls(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double, speed: Double?): List<String> {
val ad = activeDevice ?: return listOf() val ad = activeDevice ?: return listOf()
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}" val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}"
@ -608,12 +622,12 @@ class StateCasting {
).withTag("castLocalHls") ).withTag("castLocalHls")
Logger.i(TAG, "added new castLocalHls handlers (hlsPath: $hlsPath, videoPath: $videoPath, audioPath: $audioPath, subtitlePath: $subtitlePath).") Logger.i(TAG, "added new castLocalHls handlers (hlsPath: $hlsPath, videoPath: $videoPath, audioPath: $audioPath, subtitlePath: $subtitlePath).")
ad.loadVideo("BUFFERED", "application/vnd.apple.mpegurl", hlsUrl, resumePosition, video.duration.toDouble(), null) ad.loadVideo("BUFFERED", "application/vnd.apple.mpegurl", hlsUrl, resumePosition, video.duration.toDouble(), speed)
return listOf(hlsUrl, videoUrl, audioUrl, subtitleUrl) return listOf(hlsUrl, videoUrl, audioUrl, subtitleUrl)
} }
private fun castLocalDash(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double) : List<String> { private fun castLocalDash(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
val ad = activeDevice ?: return listOf(); val ad = activeDevice ?: return listOf();
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}"; val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
@ -654,12 +668,12 @@ class StateCasting {
} }
Logger.i(TAG, "added new castLocalDash handlers (dashPath: $dashPath, videoPath: $videoPath, audioPath: $audioPath, subtitlePath: $subtitlePath)."); Logger.i(TAG, "added new castLocalDash handlers (dashPath: $dashPath, videoPath: $videoPath, audioPath: $audioPath, subtitlePath: $subtitlePath).");
ad.loadVideo("BUFFERED", "application/dash+xml", dashUrl, resumePosition, video.duration.toDouble(), null); ad.loadVideo("BUFFERED", "application/dash+xml", dashUrl, resumePosition, video.duration.toDouble(), speed);
return listOf(dashUrl, videoUrl, audioUrl, subtitleUrl); return listOf(dashUrl, videoUrl, audioUrl, subtitleUrl);
} }
private suspend fun castDashDirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double) : List<String> { private suspend fun castDashDirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
val ad = activeDevice ?: return listOf(); val ad = activeDevice ?: return listOf();
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}"; val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
@ -699,12 +713,12 @@ class StateCasting {
val content = DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitlesUrl); val content = DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitlesUrl);
Logger.i(TAG, "Direct dash cast to casting device (videoUrl: $videoUrl, audioUrl: $audioUrl)."); Logger.i(TAG, "Direct dash cast to casting device (videoUrl: $videoUrl, audioUrl: $audioUrl).");
ad.loadContent("application/dash+xml", content, resumePosition, video.duration.toDouble(), null); ad.loadContent("application/dash+xml", content, resumePosition, video.duration.toDouble(), speed);
return listOf(videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: ""); return listOf(videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "");
} }
private fun castProxiedHls(video: IPlatformVideoDetails, sourceUrl: String, codec: String?, resumePosition: Double): List<String> { private fun castProxiedHls(video: IPlatformVideoDetails, sourceUrl: String, codec: String?, resumePosition: Double, speed: Double?): List<String> {
_castServer.removeAllHandlers("castProxiedHlsMaster") _castServer.removeAllHandlers("castProxiedHlsMaster")
val ad = activeDevice ?: return listOf(); val ad = activeDevice ?: return listOf();
@ -825,7 +839,7 @@ class StateCasting {
//ChromeCast is sometimes funky with resume position 0 //ChromeCast is sometimes funky with resume position 0
val hackfixResumePosition = if (ad is ChromecastCastingDevice && !video.isLive && resumePosition == 0.0) 0.1 else resumePosition; val hackfixResumePosition = if (ad is ChromecastCastingDevice && !video.isLive && resumePosition == 0.0) 0.1 else resumePosition;
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", "application/vnd.apple.mpegurl", hlsUrl, hackfixResumePosition, video.duration.toDouble(), null); ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", "application/vnd.apple.mpegurl", hlsUrl, hackfixResumePosition, video.duration.toDouble(), speed);
return listOf(hlsUrl); return listOf(hlsUrl);
} }
@ -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 ad = activeDevice ?: return listOf();
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}"; val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
val id = UUID.randomUUID(); val id = UUID.randomUUID();
@ -999,12 +1013,12 @@ class StateCasting {
).withTag("castHlsIndirectMaster") ).withTag("castHlsIndirectMaster")
Logger.i(TAG, "added new castHls handlers (hlsPath: $hlsPath)."); Logger.i(TAG, "added new castHls handlers (hlsPath: $hlsPath).");
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", "application/vnd.apple.mpegurl", hlsUrl, resumePosition, video.duration.toDouble(), null); ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", "application/vnd.apple.mpegurl", hlsUrl, resumePosition, video.duration.toDouble(), speed);
return listOf(hlsUrl, videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "", subtitlesUri.toString()); return listOf(hlsUrl, videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "", subtitlesUri.toString());
} }
private suspend fun castDashIndirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double) : List<String> { private suspend fun castDashIndirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
val ad = activeDevice ?: return listOf(); val ad = activeDevice ?: return listOf();
val proxyStreams = ad !is FCastCastingDevice; val proxyStreams = ad !is FCastCastingDevice;
@ -1074,7 +1088,7 @@ class StateCasting {
} }
Logger.i(TAG, "added new castDash handlers (dashPath: $dashPath, videoPath: $videoPath, audioPath: $audioPath)."); Logger.i(TAG, "added new castDash handlers (dashPath: $dashPath, videoPath: $videoPath, audioPath: $audioPath).");
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", "application/dash+xml", dashUrl, resumePosition, video.duration.toDouble(), null); ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", "application/dash+xml", dashUrl, resumePosition, video.duration.toDouble(), speed);
return listOf(dashUrl, videoUrl ?: "", audioUrl ?: "", subtitlesUrl ?: "", videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "", subtitlesUri.toString()); return listOf(dashUrl, videoUrl ?: "", audioUrl ?: "", subtitlesUrl ?: "", videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "", subtitlesUri.toString());
} }

View File

@ -67,7 +67,7 @@ class VideoDownload {
val videoEither: IPlatformVideo get() = videoDetails ?: video ?: throw IllegalStateException("Missing video?"); val videoEither: IPlatformVideo get() = videoDetails ?: video ?: throw IllegalStateException("Missing video?");
val id: PlatformID get() = videoEither.id val id: PlatformID get() = videoEither.id
val name: String get() = videoEither.name; 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 targetPixelCount: Long? = null;
var targetBitrate: Long? = null; var targetBitrate: Long? = null;

View File

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

View File

@ -1,5 +1,3 @@
@file:Suppress("DEPRECATION")
package com.futo.platformplayer.fragment.mainactivity.main package com.futo.platformplayer.fragment.mainactivity.main
import android.app.PictureInPictureParams import android.app.PictureInPictureParams
@ -32,6 +30,12 @@ import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.lifecycle.lifecycleScope 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.Glide
import com.bumptech.glide.request.target.CustomTarget import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition 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.Models
import com.futo.polycentric.core.Opinion import com.futo.polycentric.core.Opinion
import com.futo.polycentric.core.toURLInfoSystemLinkUrl 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 com.google.protobuf.ByteString
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -155,7 +154,6 @@ import java.time.OffsetDateTime
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.roundToLong import kotlin.math.roundToLong
class VideoDetailView : ConstraintLayout { class VideoDetailView : ConstraintLayout {
private val TAG = "VideoDetailView" private val TAG = "VideoDetailView"
@ -303,7 +301,7 @@ class VideoDetailView : ConstraintLayout {
Pair(0, 10) //around live, try every 10 seconds Pair(0, 10) //around live, try every 10 seconds
); );
@androidx.annotation.OptIn(UnstableApi::class)
constructor(context: Context, attrs : AttributeSet? = null) : super(context, attrs) { constructor(context: Context, attrs : AttributeSet? = null) : super(context, attrs) {
inflate(context, R.layout.fragview_video_detail, this); inflate(context, R.layout.fragview_video_detail, this);
@ -312,7 +310,7 @@ class VideoDetailView : ConstraintLayout {
_cast = findViewById(R.id.videodetail_cast); _cast = findViewById(R.id.videodetail_cast);
_player = findViewById(R.id.videodetail_player); _player = findViewById(R.id.videodetail_player);
_playerProgress = findViewById(R.id.videodetail_progress); _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); _title = findViewById(R.id.videodetail_title);
_subTitle = findViewById(R.id.videodetail_meta); _subTitle = findViewById(R.id.videodetail_meta);
_platform = findViewById(R.id.videodetail_platform); _platform = findViewById(R.id.videodetail_platform);
@ -514,6 +512,8 @@ class VideoDetailView : ConstraintLayout {
_player.onDatasourceError.subscribe(::onDataSourceError); _player.onDatasourceError.subscribe(::onDataSourceError);
_player.onNext.subscribe { nextVideo(true, true, true) }; _player.onNext.subscribe { nextVideo(true, true, true) };
_player.onPrevious.subscribe { prevVideo(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_play.setOnClickListener { handlePlay(); };
_minimize_controls_pause.setOnClickListener { handlePause(); }; _minimize_controls_pause.setOnClickListener { handlePause(); };
@ -576,7 +576,7 @@ class VideoDetailView : ConstraintLayout {
_playerProgress.player = _player.exoPlayer?.player; _playerProgress.player = _player.exoPlayer?.player;
_playerProgress.setProgressUpdateListener { position, _ -> _playerProgress.setProgressUpdateListener { position, _ ->
StatePlayer.instance.updateMediaSessionPlaybackState(_player.exoPlayer?.getPlaybackStateCompat() ?: PlaybackStateCompat.STATE_NONE, position); StatePlayer.instance.updateMediaSessionPlaybackState(_player.exoPlayer?.getPlaybackStateCompat() ?: PlaybackStateCompat.STATE_NONE, position);
} };
StatePlayer.instance.onQueueChanged.subscribe(this) { StatePlayer.instance.onQueueChanged.subscribe(this) {
if(!_destroyed) { if(!_destroyed) {
@ -1361,11 +1361,9 @@ class VideoDetailView : ConstraintLayout {
} }
} }
StatePlayer.instance.startOrUpdateMediaSession(context, video); StatePlayer.instance.startOrUpdateMediaSession(context, video);
StatePlayer.instance.setCurrentlyPlaying(video); StatePlayer.instance.setCurrentlyPlaying(video);
if(video.isLive && video.live != null) { if(video.isLive && video.live != null) {
loadLiveChat(video); loadLiveChat(video);
} }
@ -1472,7 +1470,7 @@ class VideoDetailView : ConstraintLayout {
_player.seekTo(resumePositionMs); _player.seekTo(resumePositionMs);
} }
else else
loadCurrentVideoCast(video, videoSource, audioSource, subtitleSource, resumePositionMs); loadCurrentVideoCast(video, videoSource, audioSource, subtitleSource, resumePositionMs, Settings.instance.playback.getDefaultPlaybackSpeed().toDouble());
_lastVideoSource = videoSource; _lastVideoSource = videoSource;
_lastAudioSource = audioSource; _lastAudioSource = audioSource;
@ -1486,17 +1484,17 @@ class VideoDetailView : ConstraintLayout {
UIDialogs.showGeneralErrorDialog(context, context.getString(R.string.failed_to_load_media), ex); UIDialogs.showGeneralErrorDialog(context, context.getString(R.string.failed_to_load_media), ex);
} }
} }
private fun loadCurrentVideoCast(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: ISubtitleSource?, resumePositionMs: Long) { private fun loadCurrentVideoCast(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: ISubtitleSource?, resumePositionMs: Long, speed: Double?) {
Logger.i(TAG, "loadCurrentVideoCast(video=$video, videoSource=$videoSource, audioSource=$audioSource, resumePositionMs=$resumePositionMs)") Logger.i(TAG, "loadCurrentVideoCast(video=$video, videoSource=$videoSource, audioSource=$audioSource, resumePositionMs=$resumePositionMs)")
if(StateCasting.instance.castIfAvailable(context.contentResolver, video, videoSource, audioSource, subtitleSource, resumePositionMs)) { if(StateCasting.instance.castIfAvailable(context.contentResolver, video, videoSource, audioSource, subtitleSource, resumePositionMs, speed)) {
_cast.setVideoDetails(video, resumePositionMs / 1000); _cast.setVideoDetails(video, resumePositionMs / 1000);
setCastEnabled(true); setCastEnabled(true);
} } else throw IllegalStateException("Disconnected cast during loading");
else throw IllegalStateException("Disconnected cast during loading");
} }
//Events //Events
@androidx.annotation.OptIn(UnstableApi::class)
private fun onSourceChanged(videoSource: IVideoSource?, audioSource: IAudioSource?, resume: Boolean){ private fun onSourceChanged(videoSource: IVideoSource?, audioSource: IAudioSource?, resume: Boolean){
Logger.i(TAG, "onSourceChanged(videoSource=$videoSource, audioSource=$audioSource, resume=$resume)") Logger.i(TAG, "onSourceChanged(videoSource=$videoSource, audioSource=$audioSource, resume=$resume)")
@ -1532,7 +1530,7 @@ class VideoDetailView : ConstraintLayout {
private var _didTriggerDatasourceError = false; private var _didTriggerDatasourceError = false;
private fun onDataSourceError(exception: Throwable) { private fun onDataSourceError(exception: Throwable) {
Logger.e(TAG, "onDataSourceError", exception); 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 val currentVideo = video
if(currentVideo == null || currentVideo !is IPluginSourced) if(currentVideo == null || currentVideo !is IPluginSourced)
return; return;
@ -1579,6 +1577,11 @@ class VideoDetailView : ConstraintLayout {
_overlay_quality_selector?.selectOption("video", _lastVideoSource); _overlay_quality_selector?.selectOption("video", _lastVideoSource);
_overlay_quality_selector?.selectOption("audio", _lastAudioSource); _overlay_quality_selector?.selectOption("audio", _lastAudioSource);
_overlay_quality_selector?.selectOption("subtitles", _lastSubtitleSource); _overlay_quality_selector?.selectOption("subtitles", _lastSubtitleSource);
val currentPlaybackRate = (if (_isCasting) StateCasting.instance.activeDevice?.speed else _player.getPlaybackRate()) ?: 1.0
_overlay_quality_selector?.groupItems?.firstOrNull { it is SlideUpMenuButtonList && it.id == "playback_rate" }?.let {
(it as SlideUpMenuButtonList).setSelected(currentPlaybackRate.toString())
};
_overlay_quality_selector?.show(); _overlay_quality_selector?.show();
_slideUpOverlay = _overlay_quality_selector; _slideUpOverlay = _overlay_quality_selector;
} }
@ -1611,6 +1614,7 @@ class VideoDetailView : ConstraintLayout {
val v = video ?: return; val v = video ?: return;
updateQualitySourcesOverlay(v, videoLocal, liveStreamVideoFormats, liveStreamAudioFormats); 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) { private fun updateQualitySourcesOverlay(videoDetails: IPlatformVideoDetails?, videoLocal: VideoLocal? = null, liveStreamVideoFormats: List<Format>? = null, liveStreamAudioFormats: List<Format>? = null) {
Logger.i(TAG, "updateQualitySourcesOverlay"); Logger.i(TAG, "updateQualitySourcesOverlay");
@ -1658,18 +1662,26 @@ class VideoDetailView : ConstraintLayout {
?.filter { it.container == bestAudioContainer } ?.filter { it.container == bestAudioContainer }
?.toList() ?: listOf(); ?.toList() ?: listOf();
val canSetSpeed = !_isCasting || StateCasting.instance.activeDevice?.canSetSpeed == true
val currentPlaybackRate = if (_isCasting) StateCasting.instance.activeDevice?.speed else _player.getPlaybackRate()
_overlay_quality_selector = SlideUpMenuOverlay(this.context, _overlay_quality_container, context.getString( _overlay_quality_selector = SlideUpMenuOverlay(this.context, _overlay_quality_container, context.getString(
R.string.quality), null, true, R.string.quality), null, true,
if (!_isCasting) SlideUpMenuTitle(this.context).apply { setTitle(context.getString(R.string.playback_rate)) } else null, if (canSetSpeed) SlideUpMenuTitle(this.context).apply { setTitle(context.getString(R.string.playback_rate)) } else null,
if (!_isCasting) SlideUpMenuButtonList(this.context).apply { if (canSetSpeed) SlideUpMenuButtonList(this.context, null, "playback_rate").apply {
setButtons(listOf("0.25", "0.5", "0.75", "1.0", "1.25", "1.5", "1.75", "2.0", "2.25"), _player.getPlaybackRate().toString()); setButtons(listOf("0.25", "0.5", "0.75", "1.0", "1.25", "1.5", "1.75", "2.0", "2.25"), currentPlaybackRate!!.toString());
onClick.subscribe { v -> onClick.subscribe { v ->
if (_isCasting) { if (_isCasting) {
return@subscribe; val ad = StateCasting.instance.activeDevice ?: return@subscribe
} if (!ad.canSetSpeed) {
return@subscribe
}
_player.setPlaybackRate(v.toFloat()); ad.changeSpeed(v.toDouble())
setSelected(v); setSelected(v);
} else {
_player.setPlaybackRate(v.toFloat());
setSelected(v);
}
}; };
} else null, } else null,
@ -1730,7 +1742,7 @@ class VideoDetailView : ConstraintLayout {
{ handleSelectAudioTrack(it) }); { handleSelectAudioTrack(it) });
}.toList().toTypedArray()) }.toList().toTypedArray())
else null, else null,
if(video?.subtitles?.isNotEmpty() ?: false && video != null) if(video?.subtitles?.isNotEmpty() == true)
SlideUpMenuGroup(this.context, context.getString(R.string.subtitles), "subtitles", SlideUpMenuGroup(this.context, context.getString(R.string.subtitles), "subtitles",
*video.subtitles *video.subtitles
.map { .map {
@ -1831,7 +1843,7 @@ class VideoDetailView : ConstraintLayout {
val d = StateCasting.instance.activeDevice; val d = StateCasting.instance.activeDevice;
if (d != null && d.connectionState == CastConnectionState.CONNECTED) if (d != null && d.connectionState == CastConnectionState.CONNECTED)
StateCasting.instance.castIfAvailable(context.contentResolver, video, videoSource, _lastAudioSource, _lastSubtitleSource, (d.expectedCurrentTime * 1000.0).toLong()); StateCasting.instance.castIfAvailable(context.contentResolver, video, videoSource, _lastAudioSource, _lastSubtitleSource, (d.expectedCurrentTime * 1000.0).toLong(), d.speed);
else if(!_player.swapSources(videoSource, _lastAudioSource, true, true, true)) else if(!_player.swapSources(videoSource, _lastAudioSource, true, true, true))
_player.hideControls(false); //TODO: Disable player? _player.hideControls(false); //TODO: Disable player?
@ -1846,7 +1858,7 @@ class VideoDetailView : ConstraintLayout {
val d = StateCasting.instance.activeDevice; val d = StateCasting.instance.activeDevice;
if (d != null && d.connectionState == CastConnectionState.CONNECTED) if (d != null && d.connectionState == CastConnectionState.CONNECTED)
StateCasting.instance.castIfAvailable(context.contentResolver, video, _lastVideoSource, audioSource, _lastSubtitleSource, (d.expectedCurrentTime * 1000.0).toLong()); StateCasting.instance.castIfAvailable(context.contentResolver, video, _lastVideoSource, audioSource, _lastSubtitleSource, (d.expectedCurrentTime * 1000.0).toLong(), d.speed);
else(!_player.swapSources(_lastVideoSource, audioSource, true, true, true)) else(!_player.swapSources(_lastVideoSource, audioSource, true, true, true))
_player.hideControls(false); //TODO: Disable player? _player.hideControls(false); //TODO: Disable player?
@ -1862,7 +1874,7 @@ class VideoDetailView : ConstraintLayout {
val d = StateCasting.instance.activeDevice; val d = StateCasting.instance.activeDevice;
if (d != null && d.connectionState == CastConnectionState.CONNECTED) if (d != null && d.connectionState == CastConnectionState.CONNECTED)
StateCasting.instance.castIfAvailable(context.contentResolver, video, _lastVideoSource, _lastAudioSource, toSet, (d.expectedCurrentTime * 1000.0).toLong()); StateCasting.instance.castIfAvailable(context.contentResolver, video, _lastVideoSource, _lastAudioSource, toSet, (d.expectedCurrentTime * 1000.0).toLong(), d.speed);
else else
_player.swapSubtitles(fragment.lifecycleScope, toSet); _player.swapSubtitles(fragment.lifecycleScope, toSet);

View File

@ -1,8 +1,14 @@
@file:Suppress("DEPRECATION")
package com.futo.platformplayer.helpers package com.futo.platformplayer.helpers
import android.net.Uri 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.IVideoSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource 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.api.media.platforms.js.models.sources.JSVideoUrlRangeSource
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.others.Language 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 import kotlin.math.abs
class VideoHelper { class VideoHelper {
@ -123,7 +124,7 @@ class VideoHelper {
return bestSource; return bestSource;
} }
@Suppress("DEPRECATION") @OptIn(UnstableApi::class)
fun convertItagSourceToChunkedDashSource(videoSource: JSVideoUrlRangeSource) : MediaSource { fun convertItagSourceToChunkedDashSource(videoSource: JSVideoUrlRangeSource) : MediaSource {
val urlToUse = videoSource.getVideoUrl(); val urlToUse = videoSource.getVideoUrl();
val manifestConfig = ProgressiveDashManifestCreator.fromVideoProgressiveStreamingUrl(urlToUse, val manifestConfig = ProgressiveDashManifestCreator.fromVideoProgressiveStreamingUrl(urlToUse,
@ -142,14 +143,25 @@ class VideoHelper {
); );
val manifest = DashManifestParser().parse(Uri.parse(""), manifestConfig.byteInputStream()); val manifest = DashManifestParser().parse(Uri.parse(""), manifestConfig.byteInputStream());
return DashMediaSource.Factory(ResolvingDataSource.Factory(videoSource.getHttpDataSourceFactory(), ResolvingDataSource.Resolver { dataSpec -> 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); Logger.v("PLAYBACK", "Video REQ Range [" + dataSpec.position + "-" + (dataSpec.position + dataSpec.length) + "](" + dataSpec.length + ")", null);
return@Resolver dataSpec; return@Resolver dataSpec;
})).createMediaSource(manifest, MediaItem.Builder().setUri(Uri.parse(videoSource.getVideoUrl())).build()) })).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 { fun convertItagSourceToChunkedDashSource(audioSource: JSAudioUrlRangeSource) : MediaSource {
val manifestConfig = ProgressiveDashManifestCreator.fromAudioProgressiveStreamingUrl(audioSource.getAudioUrl(), val manifestConfig = ProgressiveDashManifestCreator.fromAudioProgressiveStreamingUrl(audioSource.getAudioUrl(),
audioSource.duration?.times(1000) ?: 0, audioSource.duration?.times(1000) ?: 0,

View File

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

View File

@ -55,6 +55,7 @@ class MediaPlaybackService : Service() {
private var _hasFocus: Boolean = false; private var _hasFocus: Boolean = false;
private var _focusRequest: AudioFocusRequest? = null; private var _focusRequest: AudioFocusRequest? = null;
private var _audioFocusLossTime_ms: Long? = null private var _audioFocusLossTime_ms: Long? = null
private var _playbackState = PlaybackStateCompat.STATE_NONE;
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Logger.v(TAG, "onStartCommand"); Logger.v(TAG, "onStartCommand");
@ -250,13 +251,13 @@ class MediaPlaybackService : Service() {
.setSilent(true) .setSilent(true)
.setContentIntent(PendingIntent.getActivity(this, 5, bringUpIntent, PendingIntent.FLAG_IMMUTABLE)) .setContentIntent(PendingIntent.getActivity(this, 5, bringUpIntent, PendingIntent.FLAG_IMMUTABLE))
.setStyle(if(hasQueue) .setStyle(if(hasQueue)
androidx.media.app.NotificationCompat.MediaStyle() androidx.media.app.NotificationCompat.MediaStyle()
.setMediaSession(session.sessionToken) .setMediaSession(session.sessionToken)
.setShowActionsInCompactView(0, 1, 2) .setShowActionsInCompactView(0, 1, 2)
else else
androidx.media.app.NotificationCompat.MediaStyle() androidx.media.app.NotificationCompat.MediaStyle()
.setMediaSession(session.sessionToken) .setMediaSession(session.sessionToken)
.setShowActionsInCompactView(0)) .setShowActionsInCompactView(0))
.setDeleteIntent(deleteIntent) .setDeleteIntent(deleteIntent)
.setChannelId(channel.id) .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}"); 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) { 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); startForeground(MEDIA_NOTIF_ID, notif, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
} else { } else {
// For API 28 and below
startForeground(MEDIA_NOTIF_ID, notif); startForeground(MEDIA_NOTIF_ID, notif);
} }
@ -319,19 +318,21 @@ class MediaPlaybackService : Service() {
fun updateMediaSessionPlaybackState(state: Int, pos: Long) { fun updateMediaSessionPlaybackState(state: Int, pos: Long) {
_mediaSession?.setPlaybackState( _mediaSession?.setPlaybackState(
PlaybackStateCompat.Builder() PlaybackStateCompat.Builder()
.setActions( .setActions(
PlaybackStateCompat.ACTION_SEEK_TO or PlaybackStateCompat.ACTION_SEEK_TO or
PlaybackStateCompat.ACTION_PLAY or PlaybackStateCompat.ACTION_PLAY or
PlaybackStateCompat.ACTION_PAUSE or PlaybackStateCompat.ACTION_PAUSE or
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or
PlaybackStateCompat.ACTION_SKIP_TO_NEXT or PlaybackStateCompat.ACTION_SKIP_TO_NEXT or
PlaybackStateCompat.ACTION_PLAY_PAUSE PlaybackStateCompat.ACTION_PLAY_PAUSE
) )
.setState(state, pos, 1f, SystemClock.elapsedRealtime()) .setState(state, pos, 1f, SystemClock.elapsedRealtime())
.build()); .build());
if(_focusRequest == null) if(_focusRequest == null)
setAudioFocus(); setAudioFocus();
_playbackState = state;
} }
//TODO: (TBD) This code probably more fitting inside FutoVideoPlayer, as this service is generally only used for global events //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 -> { AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
MediaControlReceiver.onPauseReceived.emit(); 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"); Log.i(TAG, "Audio focus transient loss");
} }
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> { AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
Log.i(TAG, "Audio focus transient loss, can duck"); Log.i(TAG, "Audio focus transient loss, can duck");
} }
AudioManager.AUDIOFOCUS_LOSS -> { 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; _hasFocus = false;
MediaControlReceiver.onPauseReceived.emit(); MediaControlReceiver.onPauseReceived.emit();
Log.i(TAG, "Audio focus lost"); Log.i(TAG, "Audio focus lost");

View File

@ -1,8 +1,12 @@
@file:Suppress("DEPRECATION")
package com.futo.platformplayer.states package com.futo.platformplayer.states
import android.content.Context 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.R
import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails 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.models.Playlist
import com.futo.platformplayer.services.MediaPlaybackService import com.futo.platformplayer.services.MediaPlaybackService
import com.futo.platformplayer.video.PlayerManager 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 import kotlin.random.Random
/*** /***
@ -557,21 +557,23 @@ class StatePlayer {
} }
//Player Initialization //Player Initialization
fun getPlayerOrCreate(context : Context) : PlayerManager { fun getPlayerOrCreate(context: Context) : PlayerManager {
if(_exoplayer == null) { if(_exoplayer == null) {
val player = createExoPlayer(context); val player = createExoPlayer(context);
_exoplayer = PlayerManager(player); _exoplayer = PlayerManager(player);
} }
return _exoplayer!!; return _exoplayer!!;
} }
fun getThumbnailPlayerOrCreate(context : Context) : PlayerManager { fun getThumbnailPlayerOrCreate(context: Context) : PlayerManager {
if(_thumbnailExoPlayer == null) { if(_thumbnailExoPlayer == null) {
val player = createExoPlayer(context); val player = createExoPlayer(context);
_thumbnailExoPlayer = PlayerManager(player); _thumbnailExoPlayer = PlayerManager(player);
} }
return _thumbnailExoPlayer!!; return _thumbnailExoPlayer!!;
} }
private fun createExoPlayer(context : Context) : ExoPlayer {
@OptIn(UnstableApi::class)
private fun createExoPlayer(context : Context): ExoPlayer {
return ExoPlayer.Builder(context) return ExoPlayer.Builder(context)
.setLoadControl( .setLoadControl(
DefaultLoadControl.Builder() DefaultLoadControl.Builder()
@ -589,7 +591,6 @@ class StatePlayer {
.build(); .build();
} }
fun dispose(){ fun dispose(){
val player = _exoplayer; val player = _exoplayer;
val thumbPlayer = _thumbnailExoPlayer; val thumbPlayer = _thumbnailExoPlayer;

View File

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

View File

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

View File

@ -1,5 +1,3 @@
@file:Suppress("DEPRECATION")
package com.futo.platformplayer.views.casting package com.futo.platformplayer.views.casting
import android.content.Context import android.content.Context
@ -13,7 +11,11 @@ import android.widget.FrameLayout
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.annotation.OptIn
import androidx.constraintlayout.widget.ConstraintLayout 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.bumptech.glide.Glide
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails 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.states.StatePlayer
import com.futo.platformplayer.toHumanTime import com.futo.platformplayer.toHumanTime
import com.futo.platformplayer.views.behavior.GestureControlView 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.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -39,6 +38,8 @@ class CastView : ConstraintLayout {
private val _buttonSettings: ImageButton; private val _buttonSettings: ImageButton;
private val _buttonLoop: ImageButton; private val _buttonLoop: ImageButton;
private val _buttonPlay: ImageButton; private val _buttonPlay: ImageButton;
private val _buttonPrevious: ImageButton;
private val _buttonNext: ImageButton;
private val _buttonPause: ImageButton; private val _buttonPause: ImageButton;
private val _buttonCast: CastButton; private val _buttonCast: CastButton;
private val _textPosition: TextView; private val _textPosition: TextView;
@ -53,7 +54,10 @@ class CastView : ConstraintLayout {
val onMinimizeClick = Event0(); val onMinimizeClick = Event0();
val onSettingsClick = Event0(); val onSettingsClick = Event0();
val onPrevious = Event0();
val onNext = Event0();
@OptIn(UnstableApi::class)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
LayoutInflater.from(context).inflate(R.layout.view_cast, this, true); LayoutInflater.from(context).inflate(R.layout.view_cast, this, true);
@ -62,6 +66,8 @@ class CastView : ConstraintLayout {
_buttonSettings = findViewById(R.id.button_settings); _buttonSettings = findViewById(R.id.button_settings);
_buttonLoop = findViewById(R.id.button_loop); _buttonLoop = findViewById(R.id.button_loop);
_buttonPlay = findViewById(R.id.button_play); _buttonPlay = findViewById(R.id.button_play);
_buttonPrevious = findViewById(R.id.button_previous);
_buttonNext = findViewById(R.id.button_next);
_buttonPause = findViewById(R.id.button_pause); _buttonPause = findViewById(R.id.button_pause);
_buttonCast = findViewById(R.id.button_cast); _buttonCast = findViewById(R.id.button_cast);
_textPosition = findViewById(R.id.text_position); _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); _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) { override fun onScrubStart(timeBar: TimeBar, position: Long) {
StateCasting.instance.videoSeekTo(position.toDouble()); StateCasting.instance.videoSeekTo(position.toDouble());
} }
@ -105,6 +111,29 @@ class CastView : ConstraintLayout {
if (!isInEditMode) { if (!isInEditMode) {
setIsPlaying(false); 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() { fun stopTimeJob() {
@ -150,7 +179,6 @@ class CastView : ConstraintLayout {
} }
val position = StateCasting.instance.activeDevice?.expectedCurrentTime?.times(1000.0)?.toLong(); val position = StateCasting.instance.activeDevice?.expectedCurrentTime?.times(1000.0)?.toLong();
if(StatePlayer.instance.hasMediaSession()) { if(StatePlayer.instance.hasMediaSession()) {
StatePlayer.instance.updateMediaSession(null); StatePlayer.instance.updateMediaSession(null);
StatePlayer.instance.updateMediaSessionPlaybackState(getPlaybackStateCompat(), (position ?: 0)); StatePlayer.instance.updateMediaSessionPlaybackState(getPlaybackStateCompat(), (position ?: 0));
@ -183,6 +211,7 @@ class CastView : ConstraintLayout {
} }
} }
@OptIn(UnstableApi::class)
fun setVideoDetails(video: IPlatformVideoDetails, position: Long) { fun setVideoDetails(video: IPlatformVideoDetails, position: Long) {
Glide.with(_thumbnail) Glide.with(_thumbnail)
.load(video.thumbnails.getHQThumbnail()) .load(video.thumbnails.getHQThumbnail())
@ -194,6 +223,7 @@ class CastView : ConstraintLayout {
_timeBar.setDuration(video.duration); _timeBar.setDuration(video.duration);
} }
@OptIn(UnstableApi::class)
fun setTime(ms: Long) { fun setTime(ms: Long) {
_textPosition.text = ms.toHumanTime(true); _textPosition.text = ms.toHumanTime(true);
_timeBar.setPosition(ms / 1000); _timeBar.setPosition(ms / 1000);

View File

@ -18,8 +18,11 @@ class SlideUpMenuButtonList : LinearLayout {
val onClick = Event1<String>(); val onClick = Event1<String>();
val buttons: HashMap<String, LinearLayout> = hashMapOf(); val buttons: HashMap<String, LinearLayout> = hashMapOf();
var _activeText: String? = null; var _activeText: String? = null;
val id: String?
constructor(context: Context, attrs: AttributeSet? = null, id: String? = null): super(context, attrs) {
this.id = id
constructor(context: Context, attrs: AttributeSet? = null): super(context, attrs) {
LayoutInflater.from(context).inflate(R.layout.overlay_slide_up_menu_button_list, this, true); LayoutInflater.from(context).inflate(R.layout.overlay_slide_up_menu_button_list, this, true);
_root = findViewById(R.id.root); _root = findViewById(R.id.root);

View File

@ -26,7 +26,7 @@ class SlideUpMenuOverlay : RelativeLayout {
private lateinit var _viewContainer: LinearLayout; private lateinit var _viewContainer: LinearLayout;
private var _animated: Boolean = true; private var _animated: Boolean = true;
private var _groupItems: List<View>; var groupItems: List<View>;
var isVisible = false var isVisible = false
private set; private set;
@ -36,7 +36,7 @@ class SlideUpMenuOverlay : RelativeLayout {
constructor(context: Context, attrs: AttributeSet? = null): super(context, attrs) { constructor(context: Context, attrs: AttributeSet? = null): super(context, attrs) {
init(false, null); init(false, null);
_groupItems = listOf(); groupItems = listOf();
} }
constructor(context: Context, parent: ViewGroup, titleText: String, okText: String?, animated: Boolean, items: List<View>, hideButtons: Boolean = false): super(context){ constructor(context: Context, parent: ViewGroup, titleText: String, okText: String?, animated: Boolean, items: List<View>, hideButtons: Boolean = false): super(context){
@ -47,7 +47,7 @@ class SlideUpMenuOverlay : RelativeLayout {
_container!!.addView(this); _container!!.addView(this);
} }
_textTitle.text = titleText; _textTitle.text = titleText;
_groupItems = items; groupItems = items;
if(hideButtons) { if(hideButtons) {
_textCancel.visibility = GONE; _textCancel.visibility = GONE;
@ -74,7 +74,7 @@ class SlideUpMenuOverlay : RelativeLayout {
item.setParentClickListener { hide() }; item.setParentClickListener { hide() };
} }
_groupItems = items; groupItems = items;
} }
private fun init(animated: Boolean, okText: String?){ private fun init(animated: Boolean, okText: String?){
@ -116,12 +116,12 @@ class SlideUpMenuOverlay : RelativeLayout {
fun selectOption(groupTag: Any?, itemTag: Any?, multiSelect: Boolean = false, toggle: Boolean = false): Boolean { fun selectOption(groupTag: Any?, itemTag: Any?, multiSelect: Boolean = false, toggle: Boolean = false): Boolean {
var didSelect = false; var didSelect = false;
for(view in _groupItems) { for(view in groupItems) {
if(view is SlideUpMenuGroup && view.groupTag == groupTag) if(view is SlideUpMenuGroup && view.groupTag == groupTag)
didSelect = didSelect || view.selectItem(itemTag); didSelect = didSelect || view.selectItem(itemTag);
} }
if(groupTag == null) if(groupTag == null)
for(item in _groupItems) for(item in groupItems)
if(item is SlideUpMenuItem) { if(item is SlideUpMenuItem) {
if(multiSelect) { if(multiSelect) {
if(item.itemTag == itemTag) if(item.itemTag == itemTag)

View File

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

View File

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

View File

@ -1,11 +1,27 @@
@file:Suppress("DEPRECATION")
package com.futo.platformplayer.views.video package com.futo.platformplayer.views.video
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.util.AttributeSet import android.util.AttributeSet
import android.widget.RelativeLayout 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.Settings
import com.futo.platformplayer.api.media.models.chapters.IChapter import com.futo.platformplayer.api.media.models.chapters.IChapter
import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor
@ -28,22 +44,6 @@ import com.futo.platformplayer.helpers.VideoHelper
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.video.PlayerManager 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.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -251,6 +251,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
_targetTrackAudioBitrate = bitrate; _targetTrackAudioBitrate = bitrate;
updateTrackSelector(); updateTrackSelector();
} }
@OptIn(UnstableApi::class)
private fun updateTrackSelector() { private fun updateTrackSelector() {
var builder = DefaultTrackSelector.Parameters.Builder(context); var builder = DefaultTrackSelector.Parameters.Builder(context);
if(_targetTrackVideoHeight > 0) { if(_targetTrackVideoHeight > 0) {
@ -298,6 +299,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
return loadSelectedSources(play, resume); return loadSelectedSources(play, resume);
} }
@OptIn(UnstableApi::class)
fun swapSubtitles(scope: CoroutineScope, subtitles: ISubtitleSource?) { fun swapSubtitles(scope: CoroutineScope, subtitles: ISubtitleSource?) {
if(subtitles == null) if(subtitles == null)
clearSubtitles(); clearSubtitles();
@ -369,6 +371,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
} }
//Video loads //Video loads
@OptIn(UnstableApi::class)
private fun swapVideoSourceLocal(videoSource: LocalVideoSource) { private fun swapVideoSourceLocal(videoSource: LocalVideoSource) {
Logger.i(TAG, "Loading VideoSource [Local]"); Logger.i(TAG, "Loading VideoSource [Local]");
val file = File(videoSource.filePath); val file = File(videoSource.filePath);
@ -377,14 +380,13 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
_lastVideoMediaSource = ProgressiveMediaSource.Factory(DefaultDataSource.Factory(context)) _lastVideoMediaSource = ProgressiveMediaSource.Factory(DefaultDataSource.Factory(context))
.createMediaSource(MediaItem.fromUri(Uri.fromFile(file))); .createMediaSource(MediaItem.fromUri(Uri.fromFile(file)));
} }
@OptIn(UnstableApi::class)
private fun swapVideoSourceUrlRange(videoSource: JSVideoUrlRangeSource) { private fun swapVideoSourceUrlRange(videoSource: JSVideoUrlRangeSource) {
Logger.i(TAG, "Loading JSVideoUrlRangeSource"); Logger.i(TAG, "Loading JSVideoUrlRangeSource");
if(videoSource.hasItag) { if(videoSource.hasItag) {
//Temporary workaround for Youtube //Temporary workaround for Youtube
try { try {
_lastVideoMediaSource = VideoHelper.convertItagSourceToChunkedDashSource(videoSource); _lastVideoMediaSource = VideoHelper.convertItagSourceToChunkedDashSource(videoSource);
if(_lastVideoMediaSource == null)
throw java.lang.IllegalStateException("Dash manifest workaround failed");
return; return;
} }
//If it fails to create the dash workaround, fallback to standard progressive //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..."); else throw IllegalArgumentException("source without itag data...");
} }
@OptIn(UnstableApi::class)
private fun swapVideoSourceUrl(videoSource: IVideoUrlSource) { private fun swapVideoSourceUrl(videoSource: IVideoUrlSource) {
Logger.i(TAG, "Loading VideoSource [Url]"); Logger.i(TAG, "Loading VideoSource [Url]");
_lastVideoMediaSource = ProgressiveMediaSource.Factory(DefaultHttpDataSource.Factory() _lastVideoMediaSource = ProgressiveMediaSource.Factory(DefaultHttpDataSource.Factory()
.setUserAgent(DEFAULT_USER_AGENT)) .setUserAgent(DEFAULT_USER_AGENT))
.createMediaSource(MediaItem.fromUri(videoSource.getVideoUrl())); .createMediaSource(MediaItem.fromUri(videoSource.getVideoUrl()));
} }
@OptIn(UnstableApi::class)
private fun swapVideoSourceDash(videoSource: IDashManifestSource) { private fun swapVideoSourceDash(videoSource: IDashManifestSource) {
Logger.i(TAG, "Loading VideoSource [Dash]"); Logger.i(TAG, "Loading VideoSource [Dash]");
_lastVideoMediaSource = DashMediaSource.Factory(DefaultHttpDataSource.Factory() _lastVideoMediaSource = DashMediaSource.Factory(DefaultHttpDataSource.Factory()
.setUserAgent(DEFAULT_USER_AGENT)) .setUserAgent(DEFAULT_USER_AGENT))
.createMediaSource(MediaItem.fromUri(videoSource.url)) .createMediaSource(MediaItem.fromUri(videoSource.url))
} }
@OptIn(UnstableApi::class)
private fun swapVideoSourceHLS(videoSource: IHLSManifestSource) { private fun swapVideoSourceHLS(videoSource: IHLSManifestSource) {
Logger.i(TAG, "Loading VideoSource [HLS]"); Logger.i(TAG, "Loading VideoSource [HLS]");
_lastVideoMediaSource = HlsMediaSource.Factory(DefaultHttpDataSource.Factory() _lastVideoMediaSource = HlsMediaSource.Factory(DefaultHttpDataSource.Factory()
@ -416,7 +421,9 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
.createMediaSource(MediaItem.fromUri(videoSource.url)); .createMediaSource(MediaItem.fromUri(videoSource.url));
} }
//Audio loads //Audio loads
@OptIn(UnstableApi::class)
private fun swapAudioSourceLocal(audioSource: LocalAudioSource) { private fun swapAudioSourceLocal(audioSource: LocalAudioSource) {
Logger.i(TAG, "Loading AudioSource [Local]"); Logger.i(TAG, "Loading AudioSource [Local]");
val file = File(audioSource.filePath); val file = File(audioSource.filePath);
@ -425,6 +432,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
_lastAudioMediaSource = ProgressiveMediaSource.Factory(DefaultDataSource.Factory(context)) _lastAudioMediaSource = ProgressiveMediaSource.Factory(DefaultDataSource.Factory(context))
.createMediaSource(MediaItem.fromUri(Uri.fromFile(file))); .createMediaSource(MediaItem.fromUri(Uri.fromFile(file)));
} }
@OptIn(UnstableApi::class)
private fun swapAudioSourceUrlRange(audioSource: JSAudioUrlRangeSource) { private fun swapAudioSourceUrlRange(audioSource: JSAudioUrlRangeSource) {
Logger.i(TAG, "Loading JSAudioUrlRangeSource"); Logger.i(TAG, "Loading JSAudioUrlRangeSource");
if(audioSource.hasItag) { if(audioSource.hasItag) {
@ -444,12 +452,14 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
} }
else throw IllegalArgumentException("source without itag data...") else throw IllegalArgumentException("source without itag data...")
} }
@OptIn(UnstableApi::class)
private fun swapAudioSourceUrl(audioSource: IAudioUrlSource) { private fun swapAudioSourceUrl(audioSource: IAudioUrlSource) {
Logger.i(TAG, "Loading AudioSource [Url]"); Logger.i(TAG, "Loading AudioSource [Url]");
_lastAudioMediaSource = ProgressiveMediaSource.Factory(DefaultHttpDataSource.Factory() _lastAudioMediaSource = ProgressiveMediaSource.Factory(DefaultHttpDataSource.Factory()
.setUserAgent(DEFAULT_USER_AGENT)) .setUserAgent(DEFAULT_USER_AGENT))
.createMediaSource(MediaItem.fromUri(audioSource.getAudioUrl())); .createMediaSource(MediaItem.fromUri(audioSource.getAudioUrl()));
} }
@OptIn(UnstableApi::class)
private fun swapAudioSourceHLS(audioSource: IHLSManifestAudioSource) { private fun swapAudioSourceHLS(audioSource: IHLSManifestAudioSource) {
Logger.i(TAG, "Loading AudioSource [HLS]"); Logger.i(TAG, "Loading AudioSource [HLS]");
_lastAudioMediaSource = HlsMediaSource.Factory(DefaultHttpDataSource.Factory() _lastAudioMediaSource = HlsMediaSource.Factory(DefaultHttpDataSource.Factory()
@ -479,6 +489,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
return VideoHelper.selectBestAudioSource(video.video, PREFERED_AUDIO_CONTAINERS, preferredLanguage); return VideoHelper.selectBestAudioSource(video.video, PREFERED_AUDIO_CONTAINERS, preferredLanguage);
} }
@OptIn(UnstableApi::class)
private fun loadSelectedSources(play: Boolean, resume: Boolean): Boolean { private fun loadSelectedSources(play: Boolean, resume: Boolean): Boolean {
val sourceVideo = if(!isAudioMode || _lastAudioMediaSource == null) _lastVideoMediaSource else null; val sourceVideo = if(!isAudioMode || _lastAudioMediaSource == null) _lastVideoMediaSource else null;
val sourceAudio = _lastAudioMediaSource; val sourceAudio = _lastAudioMediaSource;
@ -506,11 +517,9 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
return true; return true;
} }
@OptIn(UnstableApi::class)
private fun reloadMediaSource(play: Boolean = false, resume: Boolean = true) { private fun reloadMediaSource(play: Boolean = false, resume: Boolean = true) {
val player = exoPlayer val player = exoPlayer ?: return
if (player == null)
return;
val positionBefore = player.player.currentPosition; val positionBefore = player.player.currentPosition;
if(_mediaSource != null) { if(_mediaSource != null) {
player.player.setMediaSource(_mediaSource!!); 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_READ_POSITION_OUT_OF_RANGE,
PlaybackException.ERROR_CODE_IO_UNSPECIFIED -> { PlaybackException.ERROR_CODE_IO_UNSPECIFIED -> {
_shouldPlaybackRestartOnConnectivity = true; _shouldPlaybackRestartOnConnectivity = true;
_connectivityLossTime_ms = System.currentTimeMillis() if (playing) {
_connectivityLossTime_ms = System.currentTimeMillis()
}
Logger.i(TAG, "IO error, set _shouldPlaybackRestartOnConnectivity=true _connectivityLossTime_ms=$_connectivityLossTime_ms"); 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; package com.futo.platformplayer.views.video.datasources;
import static com.google.android.exoplayer2.upstream.HttpUtil.buildRangeRequestHeader; import static androidx.media3.common.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import static androidx.media3.common.util.Util.castNonNull;
import static com.google.android.exoplayer2.util.Util.castNonNull; import static androidx.media3.datasource.HttpUtil.buildRangeRequestHeader;
import static java.lang.Math.min; import static java.lang.Math.min;
import android.net.Uri; import android.net.Uri;
import android.util.Log;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestModifier; import com.futo.platformplayer.api.media.platforms.js.models.JSRequestModifier;
import com.google.android.exoplayer2.C; import androidx.media3.common.C;
import com.google.android.exoplayer2.PlaybackException; import androidx.media3.common.PlaybackException;
import com.google.android.exoplayer2.upstream.BaseDataSource; import androidx.media3.common.util.UnstableApi;
import com.google.android.exoplayer2.upstream.DataSourceException; import androidx.media3.common.util.Util;
import com.google.android.exoplayer2.upstream.DataSpec; import androidx.media3.datasource.BaseDataSource;
import com.google.android.exoplayer2.upstream.DataSpec.HttpMethod; import androidx.media3.datasource.DataSourceException;
import com.google.android.exoplayer2.upstream.HttpDataSource; import androidx.media3.datasource.DataSpec;
import com.google.android.exoplayer2.upstream.HttpUtil; import androidx.media3.datasource.HttpDataSource;
import com.google.android.exoplayer2.upstream.TransferListener; import androidx.media3.datasource.HttpUtil;
import com.google.android.exoplayer2.util.Log; import androidx.media3.datasource.TransferListener;
import com.google.android.exoplayer2.util.Util;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.collect.ForwardingMap; import com.google.common.collect.ForwardingMap;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
@ -45,6 +47,7 @@ import java.util.zip.GZIPInputStream;
* Based on the default ExoPlayer DefaultHttpDataSource * Based on the default ExoPlayer DefaultHttpDataSource
*/ */
@UnstableApi
public class JSHttpDataSource extends BaseDataSource implements HttpDataSource { public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
public static final class Factory implements HttpDataSource.Factory { 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 * Sets a content type {@link Predicate}. If a content type is rejected by the predicate then a
* {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link * {@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}. * <p>The default is {@code null}.
* *
@ -160,7 +163,7 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
* *
* <p>The default is {@code null}. * <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. * @param transferListener The listener that will be used.
* @return This factory. * @return This factory.
@ -367,12 +370,12 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
if (dataSpec.length != C.LENGTH_UNSET) { if (dataSpec.length != C.LENGTH_UNSET) {
bytesToRead = dataSpec.length; bytesToRead = dataSpec.length;
} else { } else {
long contentLength = long contentLength = HttpUtil.getContentLength(
HttpUtil.getContentLength( connection.getHeaderField(HttpHeaders.CONTENT_LENGTH),
connection.getHeaderField(HttpHeaders.CONTENT_LENGTH), connection.getHeaderField(HttpHeaders.CONTENT_RANGE)
connection.getHeaderField(HttpHeaders.CONTENT_RANGE)); );
bytesToRead =
contentLength != C.LENGTH_UNSET ? (contentLength - bytesToSkip) : C.LENGTH_UNSET; bytesToRead = contentLength != C.LENGTH_UNSET ? (contentLength - bytesToSkip) : C.LENGTH_UNSET;
} }
} else { } else {
// Gzip is enabled. If the server opts to use gzip then the content length in the response // 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. */ /** Establishes a connection, following redirects to do so where permitted. */
private HttpURLConnection makeConnection(DataSpec dataSpec) throws IOException { private HttpURLConnection makeConnection(DataSpec dataSpec) throws IOException {
URL url = new URL(dataSpec.uri.toString()); URL url = new URL(dataSpec.uri.toString());
@HttpMethod int httpMethod = dataSpec.httpMethod; @DataSpec.HttpMethod int httpMethod = dataSpec.httpMethod;
@Nullable byte[] httpBody = dataSpec.httpBody; @Nullable byte[] httpBody = dataSpec.httpBody;
long position = dataSpec.position; long position = dataSpec.position;
long length = dataSpec.length; long length = dataSpec.length;
@ -543,7 +546,7 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
*/ */
private HttpURLConnection makeConnection( private HttpURLConnection makeConnection(
URL url, URL url,
@HttpMethod int httpMethod, @DataSpec.HttpMethod int httpMethod,
@Nullable byte[] httpBody, @Nullable byte[] httpBody,
long position, long position,
long length, long length,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -73,6 +73,19 @@
app:srcCompat="@drawable/ic_settings" /> app:srcCompat="@drawable/ic_settings" />
</LinearLayout> </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 <ImageButton
android:id="@+id/button_play" android:id="@+id/button_play"
android:layout_width="60dp" android:layout_width="60dp"
@ -85,6 +98,20 @@
android:clickable="true" android:clickable="true"
app:srcCompat="@drawable/ic_play_white_nopad" /> 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 <ImageButton
android:id="@+id/button_pause" android:id="@+id/button_pause"
android:layout_width="60dp" android:layout_width="60dp"
@ -143,7 +170,7 @@
app:layout_constraintTop_toTopOf="@id/text_position" app:layout_constraintTop_toTopOf="@id/text_position"
app:layout_constraintBottom_toBottomOf="@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:id="@+id/time_progress"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="16dp" 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="give_feedback_on_the_application">Give feedback on the application</string>
<string name="info">Info</string> <string name="info">Info</string>
<string name="live_chat_webview">Live Chat Webview</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">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="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> <string name="preview_feed_items">Preview Feed Items</string>
@ -712,6 +714,7 @@
<string name="fcast_technical_documentation">FCast Technical Documentation</string> <string name="fcast_technical_documentation">FCast Technical Documentation</string>
<string name="login_to_view_your_comments">Login to view your comments</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="polycentric_is_disabled">Polycentric is disabled</string>
<string name="play_pause">Play Pause</string>
<string-array name="home_screen_array"> <string-array name="home_screen_array">
<item>Recommendations</item> <item>Recommendations</item>
<item>Subscriptions</item> <item>Subscriptions</item>

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

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