mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-05-29 21:10:24 +02:00
Merge branch 'media3-migration' into 'master'
Media3 migration. See merge request videostreaming/grayjay!10
This commit is contained in:
commit
0432f06eb3
@ -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'
|
||||||
|
@ -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)
|
||||||
|
@ -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;
|
||||||
|
@ -1,12 +1,17 @@
|
|||||||
package com.futo.platformplayer.casting
|
package com.futo.platformplayer.casting
|
||||||
|
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import com.futo.platformplayer.logging.Logger
|
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.getConnectedSocket
|
import com.futo.platformplayer.getConnectedSocket
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.CastingDeviceInfo
|
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||||
import com.futo.platformplayer.toInetAddress
|
import com.futo.platformplayer.toInetAddress
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@ -18,7 +23,7 @@ class AirPlayCastingDevice : CastingDevice {
|
|||||||
override var usedRemoteAddress: InetAddress? = null;
|
override var usedRemoteAddress: InetAddress? = null;
|
||||||
override var localAddress: InetAddress? = null;
|
override var localAddress: InetAddress? = null;
|
||||||
override val canSetVolume: Boolean get() = false;
|
override val canSetVolume: Boolean get() = false;
|
||||||
override val canSetSpeed: Boolean get() = false; //TODO: Implement playback speed for AirPlay
|
override val canSetSpeed: Boolean get() = true;
|
||||||
|
|
||||||
var addresses: Array<InetAddress>? = null;
|
var addresses: Array<InetAddress>? = null;
|
||||||
var port: Int = 0;
|
var port: Int = 0;
|
||||||
@ -59,6 +64,10 @@ class AirPlayCastingDevice : CastingDevice {
|
|||||||
} else {
|
} else {
|
||||||
post("play", "text/parameters", "Content-Location: $contentId\r\nStart-Position: 0");
|
post("play", "text/parameters", "Content-Location: $contentId\r\nStart-Position: 0");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (speed != null) {
|
||||||
|
changeSpeed(speed)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun loadContent(contentType: String, content: String, resumePosition: Double, duration: Double, speed: Double?) {
|
override fun loadContent(contentType: String, content: String, resumePosition: Double, duration: Double, speed: Double?) {
|
||||||
@ -186,6 +195,11 @@ class AirPlayCastingDevice : CastingDevice {
|
|||||||
_scopeIO = null;
|
_scopeIO = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun changeSpeed(speed: Double) {
|
||||||
|
this.speed = speed
|
||||||
|
post("rate?value=$speed")
|
||||||
|
}
|
||||||
|
|
||||||
override fun getDeviceInfo(): CastingDeviceInfo {
|
override fun getDeviceInfo(): CastingDeviceInfo {
|
||||||
return CastingDeviceInfo(name!!, CastProtocolType.AIRPLAY, addresses!!.filter { a -> a.hostAddress != null }.map { a -> a.hostAddress!! }.toTypedArray(), port);
|
return CastingDeviceInfo(name!!, CastProtocolType.AIRPLAY, addresses!!.filter { a -> a.hostAddress != null }.map { a -> a.hostAddress!! }.toTypedArray(), port);
|
||||||
}
|
}
|
||||||
|
@ -81,7 +81,7 @@ abstract class CastingDevice {
|
|||||||
var speed: Double = 1.0
|
var speed: Double = 1.0
|
||||||
set(value) {
|
set(value) {
|
||||||
val changed = value != field;
|
val changed = value != field;
|
||||||
speed = value;
|
field = value;
|
||||||
if (changed) {
|
if (changed) {
|
||||||
onSpeedChanged.emit(value);
|
onSpeedChanged.emit(value);
|
||||||
}
|
}
|
||||||
|
@ -3,14 +3,24 @@ package com.futo.platformplayer.casting
|
|||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.casting.models.*
|
import com.futo.platformplayer.casting.models.FCastPlayMessage
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.casting.models.FCastPlaybackErrorMessage
|
||||||
|
import com.futo.platformplayer.casting.models.FCastPlaybackUpdateMessage
|
||||||
|
import com.futo.platformplayer.casting.models.FCastSeekMessage
|
||||||
|
import com.futo.platformplayer.casting.models.FCastSetSpeedMessage
|
||||||
|
import com.futo.platformplayer.casting.models.FCastSetVolumeMessage
|
||||||
|
import com.futo.platformplayer.casting.models.FCastVersionMessage
|
||||||
|
import com.futo.platformplayer.casting.models.FCastVolumeUpdateMessage
|
||||||
import com.futo.platformplayer.getConnectedSocket
|
import com.futo.platformplayer.getConnectedSocket
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.CastingDeviceInfo
|
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||||
import com.futo.platformplayer.toHexString
|
import com.futo.platformplayer.toHexString
|
||||||
import com.futo.platformplayer.toInetAddress
|
import com.futo.platformplayer.toInetAddress
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.io.DataInputStream
|
import java.io.DataInputStream
|
||||||
@ -89,6 +99,8 @@ class FCastCastingDevice : CastingDevice {
|
|||||||
time = resumePosition,
|
time = resumePosition,
|
||||||
speed = speed
|
speed = speed
|
||||||
));
|
));
|
||||||
|
|
||||||
|
this.speed = speed ?: 1.0
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun loadContent(contentType: String, content: String, resumePosition: Double, duration: Double, speed: Double?) {
|
override fun loadContent(contentType: String, content: String, resumePosition: Double, duration: Double, speed: Double?) {
|
||||||
@ -110,6 +122,8 @@ class FCastCastingDevice : CastingDevice {
|
|||||||
time = resumePosition,
|
time = resumePosition,
|
||||||
speed = speed
|
speed = speed
|
||||||
));
|
));
|
||||||
|
|
||||||
|
this.speed = speed ?: 1.0
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun changeVolume(volume: Double) {
|
override fun changeVolume(volume: Double) {
|
||||||
@ -122,12 +136,12 @@ class FCastCastingDevice : CastingDevice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun changeSpeed(speed: Double) {
|
override fun changeSpeed(speed: Double) {
|
||||||
if (invokeInIOScopeIfRequired({ changeSpeed(volume) })) {
|
if (invokeInIOScopeIfRequired({ changeSpeed(speed) })) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.speed = speed
|
this.speed = speed
|
||||||
sendMessage(Opcode.SET_SPEED, FCastSetSpeedMessage(volume))
|
sendMessage(Opcode.SET_SPEED, FCastSetSpeedMessage(speed))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun seekVideo(timeSeconds: Double) {
|
override fun seekVideo(timeSeconds: Double) {
|
||||||
@ -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);
|
||||||
|
@ -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());
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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.");
|
||||||
|
@ -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");
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -18,8 +18,11 @@ class SlideUpMenuButtonList : LinearLayout {
|
|||||||
val onClick = Event1<String>();
|
val onClick = Event1<String>();
|
||||||
val buttons: HashMap<String, LinearLayout> = hashMapOf();
|
val buttons: HashMap<String, LinearLayout> = hashMapOf();
|
||||||
var _activeText: String? = null;
|
var _activeText: String? = null;
|
||||||
|
val id: String?
|
||||||
|
|
||||||
|
constructor(context: Context, attrs: AttributeSet? = null, id: String? = null): super(context, attrs) {
|
||||||
|
this.id = id
|
||||||
|
|
||||||
constructor(context: Context, attrs: AttributeSet? = null): super(context, attrs) {
|
|
||||||
LayoutInflater.from(context).inflate(R.layout.overlay_slide_up_menu_button_list, this, true);
|
LayoutInflater.from(context).inflate(R.layout.overlay_slide_up_menu_button_list, this, true);
|
||||||
|
|
||||||
_root = findViewById(R.id.root);
|
_root = findViewById(R.id.root);
|
||||||
|
@ -26,7 +26,7 @@ class SlideUpMenuOverlay : RelativeLayout {
|
|||||||
private lateinit var _viewContainer: LinearLayout;
|
private lateinit var _viewContainer: LinearLayout;
|
||||||
private var _animated: Boolean = true;
|
private var _animated: Boolean = true;
|
||||||
|
|
||||||
private var _groupItems: List<View>;
|
var groupItems: List<View>;
|
||||||
|
|
||||||
var isVisible = false
|
var isVisible = false
|
||||||
private set;
|
private set;
|
||||||
@ -36,7 +36,7 @@ class SlideUpMenuOverlay : RelativeLayout {
|
|||||||
|
|
||||||
constructor(context: Context, attrs: AttributeSet? = null): super(context, attrs) {
|
constructor(context: Context, attrs: AttributeSet? = null): super(context, attrs) {
|
||||||
init(false, null);
|
init(false, null);
|
||||||
_groupItems = listOf();
|
groupItems = listOf();
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(context: Context, parent: ViewGroup, titleText: String, okText: String?, animated: Boolean, items: List<View>, hideButtons: Boolean = false): super(context){
|
constructor(context: Context, parent: ViewGroup, titleText: String, okText: String?, animated: Boolean, items: List<View>, hideButtons: Boolean = false): super(context){
|
||||||
@ -47,7 +47,7 @@ class SlideUpMenuOverlay : RelativeLayout {
|
|||||||
_container!!.addView(this);
|
_container!!.addView(this);
|
||||||
}
|
}
|
||||||
_textTitle.text = titleText;
|
_textTitle.text = titleText;
|
||||||
_groupItems = items;
|
groupItems = items;
|
||||||
|
|
||||||
if(hideButtons) {
|
if(hideButtons) {
|
||||||
_textCancel.visibility = GONE;
|
_textCancel.visibility = GONE;
|
||||||
@ -74,7 +74,7 @@ class SlideUpMenuOverlay : RelativeLayout {
|
|||||||
item.setParentClickListener { hide() };
|
item.setParentClickListener { hide() };
|
||||||
}
|
}
|
||||||
|
|
||||||
_groupItems = items;
|
groupItems = items;
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun init(animated: Boolean, okText: String?){
|
private fun init(animated: Boolean, okText: String?){
|
||||||
@ -116,12 +116,12 @@ class SlideUpMenuOverlay : RelativeLayout {
|
|||||||
|
|
||||||
fun selectOption(groupTag: Any?, itemTag: Any?, multiSelect: Boolean = false, toggle: Boolean = false): Boolean {
|
fun selectOption(groupTag: Any?, itemTag: Any?, multiSelect: Boolean = false, toggle: Boolean = false): Boolean {
|
||||||
var didSelect = false;
|
var didSelect = false;
|
||||||
for(view in _groupItems) {
|
for(view in groupItems) {
|
||||||
if(view is SlideUpMenuGroup && view.groupTag == groupTag)
|
if(view is SlideUpMenuGroup && view.groupTag == groupTag)
|
||||||
didSelect = didSelect || view.selectItem(itemTag);
|
didSelect = didSelect || view.selectItem(itemTag);
|
||||||
}
|
}
|
||||||
if(groupTag == null)
|
if(groupTag == null)
|
||||||
for(item in _groupItems)
|
for(item in groupItems)
|
||||||
if(item is SlideUpMenuItem) {
|
if(item is SlideUpMenuItem) {
|
||||||
if(multiSelect) {
|
if(multiSelect) {
|
||||||
if(item.itemTag == itemTag)
|
if(item.itemTag == itemTag)
|
||||||
|
@ -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;
|
||||||
|
@ -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) {
|
||||||
|
@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
Loading…
x
Reference in New Issue
Block a user