Compare commits

..

8 Commits

Author SHA1 Message Date
Kai 53f2be2b4c fix merge
Changelog: changed
2025-08-19 10:22:31 -04:00
Kai 11d4ec383e fix merge
Changelog: changed
2025-08-18 11:21:25 -04:00
Kai 493d77b43b Merge branch 'master' into simple-motion-layout
# Conflicts:
#	app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentFeedView.kt
#	app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt
#	app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt
#	app/src/main/java/com/futo/platformplayer/views/behavior/GestureControlView.kt
#	app/src/main/res/layout/video_view.xml
#	app/src/stable/assets/sources/apple-podcasts
#	app/src/unstable/assets/sources/apple-podcasts
2025-08-18 11:05:23 -04:00
Kai a058bdbfef merge 2025-02-20 16:15:04 -06:00
Kai 3d863b9c89 - fix PiP when player closed
- improve PiP speed on Android 12+
2024-12-17 14:46:18 -06:00
Kai 04c0679930 - fix PiP issues
- fix overlay issues
2024-12-17 13:44:16 -06:00
Kai 45ded8d384 update MotionLayout code
fix touch region bug
move more pieces to xml
upgrade full screen gesture
improve mini player UI
2024-12-16 18:21:57 -06:00
Kai f64efdc964 deps 2024-12-14 14:44:40 -06:00
93 changed files with 1322 additions and 5331 deletions
+1 -7
View File
@@ -163,7 +163,7 @@ dependencies {
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.11.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
//Images
annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
@@ -231,10 +231,4 @@ dependencies {
testImplementation "org.mockito:mockito-core:5.4.0"
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
//Rust casting SDK
implementation('org.futo.gitlab.videostreaming.fcast-sdk-jitpack:sender-sdk-minimal:0.3.1') {
// Polycentricandroid includes this
exclude group: 'net.java.dev.jna'
}
}
+2 -22
View File
@@ -1022,35 +1022,15 @@
return x.value
});
let settingsToUse = __DEV_SETTINGS ?? {};
if (true) {
for (let setting of this.Plugin?.currentPlugin?.settings) {
if (typeof settingsToUse[setting.variable] == "undefined") {
switch (setting?.type?.toLowerCase()) {
case "boolean":
settingsToUse[setting.variable] = setting.default === 'true';
break;
case "dropdown":
let dropDownIndex = parseInt(setting.default);
if (dropDownIndex) {
settingsToUse[setting.variable] = setting.options[dropDownIndex];
}
break;
}
}
}
}
if(name == "enable") {
if(parameterVals.length > 0)
parameterVals[0] = this.Plugin.currentPlugin;
else
parameterVals.push(this.Plugin.currentPlugin);
if(parameterVals.length > 1)
parameterVals[1] = settingsToUse;
parameterVals[1] = __DEV_SETTINGS;
else
parameterVals.push(settingsToUse);
parameterVals.push(__DEV_SETTINGS);
}
const func = source[name];
-1
View File
@@ -67,7 +67,6 @@ class ScriptException extends Error {
super(arguments[0]);
this.plugin_type = "ScriptException";
this.message = arguments[0];
this.msg = arguments[0];
}
else {
super(msg);
@@ -194,6 +194,7 @@ fun V8ObjectToHashMap(obj: V8ValueObject?): HashMap<String, String> {
return map;
}
fun <T: V8Value> V8ValuePromise.toV8ValueBlocking(plugin: V8Plugin): T {
val latch = CountDownLatch(1);
var promiseResult: T? = null;
@@ -203,19 +204,16 @@ fun <T: V8Value> V8ValuePromise.toV8ValueBlocking(plugin: V8Plugin): T {
override fun onFulfilled(p0: V8Value?) {
if(p0 is V8ValueError)
promiseException = ScriptExecutionException(plugin.config, p0.message);
else {
if(p0 is V8ValueObject)
p0.setWeak();
else
promiseResult = p0 as T;
}
latch.countDown();
}
override fun onRejected(p0: V8Value?) {
promiseException = p0?.toException(plugin.config);
promiseException = (NotImplementedError("onRejected promise not implemented.."));
latch.countDown();
}
override fun onCatch(p0: V8Value?) {
promiseException = p0?.toException(plugin.config);
promiseException = (NotImplementedError("onCatch promise not implemented.."));
latch.countDown();
}
});
@@ -225,25 +223,8 @@ fun <T: V8Value> V8ValuePromise.toV8ValueBlocking(plugin: V8Plugin): T {
promiseException = CancellationException("Cancelled by system");
latch.countDown();
}
//Logger.i("V8", "V8ValueBlocking started (Busy) [" + blockCount + "]" + Thread.currentThread().stackTrace.drop(3)?.firstOrNull()?.toString() + ", " + Thread.currentThread().stackTrace.drop(4)?.firstOrNull()?.toString()+ ", " + Thread.currentThread().stackTrace.drop(5)?.firstOrNull()?.toString());
if(!promise.isPending) {
try {
Logger.i("V8", "V8Promise resolved synchronously");
if(promise.isFulfilled)
promiseResult = promise.getResult<T>();
else
promiseException = promise.getResult<V8Value>().toException(plugin.config);
}
catch(ex: Throwable) {
promiseException = ex;
}
}
else {
plugin.unbusy {
latch.await();
}
plugin.unbusy {
latch.await();
}
if(promiseException != null)
throw promiseException!!;
@@ -269,11 +250,11 @@ fun <T: V8Value> V8ValuePromise.toV8ValueAsync(plugin: V8Plugin): V8Deferred<T>
}
override fun onRejected(p0: V8Value?) {
plugin.resolvePromise(promise);
underlyingDef.completeExceptionally(p0?.toException(plugin.config) ?: NotImplementedError("onRejected promise not implemented.."));
underlyingDef.completeExceptionally(NotImplementedError("onRejected promise not implemented.."));
}
override fun onCatch(p0: V8Value?) {
plugin.resolvePromise(promise);
underlyingDef.completeExceptionally(p0?.toException(plugin.config) ?: NotImplementedError("onCatch promise not implemented.."));
underlyingDef.completeExceptionally(NotImplementedError("onCatch promise not implemented.."));
}
});
}
@@ -284,20 +265,6 @@ fun <T: V8Value> V8ValuePromise.toV8ValueAsync(plugin: V8Plugin): V8Deferred<T>
return def;
}
fun V8Value.toException(config: IV8PluginConfig): Throwable {
val p0 = this;
if(p0 is V8ValueObject) {
val pluginType = p0.getOrDefault(config, "plugin_type", "Promise Exception", "")?.let { if(!it.isNullOrBlank()) it + "" else "" }
val msg = p0.getOrDefault<String?>(config, "msg", "Promise Exception", null)
?: p0.getOrDefault(config, "message", "Promise Exception", "");
return Exception("Promise Failed: " + pluginType + msg);
}
else if(p0 is V8ValueString)
return Exception("Promise Failed:" + p0.value);
else
return NotImplementedError("onCatch promise not implemented..");
}
class V8Deferred<T>(val deferred: Deferred<T>, val estDuration: Int = -1): Deferred<T> by deferred {
fun <R> convert(conversion: (result: T)->R): V8Deferred<R>{
@@ -25,7 +25,6 @@ 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.StateSync
import com.futo.platformplayer.states.StateUpdate
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.FragmentedStorageFileJson
@@ -35,7 +34,6 @@ import com.futo.platformplayer.views.fields.DropdownFieldOptionsId
import com.futo.platformplayer.views.fields.FieldForm
import com.futo.platformplayer.views.fields.FormField
import com.futo.platformplayer.views.fields.FormFieldButton
import com.futo.platformplayer.views.fields.FormFieldWarning
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -203,8 +201,6 @@ class Settings : FragmentedStorageFileJson() {
8 -> "zh";
9 -> "ru";
10 -> "ar";
11 -> "it";
12 -> "tr";
else -> null
}
}
@@ -612,11 +608,6 @@ class Settings : FragmentedStorageFileJson() {
@AdvancedField
@FormField(R.string.shorts_pregenerate, FieldForm.TOGGLE, R.string.shorts_pregenerate_description, 28)
var shortsPregenerate: Boolean = false;
@AdvancedField
@FormField(R.string.shorts_fit_video, FieldForm.TOGGLE, R.string.shorts_fit_video_description, 29)
@FormFieldWarning(R.string.shorts_fit_video_warning)
var shortsFitVideo: Boolean = false;
}
@FormField(R.string.comments, "group", R.string.comments_description, 6)
@@ -719,11 +710,6 @@ class Settings : FragmentedStorageFileJson() {
@Serializable(with = FlexibleBooleanSerializer::class)
var allowLinkLocalIpv4: Boolean = false;
@AdvancedField
@FormField(R.string.experimental_cast, FieldForm.TOGGLE, R.string.experimental_cast_description, 6)
@Serializable(with = FlexibleBooleanSerializer::class)
var experimentalCasting: Boolean = false
/*TODO: Should we have a different casting quality?
@FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3)
@DropdownFieldOptionsId(R.array.preferred_quality_array)
@@ -1106,39 +1092,6 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.local_connections, FieldForm.TOGGLE, R.string.local_connections_description, 3)
var localConnections: Boolean = true;
var syncServerUrl: String? = null;
@FormField(R.string.relay_server, FieldForm.READONLYTEXT, -1, 6)
val syncServer: String get() = if(syncServerUrl?.isBlank() == true) StateSync.RELAY_SERVER else syncServerUrl ?: StateSync.RELAY_SERVER;
@AdvancedField
@FormField(R.string.configure_sync_server, FieldForm.BUTTON, R.string.configure_sync_server_description, 7)
fun configureSyncServer() {
SettingsActivity.getActivity()?.let { context ->
UIDialogs.showDialog(context, R.drawable.device_sync, false,
"Enter the url to your relay server",
"Using your own relay server requires a proper setup with portforwarding.\nUse at your own risk.",
null,
syncServerUrl ?: "",
"YourRelayServerDomain.com", 0,
UIDialogs.Action("Cancel", {}),
UIDialogs.Action("Reset", {
syncServerUrl = null;
instance.save();
context.reloadSettings();
UIDialogs.toast("Sync server changes require a restart");
}, UIDialogs.ActionStyle.ACCENT),
UIDialogs.Action.withInput("Configure", {
syncServerUrl = it?.text
instance.save();
context.reloadSettings();
UIDialogs.toast("Sync server changes require a restart");
}, UIDialogs.ActionStyle.PRIMARY),
)
}
}
}
@FormField(R.string.info, FieldForm.GROUP, -1, 21)
@@ -113,8 +113,8 @@ class UIDialogs {
currentDialog.code,
currentDialog.defaultCloseAction,
*currentDialog.actions.map {
return@map Action.withInput(it.text, { str ->
it.invokeAction(str);
return@map Action(it.text, {
it.action();
multiShowDialog(context, dialogDescriptor.drop(1), finally);
}, it.style);
}.toTypedArray());
@@ -203,9 +203,7 @@ class UIDialogs {
fun showDialog(context: Context, icon: Int, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action): AlertDialog {
return showDialog(context, icon, false, text, textDetails, code, defaultCloseAction, *actions);
}
fun showDialog(context: Context, icon: Int, animated: Boolean, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action): AlertDialog
= showDialog(context, icon, animated, text, textDetails, code, null, null, defaultCloseAction, *actions);
fun showDialog(context: Context, icon: Int, animated: Boolean, text: String, textDetails: String? = null, code: String? = null, input: String?, placeholder: String?, defaultCloseAction: Int, vararg actions: Action): AlertDialog {
fun showDialog(context: Context, icon: Int, animated: Boolean, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action): AlertDialog {
val builder = AlertDialog.Builder(context);
val view = LayoutInflater.from(context).inflate(R.layout.dialog_multi_button, null);
builder.setView(view);
@@ -228,16 +226,6 @@ class UIDialogs {
this.text = textDetails;
}
};
var inputView = view.findViewById<TextView>(R.id.dialog_text_input);
inputView.apply {
if (input == null && placeholder == null) this.visibility = View.GONE;
else {
this.text = input ?: "";
this.hint = placeholder ?: "";
this.visibility = View.VISIBLE;
this.textAlignment = View.TEXT_ALIGNMENT_VIEW_START
}
};
view.findViewById<TextView>(R.id.dialog_text_code).apply {
if (code == null) this.visibility = View.GONE;
else {
@@ -262,7 +250,7 @@ class UIDialogs {
buttonView.textSize = 14f;
buttonView.typeface = resources.getFont(R.font.inter_regular);
buttonView.text = act.text;
buttonView.setOnClickListener { act.invokeAction(DialogResult(inputView?.text?.toString())); dialog.dismiss(); };
buttonView.setOnClickListener { act.action(); dialog.dismiss(); };
when(act.style) {
ActionStyle.PRIMARY -> buttonView.setBackgroundResource(R.drawable.background_button_primary);
ActionStyle.ACCENT -> buttonView.setBackgroundResource(R.drawable.background_button_accent);
@@ -287,7 +275,7 @@ class UIDialogs {
};
dialog.setOnCancelListener {
if(defaultCloseAction >= 0 && defaultCloseAction < actions.size)
actions[defaultCloseAction].invokeAction(DialogResult(inputView?.text?.toString()));
actions[defaultCloseAction].action();
}
dialog.setOnDismissListener {
registerDialogClosed(dialog);
@@ -547,36 +535,17 @@ class UIDialogs {
}
class Action {
val text: String;
val action: ((DialogResult?)->Unit);
val action: ()->Unit;
val style: ActionStyle;
var center: Boolean;
constructor(text: String, action: ()->Unit, style: ActionStyle = ActionStyle.NONE, center: Boolean = false) {
this.text = text;
this.action = { action() };
this.style = style;
this.center = center;
}
protected constructor(text: String, action: (DialogResult?)->Unit, style: ActionStyle = ActionStyle.NONE, center: Boolean = false) {
this.text = text;
this.action = action;
this.style = style;
this.center = center;
}
fun invokeAction(input: DialogResult? = null) {
this.action(input);
}
companion object {
fun withInput(text: String, action: (DialogResult?)->Unit, style: ActionStyle = ActionStyle.NONE, center: Boolean = false): Action {
return Action(text, action, style, center);
}
}
}
class DialogResult(
val text: String?
);
enum class ActionStyle {
NONE,
PRIMARY,
@@ -76,25 +76,9 @@ class LoginActivity : AppCompatActivity() {
};
var isFirstLoad = true;
val loginWarnings = authConfig.loginWarnings?.toMutableList() ?: mutableListOf<SourcePluginAuthConfig.Warning>();
val uiMods = authConfig.uiMods?.toMutableList() ?: mutableListOf<SourcePluginAuthConfig.UIMod>();
var currentScale = 100;
var currentDesktop = false;
webViewClient.onPageLoaded.subscribe { view, url ->
_textUrl.setText(url ?: "");
if(loginWarnings.size > 0 && url != null) {
synchronized(loginWarnings) {
val warning = loginWarnings.find { url.matches(it.getRegex()) };
if(warning != null) {
if(warning.once == true)
loginWarnings.remove(warning);
UIDialogs.showDialog(this@LoginActivity, R.drawable.ic_warning_yellow, warning.text ?: "", warning.details ?: "", null, 0,
UIDialogs.Action("Understood", {
}, UIDialogs.ActionStyle.PRIMARY));
}
}
}
if(!isFirstLoad)
return@subscribe;
isFirstLoad = false;
@@ -105,34 +89,18 @@ class LoginActivity : AppCompatActivity() {
view?.evaluateJavascript("setTimeout(()=> document.querySelector(\"${authConfig.loginButton}\")?.click(), 1000)", {});
}
/*
var specifiedScale = false;
var specifiedDesktop = false;
if(uiMods.size > 0 && url != null) {
synchronized(uiMods) {
val uimod = uiMods.find { url.matches(it.getRegex()) };
if(uimod != null) {
if(uimod.scale != null) {
currentScale =(uimod.scale * 100).toInt();
_webView.setInitialScale(currentScale);
specifiedScale = true;
}
if(uimod.desktop != null && uimod.desktop) {
_webView.settings.useWideViewPort = true;
specifiedDesktop = true;
}
if(loginWarnings.size > 0) {
synchronized(loginWarnings) {
val warning = loginWarnings.find { it.url.matches(it.getRegex()) };
if(warning != null) {
if(warning.once == true)
loginWarnings.remove(warning);
UIDialogs.showDialog(this@LoginActivity, R.drawable.ic_warning_yellow, warning.text ?: "", warning.details ?: "", null, 0,
UIDialogs.Action("Understood", {
}, UIDialogs.ActionStyle.PRIMARY));
}
}
}
if(!specifiedScale && currentScale != 100) {
currentScale = (100).toInt();
_webView.setInitialScale(currentScale);
}
if(!specifiedDesktop && currentDesktop) {
_webView.settings.useWideViewPort = false;
currentDesktop = false;
}
*/
}
_webView.settings.domStorageEnabled = true;
@@ -39,7 +39,6 @@ import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.models.video.LocalVideoDetails
import com.futo.platformplayer.casting.StateCasting
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.dp
@@ -366,14 +365,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragVideoDetail.onMinimize.subscribe {
updateSegmentPaddings();
};
_fragVideoDetail.onTransitioning.subscribe {
if (it || _fragVideoDetail.state != VideoDetailFragment.State.MINIMIZED)
_fragContainerOverlay.elevation =
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 15f, resources.displayMetrics);
else
_fragContainerOverlay.elevation =
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics);
}
_fragVideoDetail.onCloseEvent.subscribe {
_fragMainHome.setPreviewsEnabled(true);
@@ -769,7 +760,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
if (targetData != null) {
lifecycleScope.launch(Dispatchers.Main) {
try {
handleUrlAll(targetData, intent)
handleUrlAll(targetData)
} catch (e: Throwable) {
Logger.e(TAG, "Unhandled exception in handleUrlAll", e)
}
@@ -780,9 +771,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
}
}
suspend fun handleUrlAll(url: String, openIntent: Intent? = null) {
suspend fun handleUrlAll(url: String) {
val uri = Uri.parse(url)
val intent = openIntent ?: this.intent;
when (uri.scheme) {
"grayjay" -> {
if (url.startsWith("grayjay://license/")) {
@@ -809,11 +799,11 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
}
"content" -> {
if (!handleContent(url, intent?.type)) {
if (!handleContent(url, intent.type)) {
UIDialogs.showSingleButtonDialog(
this,
R.drawable.ic_play,
getString(R.string.unknown_content_format) + " [${url}]\n[${intent?.type}]",
getString(R.string.unknown_content_format) + " [${url}]\n[${intent.type}]",
"Ok",
{ });
}
@@ -934,12 +924,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
} else if (file.lowercase().endsWith(".txt") || mime == "text/plain") {
return handleUnknownText(String(data));
}
else if (mime?.let { it.startsWith("video/") || it.startsWith("audio/") } ?: false) {
val mediaItem = LocalVideoDetails.fromContent(file, mime);
navigateWhenReady(_fragVideoDetail, mediaItem);
return true;
}
return false;
}
@@ -1054,7 +1038,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
Logger.i(TAG, "handleFCast");
try {
StateCasting.instance.handleUrl(url)
StateCasting.instance.handleUrl(this, url)
return true;
} catch (e: Throwable) {
Log.e(TAG, "Failed to parse FCast URL '${url}'.", e)
@@ -1151,8 +1135,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
if (_fragContainerVideoDetail.visibility != View.VISIBLE)
_fragContainerVideoDetail.visibility = View.VISIBLE;
when (segment.state) {
VideoDetailFragment.State.MINIMIZED -> segment.maximizeVideoDetail()
VideoDetailFragment.State.CLOSED -> segment.maximizeVideoDetail()
VideoDetailFragment.State.MINIMIZED -> segment.maximizeVideoDetail(false)
VideoDetailFragment.State.CLOSED -> segment.maximizeVideoDetail(false)
else -> {}
}
segment.onShown(parameter, isBack);
@@ -1277,7 +1261,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
}
}
private fun updateSegmentPaddings() {
var paddingBottom = 0f;
if (fragCurrent.hasBottomBar)
@@ -1288,9 +1271,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
.toInt()
);
if (_fragVideoDetail.state == VideoDetailFragment.State.MINIMIZED)
paddingBottom += HEIGHT_VIDEO_MINIMIZED_DP;
_fragContainerMain.setPadding(
0, 0, 0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, paddingBottom, resources.displayMetrics)
.toInt()
@@ -2,24 +2,10 @@ package com.futo.platformplayer.api.media.models.streams
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalAudioContentSource
import com.futo.platformplayer.downloads.VideoLocal
class LocalVideoUnMuxedSourceDescriptor : VideoUnMuxedSourceDescriptor {
override val videoSources: Array<IVideoSource>;
override val audioSources: Array<IAudioSource>;
constructor(video: VideoLocal) {
videoSources = video.videoSource.toTypedArray();
audioSources = video.audioSource.toTypedArray();
}
constructor(audio: LocalAudioContentSource) {
videoSources = arrayOf()
audioSources = arrayOf(audio);
}
constructor(videoSources: Array<IVideoSource>, audioSources: Array<IAudioSource>) {
this.videoSources = videoSources;
this.audioSources = audioSources;
}
class LocalVideoUnMuxedSourceDescriptor(private val video: VideoLocal) : VideoUnMuxedSourceDescriptor() {
override val videoSources: Array<IVideoSource> get() = video.videoSource.toTypedArray();
override val audioSources: Array<IAudioSource> get() = video.audioSource.toTypedArray();
}
@@ -14,8 +14,7 @@ class AudioUrlSource(
override val language: String = Language.UNKNOWN,
override val duration: Long? = null,
override var priority: Boolean = false,
override var original: Boolean = false,
var isLocal: Boolean = false
override var original: Boolean = false
) : IAudioUrlSource, IStreamMetaDataSource{
override var streamMetaData: StreamMetaData? = null;
@@ -14,8 +14,7 @@ open class VideoUrlSource(
override val codec : String = "",
override val bitrate : Int? = 0,
override var priority: Boolean = false,
var isLocal: Boolean = false
override var priority: Boolean = false
) : IVideoUrlSource, IStreamMetaDataSource {
override var streamMetaData: StreamMetaData? = null;
@@ -1,122 +0,0 @@
package com.futo.platformplayer.api.media.models.video
import android.annotation.SuppressLint
import android.net.Uri
import android.provider.MediaStore
import android.provider.OpenableColumns
import androidx.core.net.toUri
import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.Serializer
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.LocalVideoUnMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.AudioUrlSource
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.api.media.models.streams.sources.LocalAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.LocalVideoSource
import com.futo.platformplayer.api.media.models.streams.sources.SubtitleRawSource
import com.futo.platformplayer.api.media.models.streams.sources.VideoUrlSource
import com.futo.platformplayer.api.media.platforms.local.models.LocalVideoMuxedSourceDescriptor
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalAudioContentSource
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoContentSource
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoFileSource
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.others.Language
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
import com.futo.platformplayer.states.StateApp
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.time.OffsetDateTime
@kotlinx.serialization.Serializable
open class LocalVideoDetails(
override val id: PlatformID,
override val name: String,
override val thumbnails: Thumbnails,
override val author: PlatformAuthorLink,
override val url: String,
override val duration: Long,
val mimeType: String? = null,
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
override val datetime: OffsetDateTime?
) : IPlatformVideo, IPlatformVideoDetails {
final override val contentType: ContentType get() = ContentType.MEDIA;
override var playbackTime: Long = -1;
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
override var playbackDate: OffsetDateTime? = null;
override val isLive: Boolean get() = false;
override val dash: IDashManifestSource? get() = null;
override val hls: IHLSManifestSource? get() = null;
override val live: IVideoSource? get() = null;
override val shareUrl: String = ""
override val viewCount: Long = -1
override val rating: IRating = RatingLikes(0)
override val description: String = "";
override val video: IVideoSourceDescriptor = (if(mimeType?.startsWith("audio/") ?: false)
(LocalVideoUnMuxedSourceDescriptor(
arrayOf(),
arrayOf(LocalAudioContentSource(url, mimeType ?: "", name))
))
else (LocalVideoMuxedSourceDescriptor(
LocalVideoContentSource(url, mimeType ?: "", name)
))
);
override val preview: ISerializedVideoSourceDescriptor? = null;
override val subtitles: List<SubtitleRawSource> = listOf()
override val isShort: Boolean = false
fun toJson() : String {
return Json.encodeToString(this);
}
fun fromJson(str : String) : SerializedPlatformVideoDetails {
return Serializer.json.decodeFromString<SerializedPlatformVideoDetails>(str);
}
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? = null;
override fun getPlaybackTracker(): IPlaybackTracker? = null;
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? = null;
companion object {
fun fromFile(name: String, filePath: String, mimeType: String? = null) : LocalVideoDetails {
if(filePath.startsWith("content://"))
return fromContent(filePath, mimeType);
return LocalVideoDetails(PlatformID("FILE", filePath, null, 0, -1),
name, Thumbnails(), PlatformAuthorLink.UNKNOWN, filePath, -1, mimeType, null);
}
fun fromContent(contentUrl: String, mimeType: String? = null) : LocalVideoDetails {
var nameToUse = getFileNameFromContentUrl(contentUrl) ?: "File";
return LocalVideoDetails(PlatformID("FILE", contentUrl, null, 0, -1),
nameToUse, Thumbnails(), PlatformAuthorLink.UNKNOWN, contentUrl, -1, mimeType, null);
}
@SuppressLint("Range")
private fun getFileNameFromContentUrl(url: String): String? {
val cursor = StateApp.instance.context.contentResolver.query(url.toUri(), null, null, null, null);
cursor?.moveToFirst();
val fileName = cursor?.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
cursor?.close();
return fileName;
}
}
}
@@ -16,8 +16,7 @@ class SourcePluginAuthConfig(
val loginButton: String? = null,
val domainHeadersToFind: Map<String, List<String>>? = null,
val loginWarning: String? = null,
val loginWarnings: List<Warning>? = null,
val uiMods: List<UIMod>? = null
val loginWarnings: List<Warning>? = null
) {
@Serializable
@@ -30,23 +29,6 @@ class SourcePluginAuthConfig(
@Contextual
private var _regex: Regex? = null;
fun getRegex(): Regex {
return _regex ?: url.let {
val reg = Regex(it);
_regex = reg;
return reg;
}
}
}
@Serializable
class UIMod(
val url: String,
val scale: Float?,
val desktop: Boolean?
) {
@Contextual
private var _regex: Regex? = null;
fun getRegex(): Regex {
return _regex ?: url.let {
val reg = Regex(it);
@@ -1,23 +1,13 @@
package com.futo.platformplayer.api.media.platforms.local.models
import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.api.media.models.streams.sources.LocalVideoSource
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoContentSource
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoFileSource
import com.futo.platformplayer.downloads.VideoLocal
class LocalVideoMuxedSourceDescriptor: VideoMuxedSourceDescriptor {
override val videoSources: Array<IVideoSource>;
constructor(video: LocalVideoFileSource) {
videoSources = arrayOf(video);
}
constructor(video: LocalVideoContentSource) {
videoSources = arrayOf(video);
}
constructor(videoSources: Array<IVideoSource>) {
this.videoSources = videoSources;
}
class LocalVideoMuxedSourceDescriptor(
private val video: LocalVideoFileSource
) : VideoMuxedSourceDescriptor() {
override val videoSources: Array<IVideoSource> get() = arrayOf(video);
}
@@ -1,33 +0,0 @@
package com.futo.platformplayer.api.media.platforms.local.models.sources
import android.content.Context
import android.provider.MediaStore
import android.provider.MediaStore.Video
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.api.media.models.streams.sources.VideoUrlSource
import com.futo.platformplayer.helpers.VideoHelper
import com.futo.platformplayer.others.Language
import java.io.File
class LocalAudioContentSource : IAudioSource {
override val name: String;
override val container: String;
override val codec: String = ""
override val bitrate: Int = 0
override val duration: Long;
override val priority: Boolean = false;
override val language: String = Language.UNKNOWN
override val original: Boolean = false;
var contentUrl: String;
constructor(contentUrl: String, mime: String, name: String? = null) {
this.name = name ?: "File";
container = mime;
duration = 0;
this.contentUrl = contentUrl;
}
}
@@ -1,34 +0,0 @@
package com.futo.platformplayer.api.media.platforms.local.models.sources
import android.content.Context
import android.provider.MediaStore
import android.provider.MediaStore.Video
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.api.media.models.streams.sources.VideoUrlSource
import com.futo.platformplayer.helpers.VideoHelper
import com.futo.platformplayer.others.Language
import java.io.File
class LocalAudioFileSource: IAudioSource {
override val name: String;
override val container: String;
override val codec: String = ""
override val bitrate: Int = 0
override val duration: Long;
override val priority: Boolean = false;
override val language: String = Language.UNKNOWN;
override val original: Boolean = false;
var file: File;
constructor(file: File) {
this.file = file;
name = file.name;
container = VideoHelper.videoExtensionToMimetype(file.extension) ?: "";
duration = 0;
}
}
@@ -1,33 +0,0 @@
package com.futo.platformplayer.api.media.platforms.local.models.sources
import android.content.Context
import android.provider.MediaStore
import android.provider.MediaStore.Video
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.api.media.models.streams.sources.VideoUrlSource
import com.futo.platformplayer.helpers.VideoHelper
import java.io.File
class LocalVideoContentSource: IVideoSource {
override val name: String;
override val width: Int;
override val height: Int;
override val container: String;
override val codec: String = ""
override val bitrate: Int = 0
override val duration: Long;
override val priority: Boolean = false;
var contentUrl: String;
constructor(contentUrl: String, mime: String, name: String? = null) {
this.name = name ?: "File";
width = 0;
height = 0;
container = mime;
duration = 0;
this.contentUrl = contentUrl;
}
}
@@ -20,10 +20,7 @@ class LocalVideoFileSource: IVideoSource {
override val duration: Long;
override val priority: Boolean = false;
var file: File;
constructor(file: File) {
this.file = file;
name = file.name;
width = 0;
height = 0;
@@ -15,7 +15,7 @@ import kotlinx.coroutines.launch
import java.net.InetAddress
import java.util.UUID
class AirPlayCastingDevice : CastingDeviceLegacy {
class AirPlayCastingDevice : CastingDevice {
//See for more info: https://nto.github.io/AirPlay
override val protocol: CastProtocolType get() = CastProtocolType.AIRPLAY;
@@ -2,78 +2,147 @@ package com.futo.platformplayer.casting
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.models.CastingDeviceInfo
import org.fcast.sender_sdk.Metadata
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.net.InetAddress
abstract class CastingDevice {
abstract val isReady: Boolean
abstract val usedRemoteAddress: InetAddress?
abstract val localAddress: InetAddress?
abstract val name: String?
abstract val onConnectionStateChanged: Event1<CastConnectionState>
abstract val onPlayChanged: Event1<Boolean>
abstract val onTimeChanged: Event1<Double>
abstract val onDurationChanged: Event1<Double>
abstract val onVolumeChanged: Event1<Double>
abstract val onSpeedChanged: Event1<Double>
abstract var connectionState: CastConnectionState
abstract val protocolType: CastProtocolType
abstract var isPlaying: Boolean
abstract val expectedCurrentTime: Double
abstract var speed: Double
abstract var time: Double
abstract var duration: Double
abstract var volume: Double
abstract fun canSetVolume(): Boolean
abstract fun canSetSpeed(): Boolean
@Throws
abstract fun resumePlayback()
@Throws
abstract fun pausePlayback()
@Throws
abstract fun stopPlayback()
@Throws
abstract fun seekTo(timeSeconds: Double)
@Throws
abstract fun changeVolume(timeSeconds: Double)
@Throws
abstract fun changeSpeed(speed: Double)
@Throws
abstract fun connect()
@Throws
abstract fun disconnect()
abstract fun getDeviceInfo(): CastingDeviceInfo
abstract fun getAddresses(): List<InetAddress>
@Throws
abstract fun loadVideo(
streamType: String,
contentType: String,
contentId: String,
resumePosition: Double,
duration: Double,
speed: Double?,
metadata: Metadata?
)
@Throws
abstract fun loadContent(
contentType: String,
content: String,
resumePosition: Double,
duration: Double,
speed: Double?,
metadata: Metadata?
)
abstract fun ensureThreadStarted()
enum class CastConnectionState {
DISCONNECTED,
CONNECTING,
CONNECTED
}
@Serializable(with = CastProtocolType.CastProtocolTypeSerializer::class)
enum class CastProtocolType {
CHROMECAST,
AIRPLAY,
FCAST;
object CastProtocolTypeSerializer : KSerializer<CastProtocolType> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("CastProtocolType", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: CastProtocolType) {
encoder.encodeString(value.name)
}
override fun deserialize(decoder: Decoder): CastProtocolType {
val name = decoder.decodeString()
return when (name) {
"FASTCAST" -> FCAST // Handle the renamed case
else -> CastProtocolType.valueOf(name)
}
}
}
}
abstract class CastingDevice {
abstract val protocol: CastProtocolType;
abstract val isReady: Boolean;
abstract var usedRemoteAddress: InetAddress?;
abstract var localAddress: InetAddress?;
abstract val canSetVolume: Boolean;
abstract val canSetSpeed: Boolean;
var name: String? = null;
var isPlaying: Boolean = false
set(value) {
val changed = value != field;
field = value;
if (changed) {
onPlayChanged.emit(value);
}
};
private var lastTimeChangeTime_ms: Long = 0
var time: Double = 0.0
private set
protected fun setTime(value: Double, changeTime_ms: Long = System.currentTimeMillis()) {
if (changeTime_ms > lastTimeChangeTime_ms && value != time) {
time = value
lastTimeChangeTime_ms = changeTime_ms
onTimeChanged.emit(value)
}
}
private var lastDurationChangeTime_ms: Long = 0
var duration: Double = 0.0
private set
protected fun setDuration(value: Double, changeTime_ms: Long = System.currentTimeMillis()) {
if (changeTime_ms > lastDurationChangeTime_ms && value != duration) {
duration = value
lastDurationChangeTime_ms = changeTime_ms
onDurationChanged.emit(value)
}
}
private var lastVolumeChangeTime_ms: Long = 0
var volume: Double = 1.0
private set
protected fun setVolume(value: Double, changeTime_ms: Long = System.currentTimeMillis()) {
if (changeTime_ms > lastVolumeChangeTime_ms && value != volume) {
volume = value
lastVolumeChangeTime_ms = changeTime_ms
onVolumeChanged.emit(value)
}
}
private var lastSpeedChangeTime_ms: Long = 0
var speed: Double = 1.0
private set
protected fun setSpeed(value: Double, changeTime_ms: Long = System.currentTimeMillis()) {
if (changeTime_ms > lastSpeedChangeTime_ms && value != speed) {
speed = value
lastSpeedChangeTime_ms = changeTime_ms
onSpeedChanged.emit(value)
}
}
val expectedCurrentTime: Double
get() {
val diff = if (isPlaying) ((System.currentTimeMillis() - lastTimeChangeTime_ms).toDouble() / 1000.0) else 0.0;
return time + diff;
};
var connectionState: CastConnectionState = CastConnectionState.DISCONNECTED
set(value) {
val changed = value != field;
field = value;
if (changed) {
onConnectionStateChanged.emit(value);
}
};
var onConnectionStateChanged = Event1<CastConnectionState>();
var onPlayChanged = Event1<Boolean>();
var onTimeChanged = Event1<Double>();
var onDurationChanged = Event1<Double>();
var onVolumeChanged = Event1<Double>();
var onSpeedChanged = Event1<Double>();
abstract fun stopCasting();
abstract fun seekVideo(timeSeconds: Double);
abstract fun stopVideo();
abstract fun pauseVideo();
abstract fun resumeVideo();
abstract fun loadVideo(streamType: String, contentType: String, contentId: String, resumePosition: Double, duration: Double, speed: Double?);
abstract fun loadContent(contentType: String, content: String, resumePosition: Double, duration: Double, speed: Double?);
open fun changeVolume(volume: Double) { throw NotImplementedError() }
open fun changeSpeed(speed: Double) { throw NotImplementedError() }
abstract fun start();
abstract fun stop();
abstract fun getDeviceInfo(): CastingDeviceInfo;
abstract fun getAddresses(): List<InetAddress>;
}
@@ -1,271 +0,0 @@
package com.futo.platformplayer.casting
import android.os.Build
import com.futo.platformplayer.BuildConfig
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.CastingDeviceInfo
import org.fcast.sender_sdk.ApplicationInfo
import org.fcast.sender_sdk.GenericKeyEvent
import org.fcast.sender_sdk.GenericMediaEvent
import org.fcast.sender_sdk.PlaybackState
import org.fcast.sender_sdk.Source
import java.net.InetAddress
import org.fcast.sender_sdk.CastingDevice as RsCastingDevice;
import org.fcast.sender_sdk.DeviceEventHandler as RsDeviceEventHandler;
import org.fcast.sender_sdk.DeviceConnectionState
import org.fcast.sender_sdk.DeviceFeature
import org.fcast.sender_sdk.IpAddr
import org.fcast.sender_sdk.LoadRequest
import org.fcast.sender_sdk.Metadata
import org.fcast.sender_sdk.ProtocolType
import org.fcast.sender_sdk.urlFormatIpAddr
import java.net.Inet4Address
import java.net.Inet6Address
private fun ipAddrToInetAddress(addr: IpAddr): InetAddress = when (addr) {
is IpAddr.V4 -> Inet4Address.getByAddress(
byteArrayOf(
addr.o1.toByte(),
addr.o2.toByte(),
addr.o3.toByte(),
addr.o4.toByte()
)
)
is IpAddr.V6 -> Inet6Address.getByAddress(
byteArrayOf(
addr.o1.toByte(),
addr.o2.toByte(),
addr.o3.toByte(),
addr.o4.toByte(),
addr.o5.toByte(),
addr.o6.toByte(),
addr.o7.toByte(),
addr.o8.toByte(),
addr.o9.toByte(),
addr.o10.toByte(),
addr.o11.toByte(),
addr.o12.toByte(),
addr.o13.toByte(),
addr.o14.toByte(),
addr.o15.toByte(),
addr.o16.toByte()
)
)
}
class CastingDeviceExp(val device: RsCastingDevice) : CastingDevice() {
class EventHandler : RsDeviceEventHandler {
var onConnectionStateChanged = Event1<DeviceConnectionState>();
var onPlayChanged = Event1<Boolean>()
var onTimeChanged = Event1<Double>()
var onDurationChanged = Event1<Double>()
var onVolumeChanged = Event1<Double>()
var onSpeedChanged = Event1<Double>()
override fun connectionStateChanged(state: DeviceConnectionState) {
onConnectionStateChanged.emit(state)
}
override fun volumeChanged(volume: Double) {
onVolumeChanged.emit(volume)
}
override fun timeChanged(time: Double) {
onTimeChanged.emit(time)
}
override fun playbackStateChanged(state: PlaybackState) {
onPlayChanged.emit(state == PlaybackState.PLAYING)
}
override fun durationChanged(duration: Double) {
onDurationChanged.emit(duration)
}
override fun speedChanged(speed: Double) {
onSpeedChanged.emit(speed)
}
override fun sourceChanged(source: Source) {
// TODO
}
override fun keyEvent(event: GenericKeyEvent) {
// Unreachable
}
override fun mediaEvent(event: GenericMediaEvent) {
// Unreachable
}
override fun playbackError(message: String) {
Logger.e(TAG, "Playback error: $message")
}
}
val eventHandler = EventHandler()
override val isReady: Boolean
get() = device.isReady()
override val name: String
get() = device.name()
override var usedRemoteAddress: InetAddress? = null
override var localAddress: InetAddress? = null
override fun canSetVolume(): Boolean = device.supportsFeature(DeviceFeature.SET_VOLUME)
override fun canSetSpeed(): Boolean = device.supportsFeature(DeviceFeature.SET_SPEED)
override val onConnectionStateChanged =
Event1<CastConnectionState>()
override val onPlayChanged: Event1<Boolean>
get() = eventHandler.onPlayChanged
override val onTimeChanged: Event1<Double>
get() = eventHandler.onTimeChanged
override val onDurationChanged: Event1<Double>
get() = eventHandler.onDurationChanged
override val onVolumeChanged: Event1<Double>
get() = eventHandler.onVolumeChanged
override val onSpeedChanged: Event1<Double>
get() = eventHandler.onSpeedChanged
override fun resumePlayback() = device.resumePlayback()
override fun pausePlayback() = device.pausePlayback()
override fun stopPlayback() = device.stopPlayback()
override fun seekTo(timeSeconds: Double) = device.seek(timeSeconds)
override fun changeVolume(newVolume: Double) {
device.changeVolume(newVolume)
volume = newVolume
}
override fun changeSpeed(speed: Double) = device.changeSpeed(speed)
override fun connect() = device.connect(
ApplicationInfo(
"Grayjay Android",
"${BuildConfig.VERSION_NAME}-${BuildConfig.FLAVOR}",
"${Build.MANUFACTURER} ${Build.MODEL}"
),
eventHandler,
1000.toULong()
)
override fun disconnect() = device.disconnect()
override fun getDeviceInfo(): CastingDeviceInfo {
val info = device.getDeviceInfo()
return CastingDeviceInfo(
info.name,
when (info.protocol) {
ProtocolType.CHROMECAST -> CastProtocolType.CHROMECAST
ProtocolType.F_CAST -> CastProtocolType.FCAST
},
addresses = info.addresses.map { urlFormatIpAddr(it) }.toTypedArray(),
port = info.port.toInt(),
)
}
override fun getAddresses(): List<InetAddress> = device.getAddresses().map {
ipAddrToInetAddress(it)
}
override fun loadVideo(
streamType: String,
contentType: String,
contentId: String,
resumePosition: Double,
duration: Double,
speed: Double?,
metadata: Metadata?
) = device.load(
LoadRequest.Video(
contentType = contentType,
url = contentId,
resumePosition = resumePosition,
speed = speed,
volume = volume,
metadata = metadata
)
)
override fun loadContent(
contentType: String,
content: String,
resumePosition: Double,
duration: Double,
speed: Double?,
metadata: Metadata?
) = device.load(
LoadRequest.Content(
contentType = contentType,
content = content,
resumePosition = resumePosition,
speed = speed,
volume = volume,
metadata = metadata,
)
)
override var connectionState = CastConnectionState.DISCONNECTED
override val protocolType: CastProtocolType
get() = when (device.castingProtocol()) {
ProtocolType.CHROMECAST -> CastProtocolType.CHROMECAST
ProtocolType.F_CAST -> CastProtocolType.FCAST
}
override var volume: Double = 1.0
override var duration: Double = 0.0
private var lastTimeChangeTime_ms: Long = 0
override var time: Double = 0.0
override var speed: Double = 0.0
override var isPlaying: Boolean = false
override val expectedCurrentTime: Double
get() {
val diff =
if (isPlaying) ((System.currentTimeMillis() - lastTimeChangeTime_ms).toDouble() / 1000.0) else 0.0;
return time + diff
}
init {
eventHandler.onConnectionStateChanged.subscribe { newState ->
when (newState) {
is DeviceConnectionState.Connected -> {
usedRemoteAddress = ipAddrToInetAddress(newState.usedRemoteAddr)
localAddress = ipAddrToInetAddress(newState.localAddr)
connectionState = CastConnectionState.CONNECTED
onConnectionStateChanged.emit(CastConnectionState.CONNECTED)
}
DeviceConnectionState.Connecting, DeviceConnectionState.Reconnecting -> {
connectionState = CastConnectionState.CONNECTING
onConnectionStateChanged.emit(CastConnectionState.CONNECTING)
}
DeviceConnectionState.Disconnected -> {
connectionState = CastConnectionState.CONNECTING
onConnectionStateChanged.emit(CastConnectionState.DISCONNECTED)
}
}
if (newState == DeviceConnectionState.Disconnected) {
try {
Logger.i(TAG, "Stopping device")
device.disconnect()
} catch (e: Throwable) {
Logger.e(TAG, "Failed to stop device: $e")
}
}
}
eventHandler.onPlayChanged.subscribe { isPlaying = it }
eventHandler.onTimeChanged.subscribe {
lastTimeChangeTime_ms = System.currentTimeMillis()
time = it
}
eventHandler.onDurationChanged.subscribe { duration = it }
eventHandler.onVolumeChanged.subscribe { volume = it }
eventHandler.onSpeedChanged.subscribe { speed = it }
}
override fun ensureThreadStarted() {}
companion object {
private val TAG = "CastingDeviceExp"
}
}
@@ -1,242 +0,0 @@
package com.futo.platformplayer.casting
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.models.CastingDeviceInfo
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import org.fcast.sender_sdk.Metadata
import java.net.InetAddress
enum class CastConnectionState {
DISCONNECTED,
CONNECTING,
CONNECTED
}
@Serializable(with = CastProtocolType.CastProtocolTypeSerializer::class)
enum class CastProtocolType {
CHROMECAST,
AIRPLAY,
FCAST;
object CastProtocolTypeSerializer : KSerializer<CastProtocolType> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("CastProtocolType", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: CastProtocolType) {
encoder.encodeString(value.name)
}
override fun deserialize(decoder: Decoder): CastProtocolType {
val name = decoder.decodeString()
return when (name) {
"FASTCAST" -> FCAST // Handle the renamed case
else -> CastProtocolType.valueOf(name)
}
}
}
}
abstract class CastingDeviceLegacy {
abstract val protocol: CastProtocolType;
abstract val isReady: Boolean;
abstract var usedRemoteAddress: InetAddress?;
abstract var localAddress: InetAddress?;
abstract val canSetVolume: Boolean;
abstract val canSetSpeed: Boolean;
var name: String? = null;
var isPlaying: Boolean = false
set(value) {
val changed = value != field;
field = value;
if (changed) {
onPlayChanged.emit(value);
}
};
private var lastTimeChangeTime_ms: Long = 0
var time: Double = 0.0
private set
protected fun setTime(value: Double, changeTime_ms: Long = System.currentTimeMillis()) {
if (changeTime_ms > lastTimeChangeTime_ms && value != time) {
time = value
lastTimeChangeTime_ms = changeTime_ms
onTimeChanged.emit(value)
}
}
private var lastDurationChangeTime_ms: Long = 0
var duration: Double = 0.0
private set
protected fun setDuration(value: Double, changeTime_ms: Long = System.currentTimeMillis()) {
if (changeTime_ms > lastDurationChangeTime_ms && value != duration) {
duration = value
lastDurationChangeTime_ms = changeTime_ms
onDurationChanged.emit(value)
}
}
private var lastVolumeChangeTime_ms: Long = 0
var volume: Double = 1.0
private set
protected fun setVolume(value: Double, changeTime_ms: Long = System.currentTimeMillis()) {
if (changeTime_ms > lastVolumeChangeTime_ms && value != volume) {
volume = value
lastVolumeChangeTime_ms = changeTime_ms
onVolumeChanged.emit(value)
}
}
private var lastSpeedChangeTime_ms: Long = 0
var speed: Double = 1.0
private set
protected fun setSpeed(value: Double, changeTime_ms: Long = System.currentTimeMillis()) {
if (changeTime_ms > lastSpeedChangeTime_ms && value != speed) {
speed = value
lastSpeedChangeTime_ms = changeTime_ms
onSpeedChanged.emit(value)
}
}
val expectedCurrentTime: Double
get() {
val diff =
if (isPlaying) ((System.currentTimeMillis() - lastTimeChangeTime_ms).toDouble() / 1000.0) else 0.0;
return time + diff;
};
var connectionState: CastConnectionState = CastConnectionState.DISCONNECTED
set(value) {
val changed = value != field;
field = value;
if (changed) {
onConnectionStateChanged.emit(value);
}
};
var onConnectionStateChanged = Event1<CastConnectionState>();
var onPlayChanged = Event1<Boolean>();
var onTimeChanged = Event1<Double>();
var onDurationChanged = Event1<Double>();
var onVolumeChanged = Event1<Double>();
var onSpeedChanged = Event1<Double>();
abstract fun stopCasting();
abstract fun seekVideo(timeSeconds: Double);
abstract fun stopVideo();
abstract fun pauseVideo();
abstract fun resumeVideo();
abstract fun loadVideo(
streamType: String,
contentType: String,
contentId: String,
resumePosition: Double,
duration: Double,
speed: Double?
);
abstract fun loadContent(
contentType: String,
content: String,
resumePosition: Double,
duration: Double,
speed: Double?
);
open fun changeVolume(volume: Double) {
throw NotImplementedError()
}
open fun changeSpeed(speed: Double) {
throw NotImplementedError()
}
abstract fun start();
abstract fun stop();
abstract fun getDeviceInfo(): CastingDeviceInfo;
abstract fun getAddresses(): List<InetAddress>;
}
class CastingDeviceLegacyWrapper(val inner: CastingDeviceLegacy) : CastingDevice() {
override val isReady: Boolean get() = inner.isReady
override val usedRemoteAddress: InetAddress? get() = inner.usedRemoteAddress
override val localAddress: InetAddress? get() = inner.localAddress
override val name: String? get() = inner.name
override val onConnectionStateChanged: Event1<CastConnectionState> get() = inner.onConnectionStateChanged
override val onPlayChanged: Event1<Boolean> get() = inner.onPlayChanged
override val onTimeChanged: Event1<Double> get() = inner.onTimeChanged
override val onDurationChanged: Event1<Double> get() = inner.onDurationChanged
override val onVolumeChanged: Event1<Double> get() = inner.onVolumeChanged
override val onSpeedChanged: Event1<Double> get() = inner.onSpeedChanged
override var connectionState: CastConnectionState
get() = inner.connectionState
set(_) = Unit
override val protocolType: CastProtocolType get() = inner.protocol
override var isPlaying: Boolean
get() = inner.isPlaying
set(_) = Unit
override val expectedCurrentTime: Double
get() = inner.expectedCurrentTime
override var speed: Double
get() = inner.speed
set(_) = Unit
override var time: Double
get() = inner.time
set(_) = Unit
override var duration: Double
get() = inner.duration
set(_) = Unit
override var volume: Double
get() = inner.volume
set(_) = Unit
override fun canSetVolume(): Boolean = inner.canSetVolume
override fun canSetSpeed(): Boolean = inner.canSetSpeed
override fun resumePlayback() = inner.resumeVideo()
override fun pausePlayback() = inner.pauseVideo()
override fun stopPlayback() = inner.stopVideo()
override fun seekTo(timeSeconds: Double) = inner.seekVideo(timeSeconds)
override fun changeVolume(timeSeconds: Double) = inner.changeVolume(timeSeconds)
override fun changeSpeed(speed: Double) = inner.changeSpeed(speed)
override fun connect() = inner.start()
override fun disconnect() = inner.stop()
override fun getDeviceInfo(): CastingDeviceInfo = inner.getDeviceInfo()
override fun getAddresses(): List<InetAddress> = inner.getAddresses()
override fun loadVideo(
streamType: String,
contentType: String,
contentId: String,
resumePosition: Double,
duration: Double,
speed: Double?,
metadata: Metadata?
) = inner.loadVideo(streamType, contentType, contentId, resumePosition, duration, speed)
override fun loadContent(
contentType: String,
content: String,
resumePosition: Double,
duration: Double,
speed: Double?,
metadata: Metadata?
) = inner.loadContent(contentType, content, resumePosition, duration, speed)
override fun ensureThreadStarted() = when (inner) {
is FCastCastingDevice -> inner.ensureThreadStarted()
is ChromecastCastingDevice -> inner.ensureThreadsStarted()
else -> {}
}
}
@@ -27,7 +27,7 @@ import javax.net.ssl.SSLSocket
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager
class ChromecastCastingDevice : CastingDeviceLegacy {
class ChromecastCastingDevice : CastingDevice {
//See for more info: https://developers.google.com/cast/docs/media/messages
override val protocol: CastProtocolType get() = CastProtocolType.CHROMECAST;
@@ -3,6 +3,7 @@ package com.futo.platformplayer.casting
import android.os.Looper
import android.util.Base64
import android.util.Log
import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.casting.models.FCastDecryptedMessage
import com.futo.platformplayer.casting.models.FCastEncryptedMessage
@@ -24,6 +25,7 @@ import com.futo.platformplayer.toInetAddress
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 kotlinx.serialization.encodeToString
@@ -32,6 +34,7 @@ import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.math.BigInteger
import java.net.Inet4Address
import java.net.InetAddress
import java.net.InetSocketAddress
import java.net.Socket
@@ -69,7 +72,7 @@ enum class Opcode(val value: Byte) {
}
}
class FCastCastingDevice : CastingDeviceLegacy {
class FCastCastingDevice : CastingDevice {
//See for more info: TODO
override val protocol: CastProtocolType get() = CastProtocolType.FCAST;
File diff suppressed because it is too large Load Diff
@@ -1,174 +0,0 @@
package com.futo.platformplayer.casting
import android.content.Context
import android.util.Log
import com.futo.platformplayer.BuildConfig
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.CastingDeviceInfo
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import org.fcast.sender_sdk.DeviceInfo as RsDeviceInfo
import org.fcast.sender_sdk.ProtocolType
import org.fcast.sender_sdk.CastContext
import org.fcast.sender_sdk.NsdDeviceDiscoverer
class StateCastingExp : StateCasting() {
private val _context = CastContext()
var _deviceDiscoverer: NsdDeviceDiscoverer? = null
class DiscoveryEventHandler(
private val onDeviceAdded: (RsDeviceInfo) -> Unit,
private val onDeviceRemoved: (String) -> Unit,
private val onDeviceUpdated: (RsDeviceInfo) -> Unit,
) : org.fcast.sender_sdk.DeviceDiscovererEventHandler {
override fun deviceAvailable(deviceInfo: RsDeviceInfo) {
onDeviceAdded(deviceInfo)
}
override fun deviceChanged(deviceInfo: RsDeviceInfo) {
onDeviceUpdated(deviceInfo)
}
override fun deviceRemoved(deviceName: String) {
onDeviceRemoved(deviceName)
}
}
init {
if (BuildConfig.DEBUG) {
org.fcast.sender_sdk.initLogger(org.fcast.sender_sdk.LogLevelFilter.DEBUG)
}
}
override fun handleUrl(url: String) {
try {
val foundDeviceInfo = org.fcast.sender_sdk.deviceInfoFromUrl(url)!!
val foundDevice = _context.createDeviceFromInfo(foundDeviceInfo)
connectDevice(CastingDeviceExp(foundDevice))
} catch (e: Throwable) {
Logger.e(TAG, "Failed to handle URL: $e")
}
}
override fun onStop() {
val ad = activeDevice ?: return
_resumeCastingDevice = ad.getDeviceInfo()
Log.i(TAG, "_resumeCastingDevice set to '${ad.name}'")
Logger.i(TAG, "Stopping active device because of onStop.")
try {
ad.disconnect()
} catch (e: Throwable) {
Logger.e(TAG, "Failed to disconnect from device: $e")
}
}
@Synchronized
override fun start(context: Context) {
if (_started)
return
_started = true
Log.i(TAG, "_resumeCastingDevice set null start")
_resumeCastingDevice = null
Logger.i(TAG, "CastingService starting...")
_castServer.start()
enableDeveloper(true)
Logger.i(TAG, "CastingService started.")
_deviceDiscoverer = NsdDeviceDiscoverer(
context,
DiscoveryEventHandler(
{ deviceInfo -> // Added
Logger.i(TAG, "Device added: ${deviceInfo.name}")
val device = _context.createDeviceFromInfo(deviceInfo)
val deviceHandle = CastingDeviceExp(device)
devices[deviceHandle.device.name()] = deviceHandle
invokeInMainScopeIfRequired {
onDeviceAdded.emit(deviceHandle)
}
},
{ deviceName -> // Removed
invokeInMainScopeIfRequired {
if (devices.containsKey(deviceName)) {
val device = devices.remove(deviceName)
if (device != null) {
onDeviceRemoved.emit(device)
}
}
}
},
{ deviceInfo -> // Updated
Logger.i(TAG, "Device updated: $deviceInfo")
val handle = devices[deviceInfo.name]
if (handle != null && handle is CastingDeviceExp) {
handle.device.setPort(deviceInfo.port)
handle.device.setAddresses(deviceInfo.addresses)
invokeInMainScopeIfRequired {
onDeviceChanged.emit(handle)
}
}
},
)
)
}
@Synchronized
override fun stop() {
if (!_started) {
return
}
_started = false
Logger.i(TAG, "CastingService stopping.")
_scopeIO.cancel()
_scopeMain.cancel()
Logger.i(TAG, "Stopping active device because StateCasting is being stopped.")
val d = activeDevice
activeDevice = null
try {
d?.disconnect()
} catch (e: Throwable) {
Logger.e(TAG, "Failed to disconnect device: $e")
}
_castServer.stop()
_castServer.removeAllHandlers()
Logger.i(TAG, "CastingService stopped.")
_deviceDiscoverer = null
}
override fun startUpdateTimeJob(
onTimeJobTimeChanged_s: Event1<Long>,
setTime: (Long) -> Unit
): Job? = null
override fun deviceFromInfo(deviceInfo: CastingDeviceInfo): CastingDeviceExp {
val rsAddrs =
deviceInfo.addresses.map { org.fcast.sender_sdk.tryIpAddrFromStr(it) } // Throws!
val rsDeviceInfo = RsDeviceInfo(
name = deviceInfo.name,
protocol = when (deviceInfo.type) {
com.futo.platformplayer.casting.CastProtocolType.CHROMECAST -> ProtocolType.CHROMECAST
com.futo.platformplayer.casting.CastProtocolType.FCAST -> ProtocolType.F_CAST
else -> throw IllegalArgumentException()
},
addresses = rsAddrs,
port = deviceInfo.port.toUShort(),
)
return CastingDeviceExp(_context.createDeviceFromInfo(rsDeviceInfo))
}
companion object {
private val TAG = "StateCastingExp"
}
}
@@ -1,397 +0,0 @@
package com.futo.platformplayer.casting
import android.content.Context
import android.net.Uri
import android.net.nsd.NsdManager
import android.net.nsd.NsdServiceInfo
import android.os.Build
import android.util.Base64
import android.util.Log
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.CastingDeviceInfo
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import java.net.InetAddress
import kotlinx.coroutines.delay
class StateCastingLegacy : StateCasting() {
private var _nsdManager: NsdManager? = null
private val _discoveryListeners = mapOf(
"_googlecast._tcp" to createDiscoveryListener(::addOrUpdateChromeCastDevice),
"_airplay._tcp" to createDiscoveryListener(::addOrUpdateAirPlayDevice),
"_fastcast._tcp" to createDiscoveryListener(::addOrUpdateFastCastDevice),
"_fcast._tcp" to createDiscoveryListener(::addOrUpdateFastCastDevice)
)
override fun handleUrl(url: String) {
val uri = Uri.parse(url)
if (uri.scheme != "fcast") {
throw Exception("Expected scheme to be FCast")
}
val type = uri.host
if (type != "r") {
throw Exception("Expected type r")
}
val connectionInfo = uri.pathSegments[0]
val json =
Base64.decode(connectionInfo, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP)
.toString(Charsets.UTF_8)
val networkConfig = Json.decodeFromString<FCastNetworkConfig>(json)
val tcpService = networkConfig.services.first { v -> v.type == 0 }
val foundInfo = addRememberedDevice(
CastingDeviceInfo(
name = networkConfig.name,
type = CastProtocolType.FCAST,
addresses = networkConfig.addresses.toTypedArray(),
port = tcpService.port
)
)
connectDevice(deviceFromInfo(foundInfo))
}
override fun onStop() {
val ad = activeDevice ?: return;
_resumeCastingDevice = ad.getDeviceInfo()
Log.i(TAG, "_resumeCastingDevice set to '${ad.name}'")
Logger.i(TAG, "Stopping active device because of onStop.");
ad.disconnect();
}
@Synchronized
override fun start(context: Context) {
if (_started)
return;
_started = true;
Log.i(TAG, "_resumeCastingDevice set null start")
_resumeCastingDevice = null;
Logger.i(TAG, "CastingService starting...");
_castServer.start();
enableDeveloper(true);
Logger.i(TAG, "CastingService started.");
_nsdManager = context.getSystemService(Context.NSD_SERVICE) as NsdManager
startDiscovering()
}
@Synchronized
private fun startDiscovering() {
_nsdManager?.apply {
_discoveryListeners.forEach {
discoverServices(it.key, NsdManager.PROTOCOL_DNS_SD, it.value)
}
}
}
@Synchronized
private fun stopDiscovering() {
_nsdManager?.apply {
_discoveryListeners.forEach {
try {
stopServiceDiscovery(it.value)
} catch (e: Throwable) {
Logger.w(TAG, "Failed to stop service discovery", e)
}
}
}
}
@Synchronized
override fun stop() {
if (!_started)
return;
_started = false;
Logger.i(TAG, "CastingService stopping.")
stopDiscovering()
_scopeIO.cancel();
_scopeMain.cancel();
Logger.i(TAG, "Stopping active device because StateCasting is being stopped.")
val d = activeDevice;
activeDevice = null;
d?.disconnect();
_castServer.stop();
_castServer.removeAllHandlers();
Logger.i(TAG, "CastingService stopped.")
_nsdManager = null
}
private fun createDiscoveryListener(addOrUpdate: (String, Array<InetAddress>, Int) -> Unit): NsdManager.DiscoveryListener {
return object : NsdManager.DiscoveryListener {
override fun onDiscoveryStarted(regType: String) {
Log.d(TAG, "Service discovery started for $regType")
}
override fun onDiscoveryStopped(serviceType: String) {
Log.i(TAG, "Discovery stopped: $serviceType")
}
override fun onServiceLost(service: NsdServiceInfo) {
Log.e(TAG, "service lost: $service")
// TODO: Handle service lost, e.g., remove device
}
override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {
Log.e(TAG, "Discovery failed for $serviceType: Error code:$errorCode")
try {
_nsdManager?.stopServiceDiscovery(this)
} catch (e: Throwable) {
Logger.w(TAG, "Failed to stop service discovery", e)
}
}
override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) {
Log.e(TAG, "Stop discovery failed for $serviceType: Error code:$errorCode")
try {
_nsdManager?.stopServiceDiscovery(this)
} catch (e: Throwable) {
Logger.w(TAG, "Failed to stop service discovery", e)
}
}
override fun onServiceFound(service: NsdServiceInfo) {
Log.v(TAG, "Service discovery success for ${service.serviceType}: $service")
val addresses = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
service.hostAddresses.toTypedArray()
} else {
arrayOf(service.host)
}
addOrUpdate(service.serviceName, addresses, service.port)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
_nsdManager?.registerServiceInfoCallback(
service,
{ it.run() },
object : NsdManager.ServiceInfoCallback {
override fun onServiceUpdated(serviceInfo: NsdServiceInfo) {
Log.v(TAG, "onServiceUpdated: $serviceInfo")
addOrUpdate(
serviceInfo.serviceName,
serviceInfo.hostAddresses.toTypedArray(),
serviceInfo.port
)
}
override fun onServiceLost() {
Log.v(TAG, "onServiceLost: $service")
// TODO: Handle service lost
}
override fun onServiceInfoCallbackRegistrationFailed(errorCode: Int) {
Log.v(TAG, "onServiceInfoCallbackRegistrationFailed: $errorCode")
}
override fun onServiceInfoCallbackUnregistered() {
Log.v(TAG, "onServiceInfoCallbackUnregistered")
}
})
} else {
_nsdManager?.resolveService(service, object : NsdManager.ResolveListener {
override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
Log.v(TAG, "Resolve failed: $errorCode")
}
override fun onServiceResolved(serviceInfo: NsdServiceInfo) {
Log.v(TAG, "Resolve Succeeded: $serviceInfo")
addOrUpdate(
serviceInfo.serviceName,
arrayOf(serviceInfo.host),
serviceInfo.port
)
}
})
}
}
}
}
override fun startUpdateTimeJob(
onTimeJobTimeChanged_s: Event1<Long>,
setTime: (Long) -> Unit
): Job? {
val d = activeDevice;
if (d is CastingDeviceLegacyWrapper && (d.inner is AirPlayCastingDevice || d.inner is ChromecastCastingDevice)) {
return _scopeMain.launch {
while (true) {
val device = instance.activeDevice
if (device == null || !device.isPlaying) {
break
}
delay(1000)
val time_ms = (device.expectedCurrentTime * 1000.0).toLong()
setTime(time_ms)
onTimeJobTimeChanged_s.emit(device.expectedCurrentTime.toLong())
}
}
}
return null
}
override fun deviceFromInfo(deviceInfo: CastingDeviceInfo): CastingDevice {
return CastingDeviceLegacyWrapper(
when (deviceInfo.type) {
CastProtocolType.CHROMECAST -> {
ChromecastCastingDevice(deviceInfo);
}
CastProtocolType.AIRPLAY -> {
AirPlayCastingDevice(deviceInfo);
}
CastProtocolType.FCAST -> {
FCastCastingDevice(deviceInfo);
}
}
)
}
private fun addOrUpdateChromeCastDevice(
name: String,
addresses: Array<InetAddress>,
port: Int
) {
return addOrUpdateCastDevice(
name,
deviceFactory = {
CastingDeviceLegacyWrapper(
ChromecastCastingDevice(
name,
addresses,
port
)
)
},
deviceUpdater = { d ->
if (d.isReady || d !is CastingDeviceLegacyWrapper || d.inner !is ChromecastCastingDevice) {
return@addOrUpdateCastDevice false;
}
val changed =
addresses.contentEquals(d.inner.addresses) || d.name != name || d.inner.port != port;
if (changed) {
d.inner.name = name;
d.inner.addresses = addresses;
d.inner.port = port;
}
return@addOrUpdateCastDevice changed;
}
);
}
private fun addOrUpdateAirPlayDevice(name: String, addresses: Array<InetAddress>, port: Int) {
return addOrUpdateCastDevice(
name,
deviceFactory = {
CastingDeviceLegacyWrapper(
AirPlayCastingDevice(
name,
addresses,
port
)
)
},
deviceUpdater = { d ->
if (d.isReady || d !is CastingDeviceLegacyWrapper || d.inner !is AirPlayCastingDevice) {
return@addOrUpdateCastDevice false;
}
val changed =
addresses.contentEquals(addresses) || d.name != name || d.inner.port != port;
if (changed) {
d.inner.name = name;
d.inner.port = port;
d.inner.addresses = addresses;
}
return@addOrUpdateCastDevice changed;
}
);
}
private fun addOrUpdateFastCastDevice(name: String, addresses: Array<InetAddress>, port: Int) {
return addOrUpdateCastDevice(
name,
deviceFactory = { CastingDeviceLegacyWrapper(FCastCastingDevice(name, addresses, port)) },
deviceUpdater = { d ->
if (d.isReady || d !is CastingDeviceLegacyWrapper || d.inner !is FCastCastingDevice) {
return@addOrUpdateCastDevice false;
}
val changed =
addresses.contentEquals(addresses) || d.name != name || d.inner.port != port;
if (changed) {
d.inner.name = name;
d.inner.port = port;
d.inner.addresses = addresses;
}
return@addOrUpdateCastDevice changed;
}
);
}
private inline fun addOrUpdateCastDevice(
name: String,
deviceFactory: () -> CastingDevice,
deviceUpdater: (device: CastingDevice) -> Boolean
) {
var invokeEvents: (() -> Unit)? = null;
synchronized(devices) {
val device = devices[name];
if (device != null) {
val changed = deviceUpdater(device);
if (changed) {
invokeEvents = {
onDeviceChanged.emit(device);
}
}
} else {
val newDevice = deviceFactory();
this.devices[name] = newDevice
invokeEvents = {
onDeviceAdded.emit(newDevice);
};
}
}
invokeEvents?.let { _scopeMain.launch { it(); }; };
}
@Serializable
private data class FCastNetworkConfig(
val name: String,
val addresses: List<String>,
val services: List<FCastService>
)
@Serializable
private data class FCastService(
val port: Int,
val type: Int
)
companion object {
private val TAG = "StateCastingLegacy"
}
}
@@ -8,13 +8,11 @@ import android.view.View
import android.view.WindowManager
import android.widget.*
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.casting.CastProtocolType
import com.futo.platformplayer.casting.StateCasting
import com.futo.platformplayer.models.CastingDeviceInfo
import com.futo.platformplayer.toInetAddress
import com.futo.platformplayer.logging.Logger
class CastingAddDialog(context: Context?) : AlertDialog(context) {
@@ -40,13 +38,7 @@ class CastingAddDialog(context: Context?) : AlertDialog(context) {
_buttonConfirm = findViewById(R.id.button_confirm);
_buttonTutorial = findViewById(R.id.button_tutorial)
val deviceTypeArray = if (Settings.instance.casting.experimentalCasting) {
R.array.exp_casting_device_type_array
} else {
R.array.casting_device_type_array
}
ArrayAdapter.createFromResource(context, deviceTypeArray, R.layout.spinner_item_simple).also { adapter ->
ArrayAdapter.createFromResource(context, R.array.casting_device_type_array, R.layout.spinner_item_simple).also { adapter ->
adapter.setDropDownViewResource(R.layout.spinner_dropdownitem_simple);
_spinnerType.adapter = adapter;
};
@@ -109,11 +101,7 @@ class CastingAddDialog(context: Context?) : AlertDialog(context) {
_textError.visibility = View.GONE;
val castingDeviceInfo = CastingDeviceInfo(name, castProtocolType, arrayOf(ip), port.toInt());
try {
StateCasting.instance.addRememberedDevice(castingDeviceInfo)
} catch (e: Throwable) {
Logger.e(TAG, "Failed to add remembered device: $e")
}
StateCasting.instance.addRememberedDevice(castingDeviceInfo);
performDismiss();
};
@@ -7,6 +7,7 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.widget.Button
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
@@ -17,6 +18,7 @@ import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.casting.CastConnectionState
import com.futo.platformplayer.casting.CastingDevice
import com.futo.platformplayer.casting.StateCasting
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
@@ -106,16 +108,15 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
synchronized(StateCasting.instance.devices) {
_devices.addAll(StateCasting.instance.devices.values.mapNotNull { it.name })
}
_rememberedDevices.addAll(StateCasting.instance.getRememberedCastingDeviceNames())
_rememberedDevices.addAll(StateCasting.instance.getRememberedCastingDeviceNames())
updateUnifiedList()
StateCasting.instance.onDeviceAdded.subscribe(this) { d ->
val name = d.name
if (name != null) {
if (name != null)
_devices.add(name)
updateUnifiedList()
}
updateUnifiedList()
}
StateCasting.instance.onDeviceChanged.subscribe(this) { d ->
@@ -12,11 +12,12 @@ import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.casting.AirPlayCastingDevice
import com.futo.platformplayer.casting.CastConnectionState
import com.futo.platformplayer.casting.CastProtocolType
import com.futo.platformplayer.casting.CastingDevice
import com.futo.platformplayer.casting.ChromecastCastingDevice
import com.futo.platformplayer.casting.FCastCastingDevice
import com.futo.platformplayer.casting.StateCasting
import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailFragment
import com.futo.platformplayer.logging.Logger
@@ -68,18 +69,18 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
_buttonPlay = findViewById(R.id.button_play);
_buttonPlay.setOnClickListener {
StateCasting.instance.resumeVideo()
StateCasting.instance.activeDevice?.resumeVideo()
}
_buttonPause = findViewById(R.id.button_pause);
_buttonPause.setOnClickListener {
StateCasting.instance.pauseVideo()
StateCasting.instance.activeDevice?.pauseVideo()
}
_buttonStop = findViewById(R.id.button_stop);
_buttonStop.setOnClickListener {
(ownerActivity as MainActivity?)?.getFragment<VideoDetailFragment>()?.closeVideoDetails()
StateCasting.instance.stopVideo()
StateCasting.instance.activeDevice?.stopVideo()
}
_buttonNext = findViewById(R.id.button_next);
@@ -89,11 +90,7 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
_buttonClose.setOnClickListener { dismiss(); };
_buttonDisconnect.setOnClickListener {
try {
StateCasting.instance.activeDevice?.disconnect()
} catch (e: Throwable) {
Logger.e(TAG, "Active device failed to disconnect: $e")
}
StateCasting.instance.activeDevice?.stopCasting();
dismiss();
};
@@ -102,7 +99,12 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
return@OnChangeListener
}
StateCasting.instance.videoSeekTo(value.toDouble())
val activeDevice = StateCasting.instance.activeDevice ?: return@OnChangeListener;
try {
activeDevice.seekVideo(value.toDouble());
} catch (e: Throwable) {
Logger.e(TAG, "Failed to change volume.", e);
}
});
//TODO: Check if volume slider is properly hidden in all cases
@@ -111,7 +113,14 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
return@OnChangeListener
}
StateCasting.instance.changeVolume(value.toDouble())
val activeDevice = StateCasting.instance.activeDevice ?: return@OnChangeListener;
if (activeDevice.canSetVolume) {
try {
activeDevice.changeVolume(value.toDouble());
} catch (e: Throwable) {
Logger.e(TAG, "Failed to change volume.", e);
}
}
});
setLoading(false);
@@ -163,25 +172,15 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
private fun updateDevice() {
val d = StateCasting.instance.activeDevice ?: return;
when (d.protocolType) {
CastProtocolType.CHROMECAST -> {
_imageDevice.setImageResource(R.drawable.ic_chromecast);
_textType.text = "Chromecast";
}
CastProtocolType.AIRPLAY -> {
_imageDevice.setImageResource(R.drawable.ic_airplay);
_textType.text = "AirPlay";
}
CastProtocolType.FCAST -> {
_imageDevice.setImageResource(
if (Settings.instance.casting.experimentalCasting) {
R.drawable.ic_exp_fc
} else {
R.drawable.ic_fc
}
)
_textType.text = "FCast";
}
if (d is ChromecastCastingDevice) {
_imageDevice.setImageResource(R.drawable.ic_chromecast);
_textType.text = "Chromecast";
} else if (d is AirPlayCastingDevice) {
_imageDevice.setImageResource(R.drawable.ic_airplay);
_textType.text = "AirPlay";
} else if (d is FCastCastingDevice) {
_imageDevice.setImageResource(R.drawable.ic_fc);
_textType.text = "FastCast";
}
_textName.text = d.name;
@@ -193,7 +192,7 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
_sliderPosition.value = d.time.toFloat().coerceAtLeast(0.0f).coerceAtMost(dur)
_sliderPosition.valueTo = dur
if (d.canSetVolume()) {
if (d.canSetVolume) {
_layoutVolumeAdjustable.visibility = View.VISIBLE;
_layoutVolumeFixed.visibility = View.GONE;
} else {
@@ -215,7 +214,8 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
CastConnectionState.CONNECTED -> {
enableControls(interactiveControls)
}
CastConnectionState.CONNECTING, CastConnectionState.DISCONNECTED -> {
CastConnectionState.CONNECTING,
CastConnectionState.DISCONNECTED -> {
disableControls(interactiveControls)
}
}
@@ -242,12 +242,10 @@ class V8Plugin {
}
fun <T> busy(handle: ()->T): T {
_busyLock.lock();
//Logger.i(TAG, "Busy Enter [" + _busyLock.holdCount + "]" + Thread.currentThread().stackTrace.drop(3)?.firstOrNull()?.toString() + ", " + Thread.currentThread().stackTrace.drop(4)?.firstOrNull()?.toString() + ", " + Thread.currentThread().stackTrace.drop(5)?.firstOrNull()?.toString())
try {
return handle();
}
finally {
//Logger.i(TAG, "Busy Leave [" + _busyLock.holdCount + "]" + Thread.currentThread().stackTrace.drop(3)?.firstOrNull()?.toString() + ", " + Thread.currentThread().stackTrace.drop(4)?.firstOrNull()?.toString()+ ", " + Thread.currentThread().stackTrace.drop(5)?.firstOrNull()?.toString())
_busyLock.unlock();
}
/*
@@ -199,7 +199,7 @@ class ChannelFragment : MainFragment() {
when (v) {
is IPlatformVideo -> {
StatePlayer.instance.clearQueue()
fragment.navigate<VideoDetailFragment>(v).maximizeVideoDetail()
fragment.navigate<VideoDetailFragment>(v).maximizeVideoDetail(false)
}
is IPlatformPlaylist -> {
@@ -245,7 +245,7 @@ class ChannelFragment : MainFragment() {
when (contentType) {
ContentType.MEDIA -> {
StatePlayer.instance.clearQueue()
fragment.navigate<VideoDetailFragment>(url).maximizeVideoDetail()
fragment.navigate<VideoDetailFragment>(url).maximizeVideoDetail(false)
}
ContentType.URL -> fragment.navigate<BrowserFragment>(url)
@@ -197,9 +197,9 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
StatePlayer.instance.insertToQueue(content, true);
} else {
if (Settings.instance.playback.shouldResumePreview(time))
fragment.navigate<VideoDetailFragment>(content.withTimestamp(time)).maximizeVideoDetail();
fragment.navigate<VideoDetailFragment>(content.withTimestamp(time)).maximizeVideoDetail(false);
else
fragment.navigate<VideoDetailFragment>(content).maximizeVideoDetail();
fragment.navigate<VideoDetailFragment>(content).maximizeVideoDetail(false);
}
} else if (content is IPlatformPlaylist) {
fragment.navigate<RemotePlaylistFragment>(content);
@@ -218,7 +218,7 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
when(contentType) {
ContentType.MEDIA -> {
StatePlayer.instance.clearQueue()
fragment.navigate<VideoDetailFragment>(url).maximizeVideoDetail()
fragment.navigate<VideoDetailFragment>(url).maximizeVideoDetail(false)
}
ContentType.PLAYLIST -> fragment.navigate<RemotePlaylistFragment>(url)
ContentType.URL -> fragment.navigate<BrowserFragment>(url)
@@ -174,7 +174,7 @@ class DownloadsFragment : MainFragment() {
.asAnyWithTop(findViewById(R.id.downloads_top)) {
it.onClick.subscribe {
StatePlayer.instance.clearQueue();
_frag.navigate<VideoDetailFragment>(it).maximizeVideoDetail();
_frag.navigate<VideoDetailFragment>(it).maximizeVideoDetail(false);
}
};
@@ -247,7 +247,7 @@ class HistoryFragment : MainFragment() {
val diff = v.video.duration - v.position;
val vid: Any = if (diff > 5) { v.video.withTimestamp(v.position) } else { v.video };
StatePlayer.instance.clearQueue();
_fragment.navigate<VideoDetailFragment>(vid).maximizeVideoDetail();
_fragment.navigate<VideoDetailFragment>(vid).maximizeVideoDetail(false);
_editSearch.clearFocus();
inputMethodManager.hideSoftInputFromWindow(_editSearch.windowToken, 0);
@@ -279,14 +279,6 @@ class HomeFragment : MainFragment() {
else {
view.setToggle(!active);
}
}, { view, views, enabled ->
val toDisable = views.filter { it != view && it.tag == "plugins" };
if(!view.isActive)
view.handleClick();
for(tag in toDisable) {
if(tag.isActive)
tag.handleClick();
}
}).withTag("plugins")
})
else listOf())
@@ -34,13 +34,12 @@ import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.PlatformVideoWithTime
import com.futo.platformplayer.models.UrlVideoWithTime
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.views.containers.SingleViewTouchableMotionLayout
import com.futo.platformplayer.views.containers.CustomMotionLayout
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
//region Fragment
@UnstableApi
class VideoDetailFragment() : MainFragment() {
@@ -51,8 +50,8 @@ class VideoDetailFragment() : MainFragment() {
private var _isActive: Boolean = false;
private var _viewDetail : VideoDetailView? = null;
private var _view : SingleViewTouchableMotionLayout? = null;
private var _viewDetail : VideoDetailView? = null
private var _motionLayout: CustomMotionLayout? = null
var isFullscreen : Boolean = false;
/**
@@ -61,8 +60,6 @@ class VideoDetailFragment() : MainFragment() {
*/
var isMinimizingFromFullScreen : Boolean = false;
val onFullscreenChanged = Event1<Boolean>();
var isTransitioning : Boolean = false
private set;
var isInPictureInPicture : Boolean = false
private set;
@@ -78,13 +75,8 @@ class VideoDetailFragment() : MainFragment() {
val currentUrl get() = _viewDetail?.currentUrl;
val onMinimize = Event0();
val onTransitioning = Event1<Boolean>();
val onMaximized = Event0();
private var _isInitialMaximize = true;
private val _maximizeProgress get() = _view?.progress ?: 0.0f;
private var _loadUrlOnCreate: UrlVideoWithTime? = null;
private var _leavingPiP = false;
@@ -295,22 +287,17 @@ class VideoDetailFragment() : MainFragment() {
fun minimizeVideoDetail() {
_viewDetail?.setFullscreen(false);
if(_view != null)
_view!!.transitionToStart();
_motionLayout?.transitionToState(R.id.collapsed)
}
fun maximizeVideoDetail(instant: Boolean = false) {
if((_maximizeProgress > 0.9f || instant) && state != State.MAXIMIZED) {
state = State.MAXIMIZED;
onMaximized.emit();
fun maximizeVideoDetail(instant: Boolean) {
state = State.MAXIMIZED
onMaximized.emit()
if(instant) {
_motionLayout?.setTransition(R.id.maximize)
_motionLayout?.progress = 1f
} else {
_motionLayout?.transitionToState(R.id.expanded)
}
_view?.let {
if(!instant) {
it.transitionToEnd();
} else {
it.progress = 1f;
onTransitioning.emit(true);
}
};
}
fun closeVideoDetails() {
Logger.i(TAG, "closeVideoDetails()")
@@ -323,83 +310,48 @@ class VideoDetailFragment() : MainFragment() {
}
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_view = inflater.inflate(R.layout.fragment_video_detail, container, false) as SingleViewTouchableMotionLayout;
_viewDetail = _view!!.findViewById<VideoDetailView>(R.id.fragview_videodetail).also {
it.applyFragment(this);
it.onFullscreenChanged.subscribe(::onFullscreenChanged);
it.onVideoChanged.subscribe(::onVideoChanged)
it.onMinimize.subscribe {
isMinimizingFromFullScreen = true
_view!!.transitionToStart();
};
it.onClose.subscribe {
Logger.i(TAG, "onClose")
closeVideoDetails();
};
it.onMaximize.subscribe { maximizeVideoDetail(it) };
it.onEnterPictureInPicture.subscribe {
Logger.i(TAG, "onEnterPictureInPicture")
isInPictureInPicture = true;
_viewDetail?.handleEnterPictureInPicture();
_viewDetail?.invalidate();
};
it.onTouchCancel.subscribe {
val v = _view ?: return@subscribe;
if (v.progress >= 0.5 && v.progress < 1) {
maximizeVideoDetail();
}
if (v.progress < 0.5 && v.progress > 0) {
minimizeVideoDetail();
}
};
}
_view!!.setTransitionListener(object : MotionLayout.TransitionListener {
override fun onTransitionChange(motionLayout: MotionLayout?, startId: Int, endId: Int, progress: Float) {
val viewDetail = VideoDetailView(this, inflater);
_motionLayout = viewDetail.findViewById(R.id.videodetail_root)
viewDetail.applyFragment(this);
viewDetail.onFullscreenChanged.subscribe(::onFullscreenChanged);
viewDetail.onVideoChanged.subscribe(::onVideoChanged)
viewDetail.onMinimize.subscribe {
isMinimizingFromFullScreen = true
_motionLayout?.transitionToState(R.id.collapsed)
};
viewDetail.onClose.subscribe {
Logger.i(TAG, "onClose")
closeVideoDetails();
};
viewDetail.onMaximize.subscribe { maximizeVideoDetail(it) };
viewDetail.onEnterPictureInPicture.subscribe {
Logger.i(TAG, "onEnterPictureInPicture")
isInPictureInPicture = true;
_viewDetail?.handleEnterPictureInPicture();
_viewDetail?.invalidate();
};
_motionLayout!!.addTransitionListener(object : MotionLayout.TransitionListener {
override fun onTransitionCompleted(motionLayout: MotionLayout?, currentId: Int) {
_viewDetail?.stopAllGestures()
if (state != State.MINIMIZED && progress < 0.1) {
state = State.MINIMIZED;
if (state != State.MINIMIZED && currentId == R.id.collapsed) {
state = State.MINIMIZED
isMinimizingFromFullScreen = false
onMinimize.emit();
}
else if (state != State.MAXIMIZED && progress > 0.9) {
if (_isInitialMaximize) {
state = State.CLOSED;
_isInitialMaximize = false;
}
else {
state = State.MAXIMIZED;
onMaximized.emit();
}
onMinimize.emit()
}
if (isTransitioning && (progress > 0.95 || progress < 0.05)) {
isTransitioning = false;
onTransitioning.emit(isTransitioning);
if(isInPictureInPicture) leavePictureInPictureMode(false); //Workaround to prevent getting stuck in p2p
}
else if (!isTransitioning && (progress < 0.95 && progress > 0.05)) {
isTransitioning = true;
onTransitioning.emit(isTransitioning);
if(isInPictureInPicture) leavePictureInPictureMode(false); //Workaround to prevent getting stuck in p2p
if (state != State.MAXIMIZED && currentId == R.id.expanded) {
state = State.MAXIMIZED
onMaximized.emit()
}
}
override fun onTransitionCompleted(motionLayout: MotionLayout?, currentId: Int) { }
override fun onTransitionStarted(p0: MotionLayout?, p1: Int, p2: Int) { }
override fun onTransitionTrigger(p0: MotionLayout?, p1: Int, p2: Boolean, p3: Float) { }
});
_view?.let {
if (it.progress >= 0.5 && it.progress < 1.0)
maximizeVideoDetail();
if (it.progress < 0.5 && it.progress > 0.0)
minimizeVideoDetail();
}
override fun onTransitionStarted(motionLayout: MotionLayout?, startId: Int, endId: Int) {}
override fun onTransitionTrigger(motionLayout: MotionLayout?, triggerId: Int, positive: Boolean, progress: Float) {}
override fun onTransitionChange(motionLayout: MotionLayout?, startId: Int, endId: Int, progress: Float) {}
})
_loadUrlOnCreate?.let { _viewDetail?.setVideo(it.url, it.timeSeconds, it.playWhenReady) };
maximizeVideoDetail();
maximizeVideoDetail(false);
SettingsActivity.settingsActivityClosed.subscribe(this) {
updateOrientation()
@@ -432,7 +384,8 @@ class VideoDetailFragment() : MainFragment() {
}
_autoRotateObserver?.startObserving()
return _view!!;
_viewDetail = viewDetail
return viewDetail
}
fun onUserLeaveHint() {
@@ -554,21 +507,12 @@ class VideoDetailFragment() : MainFragment() {
_portraitOrientationListener?.disableListener()
_autoRotateObserver?.stopObserving()
_viewDetail?.let {
_viewDetail = null;
it.onDestroy();
}
_view = null;
_viewDetail = null;
}
override fun onDestroy() {
super.onDestroy()
_viewDetail?.let {
_viewDetail = null;
it.onDestroy();
}
StateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this);
Logger.i(TAG, "onDestroy");
@@ -626,7 +570,7 @@ class VideoDetailFragment() : MainFragment() {
}
updateOrientation();
_view?.allowMotion = !fullscreen;
_motionLayout?.isInteractionEnabled = !fullscreen
}
companion object {
@@ -18,11 +18,10 @@ import android.net.Uri
import android.os.Build
import android.support.v4.media.session.PlaybackStateCompat
import android.text.Spanned
import android.util.AttributeSet
import android.util.Log
import android.util.Rational
import android.util.TypedValue
import android.view.MotionEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
@@ -33,6 +32,7 @@ import android.widget.ImageButton
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.C
@@ -134,9 +134,9 @@ import com.futo.platformplayer.views.FeedStyle
import com.futo.platformplayer.views.LoaderView
import com.futo.platformplayer.views.MonetizationView
import com.futo.platformplayer.views.adapters.feedtypes.PreviewVideoView
import com.futo.platformplayer.views.behavior.TouchInterceptFrameLayout
import com.futo.platformplayer.views.casting.CastView
import com.futo.platformplayer.views.comments.AddCommentView
import com.futo.platformplayer.views.containers.CustomMotionLayout
import com.futo.platformplayer.views.others.CreatorThumbnail
import com.futo.platformplayer.views.overlays.ChaptersOverlay
import com.futo.platformplayer.views.overlays.DescriptionOverlay
@@ -167,6 +167,7 @@ import com.futo.polycentric.core.PolycentricProfile
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
import com.google.protobuf.ByteString
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
@@ -182,7 +183,7 @@ import kotlin.math.abs
import kotlin.math.roundToLong
@UnstableApi
class VideoDetailView : ConstraintLayout {
class VideoDetailView(fragment: VideoDetailFragment, inflater: LayoutInflater) : FrameLayout(inflater.context) {
private val TAG = "VideoDetailView"
lateinit var fragment: VideoDetailFragment;
@@ -211,7 +212,7 @@ class VideoDetailView : ConstraintLayout {
private val _timeBar: TimeBar;
private var _upNext: UpNextView;
private val rootView: ConstraintLayout;
private val rootView: CustomMotionLayout;
private val _title: TextView;
private val _subTitle: TextView;
@@ -240,7 +241,6 @@ class VideoDetailView : ConstraintLayout {
private val _commentsList: CommentsList;
private var _minimizeProgress: Float = 0f;
private val _buttonSubscribe: SubscribeButton;
private val _buttonPins: RoundButtonGroup;
@@ -262,7 +262,7 @@ class VideoDetailView : ConstraintLayout {
private val _textResume: TextView;
private val _layoutResume: LinearLayout;
private var _jobHideResume: Job? = null;
private val _layoutPlayerContainer: TouchInterceptFrameLayout;
private val _layoutPlayerContainer: FrameLayout;
private val _layoutChangeBottomSection: LinearLayout;
//Overlays
@@ -340,7 +340,6 @@ class VideoDetailView : ConstraintLayout {
val onShouldEnterPictureInPictureChanged = Event0();
val onTouchCancel = Event0();
private var _lastPositionSaveTime: Long = -1;
private val DP_5 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics);
@@ -358,9 +357,8 @@ class VideoDetailView : ConstraintLayout {
Pair(0, 10) //around live, try every 10 seconds
);
@androidx.annotation.OptIn(UnstableApi::class)
constructor(context: Context, attrs : AttributeSet? = null) : super(context, attrs) {
inflate(context, R.layout.fragview_video_detail, this);
init {
inflater.inflate(R.layout.fragview_video_detail, this)
//Declare Views
rootView = findViewById(R.id.videodetail_root);
@@ -413,8 +411,7 @@ class VideoDetailView : ConstraintLayout {
_textSkip = findViewById(R.id.text_skip);
_layoutResume = findViewById(R.id.layout_resume);
_textResume = findViewById(R.id.text_resume);
_layoutPlayerContainer = findViewById(R.id.layout_player_container);
_layoutPlayerContainer.onClick.subscribe { onMaximize.emit(false); };
_layoutPlayerContainer = findViewById(R.id.layout_player_container)
_layoutRating = findViewById(R.id.layout_rating);
_textDislikes = findViewById(R.id.text_dislikes);
@@ -576,8 +573,9 @@ class VideoDetailView : ConstraintLayout {
if(chapter?.type == ChapterType.SKIPPABLE) {
_layoutSkip.visibility = VISIBLE;
} else if(chapter?.type == ChapterType.SKIP || chapter?.type == ChapterType.SKIPONCE) {
if (StateCasting.instance.activeDevice != null) {
StateCasting.instance.videoSeekTo(chapter.timeEnd)
val ad = StateCasting.instance.activeDevice
if (ad != null) {
ad.seekVideo(chapter.timeEnd)
} else {
_player.seekTo((chapter.timeEnd * 1000).toLong());
}
@@ -608,10 +606,6 @@ class VideoDetailView : ConstraintLayout {
updatePlaybackTracking(position);
};
_player.onVideoClicked.subscribe {
if(_minimizeProgress < 0.5)
onMaximize.emit(false);
}
_player.onSourceChanged.subscribe(::onSourceChanged);
_player.onSourceEnded.subscribe {
if (!fragment.isInPictureInPicture) {
@@ -885,7 +879,7 @@ class VideoDetailView : ConstraintLayout {
if (ad != null) {
val currentChapter = _cast.getCurrentChapter((ad.time * 1000).toLong());
if(currentChapter?.type == ChapterType.SKIPPABLE) {
StateCasting.instance.videoSeekTo(currentChapter.timeEnd);
ad.seekVideo(currentChapter.timeEnd);
}
} else {
val currentChapter = _player.getCurrentChapter(_player.position);
@@ -894,6 +888,48 @@ class VideoDetailView : ConstraintLayout {
}
}
}
var currentState = R.id.expanded
rootView.addTransitionListener(object : MotionLayout.TransitionListener {
override fun onTransitionCompleted(motionLayout: MotionLayout?, currentId: Int) {
when (currentId) {
R.id.collapsed -> {
_player.gestureControl.setOnClickListener {
fragment.maximizeVideoDetail(false)
}
_player.gestureControl.controlsEnabled = false
}
R.id.expanded -> {
_layoutResume.alpha = 1f
_cast.setButtonAlpha(1f)
_player.lockControlsAlpha(false)
_player.hideControls(false)
_player.gestureControl.controlsEnabled = true
_player.gestureControl.setOnClickListener(null)
}
}
currentState = currentId
if(currentId == R.id.full_screen_gesture) {
setFullscreen(true)
motionLayout?.transitionToState(R.id.expanded)
}
}
override fun onTransitionStarted(motionLayout: MotionLayout?, startId: Int, endId: Int) {
if (currentState == R.id.expanded) {
_layoutResume.alpha = 0f
_cast.setButtonAlpha(0f)
_player.lockControlsAlpha(true)
_player.hideControls(true);
}
}
override fun onTransitionTrigger(motionLayout: MotionLayout?, triggerId: Int, positive: Boolean, progress: Float) { }
override fun onTransitionChange(motionLayout: MotionLayout?, startId: Int, endId: Int, progress: Float) {}
})
}
val _trackingUpdateTimeLock = Object();
@@ -1004,7 +1040,8 @@ class VideoDetailView : ConstraintLayout {
}
}
_slideUpOverlay?.hide();
} else if(video is JSVideoDetails && (video as JSVideoDetails).hasVODEvents())
} else null,
if(video is JSVideoDetails && (video as JSVideoDetails).hasVODEvents())
RoundButton(context, R.drawable.ic_chat, context.getString(R.string.vod_chat), TAG_VODCHAT) {
video?.let {
try {
@@ -1153,7 +1190,7 @@ class VideoDetailView : ConstraintLayout {
//Recover cancelled loads
if(video == null) {
val t = (lastPositionMilliseconds / 1000.0f).roundToLong();
if(_searchVideo != null && !wasLoginCall)
if(_searchVideo != null)
setVideoOverview(_searchVideo!!, true, t);
else if(_url != null && !wasLoginCall)
setVideo(_url!!, t, _playWhenReady);
@@ -2200,16 +2237,6 @@ class VideoDetailView : ConstraintLayout {
}
}
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
if (ev?.actionMasked == MotionEvent.ACTION_CANCEL ||
ev?.actionMasked == MotionEvent.ACTION_POINTER_DOWN ||
ev?.actionMasked == MotionEvent.ACTION_POINTER_UP) {
onTouchCancel.emit();
}
return super.onInterceptTouchEvent(ev);
}
//Actions
private fun showVideoSettings() {
Logger.i(TAG, "showVideoSettings")
@@ -2367,11 +2394,11 @@ class VideoDetailView : ConstraintLayout {
?.distinct()
?.toList() ?: listOf() else audioSources?.toList() ?: listOf();
val canSetSpeed = !_isCasting || StateCasting.instance.activeDevice?.canSetSpeed() == true
val canSetSpeed = !_isCasting || StateCasting.instance.activeDevice?.canSetSpeed == true
val currentPlaybackRate = if (_isCasting) StateCasting.instance.activeDevice?.speed else _player.getPlaybackRate()
val qualityPlaybackSpeedTitle = if (canSetSpeed) SlideUpMenuTitle(this.context).apply { setTitle(context.getString(R.string.playback_rate) + " (${String.format("%.2f", currentPlaybackRate)})"); } else null;
_overlay_quality_selector = SlideUpMenuOverlay(this.context, _overlay_quality_container, context.getString(
R.string.quality), null, true,
R.string.quality), null, true,
qualityPlaybackSpeedTitle,
if (canSetSpeed) SlideUpMenuButtonList(this.context, null, "playback_rate").apply {
val playbackSpeeds = Settings.instance.playback.getPlaybackSpeeds();
@@ -2392,7 +2419,7 @@ class VideoDetailView : ConstraintLayout {
val newPlaybackSpeed = playbackSpeedString.toDouble();
if (_isCasting) {
val ad = StateCasting.instance.activeDevice ?: return@subscribe
if (!ad.canSetSpeed()) {
if (!ad.canSetSpeed) {
return@subscribe
}
@@ -2516,7 +2543,6 @@ class VideoDetailView : ConstraintLayout {
if (!StateCasting.instance.resumeVideo()) {
_player.play();
}
onShouldEnterPictureInPictureChanged.emit()
//TODO: This was needed because handleLowerVolume was done.
//_player.setVolume(1.0f);
@@ -2533,7 +2559,6 @@ class VideoDetailView : ConstraintLayout {
if (!StateCasting.instance.pauseVideo()) {
_player.pause();
}
onShouldEnterPictureInPictureChanged.emit()
}
private fun handleSeek(ms: Long) {
Logger.i(TAG, "handleSeek(ms=$ms)")
@@ -2705,10 +2730,7 @@ class VideoDetailView : ConstraintLayout {
Logger.i(TAG, "handleFullScreen(fullscreen=$fullscreen)")
if(fullscreen) {
_container_content.visibility = GONE
_layoutPlayerContainer.setPadding(0, 0, 0, 0);
val lp = _container_content.layoutParams as LayoutParams;
val lp = _container_content.layoutParams as ConstraintLayout.LayoutParams;
lp.topMargin = 0;
_container_content.layoutParams = lp;
@@ -2719,10 +2741,7 @@ class VideoDetailView : ConstraintLayout {
setProgressBarOverlayed(null);
}
else {
_container_content.visibility = VISIBLE
_layoutPlayerContainer.setPadding(0, 0, 0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 6.0f, Resources.getSystem().displayMetrics).toInt());
val lp = _container_content.layoutParams as LayoutParams;
val lp = _container_content.layoutParams as ConstraintLayout.LayoutParams;
lp.topMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, -18.0f, Resources.getSystem().displayMetrics).toInt();
_container_content.layoutParams = lp;
@@ -2951,7 +2970,7 @@ class VideoDetailView : ConstraintLayout {
hideAddTo()
onVideoClicked.subscribe { video, _ ->
fragment.navigate<VideoDetailFragment>(video).maximizeVideoDetail()
fragment.navigate<VideoDetailFragment>(video).maximizeVideoDetail(false)
}
onChannelClicked.subscribe {
@@ -2998,17 +3017,12 @@ class VideoDetailView : ConstraintLayout {
_container_content.visibility = GONE
_player.fillHeight(false)
_layoutPlayerContainer.setPadding(0, 0, 0, 0);
}
fun handleLeavePictureInPicture() {
Logger.i(TAG, "handleLeavePictureInPicture")
if(!_player.isFullScreen) {
_container_content.visibility = VISIBLE
_player.fitHeight();
_layoutPlayerContainer.setPadding(0, 0, 0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 6.0f, Resources.getSystem().displayMetrics).toInt());
} else {
_layoutPlayerContainer.setPadding(0, 0, 0, 0);
}
}
fun getPictureInPictureParams() : PictureInPictureParams {
@@ -3113,53 +3127,6 @@ class VideoDetailView : ConstraintLayout {
}
}
//Animation related setters
fun setMinimizeProgress(progress : Float) {
_minimizeProgress = progress;
_player.lockControlsAlpha(progress < 0.9);
_layoutPlayerContainer.shouldInterceptTouches = progress < 0.95;
if(progress > 0.9) {
if(_minimize_controls.visibility != View.GONE)
_minimize_controls.visibility = View.GONE;
}
else if(_minimize_controls.visibility != View.VISIBLE) {
_minimize_controls.visibility = View.VISIBLE;
}
//Switching video to fill
if(progress > 0.25) {
if(!_player.isFullScreen && _player.layoutParams.height != WRAP_CONTENT) {
_player.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT);
if(!fragment.isInPictureInPicture) {
_player.fitHeight();
_layoutPlayerContainer.setPadding(0, 0, 0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 6.0f, Resources.getSystem().displayMetrics).toInt());
}
else {
_layoutPlayerContainer.setPadding(0, 0, 0, 0);
}
_cast.layoutParams = _cast.layoutParams.apply {
(this as MarginLayoutParams).bottomMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 6.0f, resources.displayMetrics).toInt();
};
setProgressBarOverlayed(false);
_player.hideControls(false);
}
}
else {
if(_player.layoutParams.height == WRAP_CONTENT) {
_player.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT);
_player.fillHeight(true)
_cast.layoutParams = _cast.layoutParams.apply {
(this as MarginLayoutParams).bottomMargin = 0;
};
setProgressBarOverlayed(true);
_player.hideControls(false);
_layoutPlayerContainer.setPadding(0, 0, 0, 0);
}
}
}
private fun setPolycentricProfile(profile: PolycentricProfile?, animate: Boolean) {
_polycentricProfile = profile
@@ -3183,42 +3150,8 @@ class VideoDetailView : ConstraintLayout {
}
fun setProgressBarOverlayed(isOverlayed: Boolean?) {
Logger.v(TAG, "setProgressBarOverlayed(isOverlayed: ${isOverlayed ?: "null"})");
isOverlayed?.let{ _cast.setProgressBarOverlayed(it) };
if(isOverlayed == null) {
//For now this seems to be the best way to keep it updated?
_playerProgress.layoutParams = _playerProgress.layoutParams.apply {
(this as MarginLayoutParams).bottomMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, -12f, resources.displayMetrics).toInt();
};
_playerProgress.elevation = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2f, resources.displayMetrics);
}
else if(isOverlayed) {
_playerProgress.layoutParams = _playerProgress.layoutParams.apply {
(this as MarginLayoutParams).bottomMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, -2f, resources.displayMetrics).toInt();
};
_playerProgress.elevation = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics);
}
else {
_playerProgress.layoutParams = _playerProgress.layoutParams.apply {
(this as MarginLayoutParams).bottomMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 6f, resources.displayMetrics).toInt();
};
_playerProgress.elevation = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2f, resources.displayMetrics);
}
}
fun setContentAlpha(alpha: Float) {
_container_content.alpha = alpha;
}
fun setControllerAlpha(alpha: Float) {
_layoutResume.alpha = alpha;
_player.videoControls.alpha = alpha;
_cast.setButtonAlpha(alpha);
}
fun setMinimizeControlsAlpha(alpha : Float) {
_minimize_controls.alpha = alpha;
val clickable = alpha > 0.9;
if(_minimize_controls.isClickable != clickable)
_minimize_controls.isClickable = clickable;
Logger.v(TAG, "setProgressBarOverlayed(isOverlayed: ${isOverlayed ?: "null"})")
isOverlayed?.let { _cast.setProgressBarOverlayed(it) }
}
override fun onConfigurationChanged(newConfig: Configuration?) {
@@ -3230,16 +3163,6 @@ class VideoDetailView : ConstraintLayout {
}
}
fun setVideoMinimize(value : Float) {
val padRight = (resources.displayMetrics.widthPixels * 0.70 * value).toInt()
_player.setPadding(0, _player.paddingTop, padRight, 0)
_cast.setPadding(0, _cast.paddingTop, padRight, 0)
}
fun setTopPadding(value: Float) {
_player.setPadding(_player.paddingLeft, value.toInt(), _player.paddingRight, 0)
}
//Tasks
private val _taskLoadVideo = if(!isInEditMode) TaskHandler<String, IPlatformVideoDetails>(
StateApp.instance.scopeGetter,
@@ -3,9 +3,16 @@ package com.futo.platformplayer.models
import com.futo.platformplayer.casting.CastProtocolType
@kotlinx.serialization.Serializable
class CastingDeviceInfo(
var name: String,
var type: CastProtocolType,
var addresses: Array<String>,
var port: Int
)
class CastingDeviceInfo {
var name: String;
var type: CastProtocolType;
var addresses: Array<String>;
var port: Int;
constructor(name: String, type: CastProtocolType, addresses: Array<String>, port: Int) {
this.name = name;
this.type = type;
this.addresses = addresses;
this.port = port;
}
}
@@ -135,12 +135,8 @@ class StateApp {
return _scope;
}
val scope: CoroutineScope get() {
val thisScope = scopeOrNull;
if(thisScope == null) {
//throw IllegalStateException("Attempted to use a global lifetime scope while MainActivity is no longer available");
Logger.w(TAG, "Attempted to use a global lifetime scope while MainActivity is no longer available, USING GLOBAL SCOPE");
return GlobalScope;
}
val thisScope = scopeOrNull
?: throw IllegalStateException("Attempted to use a global lifetime scope while MainActivity is no longer available");
return thisScope;
}
val scopeGetter: ()->CoroutineScope get() {
@@ -177,11 +177,16 @@ class StatePlatform {
}
withContext(Dispatchers.IO) {
var toDisables = mutableListOf<IPlatformClient>();
var enabled: Array<String>;
synchronized(_clientsLock) {
for(e in _enabledClients) {
toDisables.add(e);
try {
e.disable();
onSourceDisabled.emit(e);
}
catch(ex: Throwable) {
UIDialogs.appToast(ToastView.Toast("If this happens often, please inform the developers on Github", false, null, "Plugin [${e.name}] failed to disable"));
}
}
_enabledClients.clear();
@@ -231,18 +236,6 @@ class StatePlatform {
}
}
selectClients(*enabled);
for(toDisable in toDisables) {
launch(Dispatchers.IO) {
try {
toDisable.disable();
onSourceDisabled.emit(toDisable);
}
catch(ex: Throwable) {
Logger.e(TAG, "FAILED TO DISABLE CLIENT [${toDisable?.name}] AFTER UpdateAvailableClients", ex);
}
}
}
};
}
@@ -355,11 +348,11 @@ class StatePlatform {
StateApp.instance.handleCaptchaException(c, ex);
}
var toDisable: IPlatformClient? = null;
synchronized(_clientsLock) {
if (_enabledClients.contains(client)) {
_enabledClients.remove(client);
toDisable = client;
client.disable();
onSourceDisabled.emit(client);
newClient.initialize();
_enabledClients.add(newClient);
}
@@ -367,18 +360,6 @@ class StatePlatform {
_availableClients.removeIf { it.id == id };
_availableClients.add(newClient);
}
if(toDisable != null) {
launch(Dispatchers.IO) {
try {
toDisable?.disable();
onSourceDisabled.emit(client);
}
catch (ex: Throwable) {
Logger.e(TAG, "FAILED TO DISABLE CLIENT [${toDisable?.name}] AFTER RELOAD", ex);
}
}
}
afterReload?.invoke();
return@withContext newClient;
};
@@ -402,25 +402,18 @@ class StatePlugins {
}
val icon = config.absoluteIconUrl?.let { absIconUrl ->
withContext(Dispatchers.Main) {
it.setText("Saving plugin...");
it.setProgress(0.75);
}
val iconResp = client.get(absIconUrl);
if(iconResp.isOk)
return@let iconResp.body?.byteStream()?.use { it.readBytes() };
return@let null;
}
withContext(Dispatchers.Main) {
it.setText("Saving plugin...");
it.setProgress(0.75);
}
val installEx = StatePlugins.instance.createPlugin(config, script, icon, reinstall);
if(installEx != null)
throw installEx;
withContext(Dispatchers.Main) {
it.setText("Reloading available plugins...");
it.setProgress(0.9);
}
StatePlatform.instance.updateAvailableClients(context);
withContext(Dispatchers.Main) {
@@ -529,7 +522,9 @@ class StatePlugins {
if(id == StateDeveloper.DEV_ID)
throw IllegalStateException("Attempted to retrieve a persistent developer plugin, this is not allowed");
return _plugins.findItem { it.config.id == id };
synchronized(_plugins) {
return _plugins.findItem { it.config.id == id };
}
}
fun getPlugins(): List<SourcePluginDescriptor> {
return _plugins.getItems();
@@ -538,10 +533,12 @@ class StatePlugins {
fun deletePlugin(id: String) {
synchronized(_pluginScripts) {
_pluginScripts.deleteFile(id);
val plugins = _plugins.findItems { it.config.id == id };
for(plugin in plugins)
_plugins.delete(plugin);
synchronized(_plugins) {
_pluginScripts.deleteFile(id);
val plugins = _plugins.findItems { it.config.id == id };
for(plugin in plugins)
_plugins.delete(plugin);
}
}
}
fun createPlugin(config: SourcePluginConfig, script: String, icon: ByteArray? = null, reinstall: Boolean = false, flags: List<String> = listOf()) : Throwable? {
@@ -57,12 +57,9 @@ class StateSync {
return
}
var relayServerUrl = Settings.instance.synchronization.syncServer;
Logger.i(TAG, "Relay used: ${relayServerUrl}");
syncService = SyncService(
SERVICE_NAME,
relayServerUrl,
RELAY_SERVER,
RELAY_PUBLIC_KEY,
APP_ID,
StoreBasedSyncDatabaseProvider(),
@@ -4,7 +4,6 @@ import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.LinearLayout
import androidx.core.view.children
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
@@ -29,8 +28,6 @@ import kotlinx.coroutines.launch
class ToggleBar : LinearLayout {
private val _tagsContainer: LinearLayout;
private var allowLongPress: Boolean = false;
override fun onAttachedToWindow() {
super.onAttachedToWindow();
}
@@ -51,31 +48,12 @@ class ToggleBar : LinearLayout {
for(button in buttons) {
_tagsContainer.addView(ToggleTagView(context).apply {
if(button.icon > 0)
this.setInfo(button.icon, button.name, button.isActive, button.isButton, button.tag);
this.setInfo(button.icon, button.name, button.isActive, button.isButton);
else if(button.iconVariable != null)
this.setInfo(button.iconVariable, button.name, button.isActive, button.isButton, button.tag);
this.setInfo(button.iconVariable, button.name, button.isActive, button.isButton);
else
this.setInfo(button.name, button.isActive, button.isButton, button.tag);
this.setInfo(button.name, button.isActive, button.isButton);
this.onClick.subscribe({ view, enabled -> button.action(view, enabled); });
if(allowLongPress) {
this.onLongClick.subscribe({ view, enabled ->
for (tagView in _tagsContainer.children.filter { it is ToggleTagView }) {
if (tagView != view && tagView is ToggleTagView && !tagView.isButton) {
if (enabled && !tagView.isActive) {
tagView.handleClick();
} else if (!enabled && tagView.isActive) {
tagView.handleClick();
}
}
}
})
}
else if(button.actionLong != null) {
this.onLongClick.subscribe({ view, enabled ->
val tags = _tagsContainer.children.filter { it is ToggleTagView }.map { it as ToggleTagView }.toList();
button.actionLong!!(view, tags, enabled);
});
}
});
}
}
@@ -85,18 +63,16 @@ class ToggleBar : LinearLayout {
val icon: Int;
val iconVariable: ImageVariable?;
val action: (ToggleTagView, Boolean)->Unit;
val actionLong: ((ToggleTagView, List<ToggleTagView>, Boolean) -> Unit)?;
val isActive: Boolean;
var isButton: Boolean = false
private set;
var tag: String? = null;
constructor(name: String, icon: ImageVariable?, isActive: Boolean = false, action: (ToggleTagView, Boolean)->Unit, actionLong: ((ToggleTagView, List<ToggleTagView>, Boolean)->Unit)? = null) {
constructor(name: String, icon: ImageVariable?, isActive: Boolean = false, action: (ToggleTagView, Boolean)->Unit) {
this.name = name;
this.icon = 0;
this.iconVariable = icon;
this.action = action;
this.actionLong = actionLong;
this.isActive = isActive;
}
constructor(name: String, icon: Int, isActive: Boolean = false, action: (ToggleTagView, Boolean)->Unit) {
@@ -104,7 +80,6 @@ class ToggleBar : LinearLayout {
this.icon = icon;
this.iconVariable = null;
this.action = action;
this.actionLong = null;
this.isActive = isActive;
}
constructor(name: String, isActive: Boolean = false, action: (ToggleTagView, Boolean)->Unit) {
@@ -112,7 +87,6 @@ class ToggleBar : LinearLayout {
this.icon = 0;
this.iconVariable = null;
this.action = action;
this.actionLong = null;
this.isActive = isActive;
}
@@ -4,19 +4,21 @@ import android.graphics.drawable.Animatable
import android.view.View
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.casting.AirPlayCastingDevice
import com.futo.platformplayer.casting.CastConnectionState
import com.futo.platformplayer.casting.CastProtocolType
import com.futo.platformplayer.casting.CastingDevice
import com.futo.platformplayer.casting.ChromecastCastingDevice
import com.futo.platformplayer.casting.FCastCastingDevice
import com.futo.platformplayer.casting.StateCasting
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.constructs.Event2
import androidx.core.view.isVisible
import com.futo.platformplayer.UIDialogs
class DeviceViewHolder : ViewHolder {
private val _layoutDevice: FrameLayout;
@@ -54,18 +56,16 @@ class DeviceViewHolder : ViewHolder {
val connect = {
device?.let { dev ->
try {
if (dev.isReady) {
StateCasting.instance.activeDevice?.stopPlayback()
StateCasting.instance.connectDevice(dev)
onConnect.emit(dev)
} else {
view.context?.let {
UIDialogs.toast(it, "Device not ready, may be offline")
}
if (dev.isReady) {
StateCasting.instance.activeDevice?.stopCasting()
StateCasting.instance.connectDevice(dev)
onConnect.emit(dev)
} else {
try {
view.context?.let { UIDialogs.toast(it, "Device not ready, may be offline") }
} catch (e: Throwable) {
//Ignored
}
} catch (e: Throwable) {
Logger.e(TAG, "Failed to connect: $e")
}
}
}
@@ -81,25 +81,15 @@ class DeviceViewHolder : ViewHolder {
}
fun bind(d: CastingDevice, isOnlineDevice: Boolean, isPinnedDevice: Boolean) {
when (d.protocolType) {
CastProtocolType.CHROMECAST -> {
_imageDevice.setImageResource(R.drawable.ic_chromecast);
_textType.text = "Chromecast";
}
CastProtocolType.AIRPLAY -> {
_imageDevice.setImageResource(R.drawable.ic_airplay);
_textType.text = "AirPlay";
}
CastProtocolType.FCAST -> {
_imageDevice.setImageResource(
if (Settings.instance.casting.experimentalCasting) {
R.drawable.ic_exp_fc
} else {
R.drawable.ic_fc
}
)
_textType.text = "FCast";
}
if (d is ChromecastCastingDevice) {
_imageDevice.setImageResource(R.drawable.ic_chromecast);
_textType.text = "Chromecast";
} else if (d is AirPlayCastingDevice) {
_imageDevice.setImageResource(R.drawable.ic_airplay);
_textType.text = "AirPlay";
} else if (d is FCastCastingDevice) {
_imageDevice.setImageResource(R.drawable.ic_fc);
_textType.text = "FCast";
}
_textName.text = d.name;
@@ -146,8 +136,4 @@ class DeviceViewHolder : ViewHolder {
device = d;
}
companion object {
private val TAG = "DeviceViewHolder"
}
}
@@ -119,6 +119,8 @@ class GestureControlView : LinearLayout {
var fullScreenGestureEnabled = true
var controlsEnabled = true
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
LayoutInflater.from(context).inflate(R.layout.view_gesture_controls, this, true);
@@ -350,6 +352,10 @@ class GestureControlView : LinearLayout {
override fun onTouchEvent(event: MotionEvent?): Boolean {
val ev = event ?: return super.onTouchEvent(event);
if(!controlsEnabled) {
return super.onTouchEvent(ev)
}
if (ev.action == MotionEvent.ACTION_UP && _speedHolding) {
_speedHolding = false
hideHoldSpeedControls()
@@ -2,7 +2,12 @@ package com.futo.platformplayer.views.casting
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.FrameLayout
import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.content.ContextCompat
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
@@ -21,13 +21,14 @@ import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.api.media.models.chapters.IChapter
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.casting.CastConnectionState
import com.futo.platformplayer.casting.AirPlayCastingDevice
import com.futo.platformplayer.casting.ChromecastCastingDevice
import com.futo.platformplayer.casting.StateCasting
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.formatDuration
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateHistory
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.views.TargetTapLoaderView
import com.futo.platformplayer.views.behavior.GestureControlView
@@ -35,6 +36,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class CastView : ConstraintLayout {
@@ -97,30 +99,19 @@ class CastView : ConstraintLayout {
val d = StateCasting.instance.activeDevice ?: return@subscribe;
_speedHoldWasPlaying = d.isPlaying
_speedHoldPrevRate = d.speed
try {
if (d.canSetSpeed()) {
d.changeSpeed(Settings.instance.playback.getHoldPlaybackSpeed())
}
d.resumePlayback()
} catch (e: Throwable) {
Logger.e(TAG, "Failed to change playback speed to hold playback speed: $e")
}
if (d.canSetSpeed)
d.changeSpeed(Settings.instance.playback.getHoldPlaybackSpeed())
d.resumeVideo()
}
_gestureControlView.onSpeedHoldEnd.subscribe {
try {
val d = StateCasting.instance.activeDevice ?: return@subscribe;
if (!_speedHoldWasPlaying) {
d.pausePlayback()
}
d.changeSpeed(_speedHoldPrevRate)
} catch (e: Throwable) {
Logger.e(TAG, "Failed to change playback speed to previous hold playback speed: $e")
}
val d = StateCasting.instance.activeDevice ?: return@subscribe;
if (!_speedHoldWasPlaying) d.pauseVideo()
d.changeSpeed(_speedHoldPrevRate)
}
_gestureControlView.onSeek.subscribe {
val d = StateCasting.instance.activeDevice ?: return@subscribe;
StateCasting.instance.videoSeekTo( d.expectedCurrentTime + it / 1000);
StateCasting.instance.videoSeekTo(d.expectedCurrentTime + it / 1000);
};
_buttonLoop.setOnClickListener {
@@ -229,9 +220,22 @@ class CastView : ConstraintLayout {
stopTimeJob()
if(isPlaying) {
StateCasting.instance.startUpdateTimeJob(
onTimeJobTimeChanged_s
) { setTime(it) }
val d = StateCasting.instance.activeDevice;
if (d is AirPlayCastingDevice || d is ChromecastCastingDevice) {
_updateTimeJob = _scope.launch {
while (true) {
val device = StateCasting.instance.activeDevice;
if (device == null || !device.isPlaying) {
break;
}
delay(1000);
val time_ms = (device.expectedCurrentTime * 1000.0).toLong()
setTime(time_ms);
onTimeJobTimeChanged_s.emit(device.expectedCurrentTime.toLong())
}
}
}
if (!_inPictureInPicture) {
_buttonPause.visibility = View.VISIBLE;
@@ -329,8 +333,4 @@ class CastView : ConstraintLayout {
_loaderGame.visibility = View.VISIBLE
_loaderGame.startLoader(expectedDurationMs.toLong())
}
companion object {
private val TAG = "CastView";
}
}
@@ -0,0 +1,121 @@
package com.futo.platformplayer.views.containers
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Rect
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import androidx.constraintlayout.motion.widget.MotionLayout
import com.futo.platformplayer.R
import kotlin.math.abs
class CustomMotionLayout(context: Context, attributeSet: AttributeSet? = null) :
MotionLayout(context, attributeSet) {
private val viewToDetectTouch by lazy {
findViewById<View>(R.id.layout_player_container) //TODO move to Attributes
}
private val viewToDetectTouch2 by lazy {
findViewById<View>(R.id.minimize_controls) //TODO move to Attributes
}
private var savedActionDown: MotionEvent? = null
private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop
// intercepting touch events is necessary because something to do with PlayerControlView makes things not work
override fun onInterceptTouchEvent(event: MotionEvent?): Boolean {
val ev = event ?: return super.onInterceptTouchEvent(null)
// special touch interception logic is unnecessary if interaction is disabled
if (!isInteractionEnabled) {
return super.onInterceptTouchEvent(ev)
}
when (ev.actionMasked) {
MotionEvent.ACTION_DOWN -> {
val viewRect = Rect()
viewToDetectTouch.getHitRect(viewRect)
val isInView = viewRect.contains(ev.x.toInt(), ev.y.toInt())
viewToDetectTouch2.getHitRect(viewRect)
val isInView2 = viewRect.contains(ev.x.toInt(), ev.y.toInt())
// Don't intercept touches if they are outside of the player or the mini player controls
if (!isInView && !isInView2) {
return false
}
val thing = super.onInterceptTouchEvent(ev)
// If the MotionLayout is already intercepting this touch then don't track it
if (thing) {
return true
}
// MotionLayout didn't intercept the touch but the touch is over the player/mini controls views
// in the future the class will
// save the touch event for later
// need to replay this initial touch to the MotionLayout if it ends up turning into a drag
// return false because that matches the return from the super call above
savedActionDown?.recycle() // Recycle the old event to prevent memory leaks (if for some reason it wasn't cleaned up in the other code paths)
savedActionDown = MotionEvent.obtain(ev)
return false
}
MotionEvent.ACTION_MOVE -> {
val localSavedActionDown = savedActionDown
// only handle the move event if there is a saved action stored
// then check to see if it has turned into a drag
if (localSavedActionDown != null) {
val dy = abs(ev.y - localSavedActionDown.y)
if (dy > touchSlop) {
// if it has turned into a drag then
// replay the down action saved earlier
// clean up our data
// return true so that the MotionLayout's onTouchEvent will receive future events for this gesture
//
// it is necessary to replay the down action because otherwise MotionLayout will not always initialize the drag correctly
super.onTouchEvent(localSavedActionDown)
localSavedActionDown.recycle() // Clean up the saved event after replaying
savedActionDown = null
return true
}
}
}
// if it's an up or cancel action clean up our tracking
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
savedActionDown?.recycle()
savedActionDown = null
}
}
// since the function hasn't handled the even this far send it to the parent class
return super.onInterceptTouchEvent(ev)
}
// onTouchEvent is necessary to make sure that only touch and drag on the video triggers the animation (instead of everywhere on the screen)
@SuppressLint("ClickableViewAccessibility") // pretty sure this issue doesn't apply
override fun onTouchEvent(event: MotionEvent?): Boolean {
val ev = event ?: return super.onTouchEvent(null)
// special touch event handling logic is unnecessary if interaction is disabled
if (!isInteractionEnabled) {
return super.onTouchEvent(ev)
}
val viewRect = Rect()
viewToDetectTouch.getHitRect(viewRect)
val isInView = viewRect.contains(ev.x.toInt(), ev.y.toInt())
viewToDetectTouch2.getHitRect(viewRect)
val isInView2 = viewRect.contains(ev.x.toInt(), ev.y.toInt())
// don't want to handle touches outside of the player/mini controls views
if ((!isInView && !isInView2) && event.actionMasked == MotionEvent.ACTION_DOWN) {
return false
}
return super.onTouchEvent(event)
}
}
@@ -1,94 +0,0 @@
package com.futo.platformplayer.views.containers
import android.content.Context
import android.graphics.Rect
import android.util.AttributeSet
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import androidx.constraintlayout.motion.widget.MotionLayout
import com.futo.platformplayer.R
class SingleViewTouchableMotionLayout(context: Context, attributeSet: AttributeSet? = null) : MotionLayout(context, attributeSet) {
private val viewToDetectTouch by lazy {
findViewById<View>(R.id.touchContainer) //TODO move to Attributes
}
private val viewRect = Rect()
private var touchStarted = false
private val transitionListenerList = mutableListOf<TransitionListener?>()
var allowMotion : Boolean = true;
init {
addTransitionListener(object : TransitionListener {
override fun onTransitionChange(p0: MotionLayout?, p1: Int, p2: Int, p3: Float) {
}
override fun onTransitionCompleted(p0: MotionLayout?, p1: Int) {
touchStarted = false
}
override fun onTransitionStarted(p0: MotionLayout?, p1: Int, p2: Int) {
}
override fun onTransitionTrigger(p0: MotionLayout?, p1: Int, p2: Boolean, p3: Float) {
}
})
super.setTransitionListener(object : TransitionListener {
override fun onTransitionChange(p0: MotionLayout?, p1: Int, p2: Int, p3: Float) {
transitionListenerList.filterNotNull()
.forEach { it.onTransitionChange(p0, p1, p2, p3) }
}
override fun onTransitionCompleted(p0: MotionLayout?, p1: Int) {
transitionListenerList.filterNotNull()
.forEach { it.onTransitionCompleted(p0, p1) }
}
override fun onTransitionStarted(p0: MotionLayout?, p1: Int, p2: Int) {
}
override fun onTransitionTrigger(p0: MotionLayout?, p1: Int, p2: Boolean, p3: Float) {
}
})
//isInteractionEnabled = false;
}
override fun setTransitionListener(listener: TransitionListener?) {
addTransitionListener(listener)
}
override fun addTransitionListener(listener: TransitionListener?) {
transitionListenerList += listener
}
//This always triggers, workaround calling super.onTouchEvent
//Blocks click events underneath
override fun onInterceptTouchEvent(event: MotionEvent?): Boolean {
if(!allowMotion)
return false;
if(event != null) {
when (event.actionMasked) {
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
touchStarted = false
return super.onTouchEvent(event) && false;
}
}
if (!touchStarted) {
viewToDetectTouch.getHitRect(viewRect);
val isInView = viewRect.contains(event.x.toInt(), event.y.toInt());
touchStarted = isInView
}
}
return touchStarted && super.onTouchEvent(event) && false;
}
//Not triggered on its own due to child views, intercept is used instead.
override fun onTouchEvent(event: MotionEvent): Boolean {
return false;
}
}
@@ -4,7 +4,7 @@ import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.constructs.Event3
import java.lang.reflect.Field
@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY, AnnotationTarget.FUNCTION)
@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class AdvancedField();
@@ -50,29 +50,6 @@ class RadioGroupView : FlexboxLayout {
radioView.layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
radioView.setInfo(option.first, initiallySelectedOptions.contains(option.second));
radioView.setPadding(_padding_px, _padding_px, _padding_px, _padding_px);
if(multiSelect)
radioView.onLongClick.subscribe {
val selected = !radioView.selected;
if (selected) {
selectedOptions.clear();
for(v in radioViews)
v.setIsSelected(true);
selectedOptions.addAll(options.map { it.second });
} else {
if(atLeastOne) {
for(v in radioViews)
v.setIsSelected(false);
selectedOptions.clear();
selectedOptions.add(option.second);
}
else {
for(v in radioViews)
v.setIsSelected(false);
selectedOptions.clear();
}
}
onSelectedChange.emit(selectedOptions);
}
radioView.onClick.subscribe {
val selected = !radioView.selected;
if (selected) {
@@ -20,7 +20,6 @@ class RadioView : LinearLayout {
val selected get() = _selected;
var onClick = Event0();
var onLongClick = Event0();
var onSelectedChange = Event1<Boolean>();
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
@@ -33,13 +32,6 @@ class RadioView : LinearLayout {
setIsSelected(!_selected)
}
};
_root.setOnLongClickListener {
onLongClick.emit();
if (_handleClick) {
setIsSelected(!_selected)
}
return@setOnLongClickListener true;
}
_root.setBackgroundResource(R.drawable.background_radio_unselected);
_textTag.setTextColor(ContextCompat.getColor(context, R.color.gray_67));
@@ -23,16 +23,12 @@ class ToggleTagView : LinearLayout {
private var _text: String = "";
private var _image: ImageView;
var tag: String? = null
private set;
var isActive: Boolean = false
private set;
var isButton: Boolean = false
private set;
var onClick = Event2<ToggleTagView, Boolean>();
var onLongClick = Event2<ToggleTagView, Boolean>();
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
LayoutInflater.from(context).inflate(R.layout.view_toggle_tag, this, true);
@@ -40,25 +36,10 @@ class ToggleTagView : LinearLayout {
_textTag = findViewById(R.id.text_tag);
_image = findViewById(R.id.image_tag);
_root.setOnClickListener {
handleClick();
if(!isButton)
setToggle(!isActive);
onClick.emit(this, isActive);
}
_root.setOnLongClickListener {
if(onLongClick.hasListeners())
onLongClick.emit(this, isActive);
else {
if(!isButton) {
setToggle(!isActive);
}
onClick.emit(this, isActive);
}
return@setOnLongClickListener true;
}
}
fun handleClick() {
if(!isButton)
setToggle(!isActive);
onClick.emit(this, isActive);
}
fun setToggle(isActive: Boolean) {
@@ -89,10 +70,9 @@ class ToggleTagView : LinearLayout {
_image.visibility = View.VISIBLE;
_textTag.visibility = if(!toggle.name.isNullOrEmpty()) View.VISIBLE else View.GONE;
this.isButton = isButton;
tag = toggle.tag;
}
fun setInfo(imageResource: Int, text: String, isActive: Boolean, isButton: Boolean = false, tag: String? = null) {
fun setInfo(imageResource: Int, text: String, isActive: Boolean, isButton: Boolean = false) {
_text = text;
_textTag.text = text;
setToggle(isActive);
@@ -100,9 +80,8 @@ class ToggleTagView : LinearLayout {
_image.visibility = View.VISIBLE;
_textTag.visibility = if(!text.isNullOrEmpty()) View.VISIBLE else View.GONE;
this.isButton = isButton;
this.tag = tag;
}
fun setInfo(image: ImageVariable, text: String, isActive: Boolean, isButton: Boolean = false, tag: String? = null) {
fun setInfo(image: ImageVariable, text: String, isActive: Boolean, isButton: Boolean = false) {
_text = text;
_textTag.text = text;
setToggle(isActive);
@@ -110,15 +89,13 @@ class ToggleTagView : LinearLayout {
_image.visibility = View.VISIBLE;
_textTag.visibility = if(!text.isNullOrEmpty()) View.VISIBLE else View.GONE;
this.isButton = isButton;
this.tag = tag;
}
fun setInfo(text: String, isActive: Boolean, isButton: Boolean = false, tag: String? = null) {
fun setInfo(text: String, isActive: Boolean, isButton: Boolean = false) {
_image.visibility = View.GONE;
_text = text;
_textTag.text = text;
_textTag.visibility = if(!text.isNullOrEmpty()) View.VISIBLE else View.GONE;
setToggle(isActive);
this.isButton = isButton;
this.tag = tag;
}
}
@@ -11,12 +11,10 @@ import androidx.annotation.OptIn
import androidx.media3.common.PlaybackParameters
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.ui.AspectRatioFrameLayout
import androidx.media3.ui.DefaultTimeBar
import androidx.media3.ui.PlayerView
import androidx.media3.ui.TimeBar
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StatePlayer
@@ -68,11 +66,6 @@ class FutoShortPlayer(context: Context, attrs: AttributeSet? = null) :
videoView = findViewById(R.id.short_player_view)
progressBar = findViewById(R.id.short_player_progress_bar)
if(Settings.instance.playback.shortsFitVideo)
videoView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT;
else
videoView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM;
videoView.subtitleView?.setFixedTextSize(Dimension.SP, 18F);
if (!isInEditMode) {
@@ -125,7 +125,6 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
private var _lastSourceFit: Float? = null;
private var _lastWindowWidth: Int = resources.configuration.screenWidthDp
private var _lastWindowHeight: Int = resources.configuration.screenHeightDp
private var _originalBottomMargin: Int = 0;
private var _isControlsLocked: Boolean = false;
@@ -154,10 +153,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
val onSourceEnded = Event0();
val onPrevious = Event0();
val onNext = Event0();
val onChapterChanged = Event2<IChapter?, Boolean>();
val onVideoClicked = Event0();
val onTimeBarChanged = Event2<Long, Long>();
val onChapterClicked = Event1<IChapter>();
@@ -650,13 +646,10 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
}
if (fullScreen) {
val lp = background.layoutParams as ConstraintLayout.LayoutParams;
lp.bottomMargin = 0;
background.layoutParams = lp;
_videoView.setPadding(_videoView.paddingLeft, _videoView.paddingTop, _videoView.paddingRight, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 0.0f, Resources.getSystem().displayMetrics).toInt())
_videoView.setBackgroundColor(Color.parseColor("#FF000000"))
gestureControl.hideControls();
//videoControlsBar.visibility = View.GONE;
_videoView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT;
_videoControls_fullscreen.show();
@@ -664,13 +657,10 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
videoControls.visibility = View.GONE;
}
else {
val lp = background.layoutParams as ConstraintLayout.LayoutParams;
lp.bottomMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 6.0f, Resources.getSystem().displayMetrics).toInt();
background.layoutParams = lp;
_videoView.setPadding(_videoView.paddingLeft, _videoView.paddingTop, _videoView.paddingRight, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 7.0f, Resources.getSystem().displayMetrics).toInt())
_videoView.setBackgroundColor(Color.parseColor("#00000000"))
gestureControl.hideControls();
//videoControlsBar.visibility = View.VISIBLE;
_videoView.resizeMode = _desiredResizeModePortrait;
videoControls.show();
@@ -701,10 +691,6 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
_isControlsLocked = locked;
}
override fun play() {
super.play();
}
override fun onVideoSizeChanged(videoSize: VideoSize) {
gestureControl.resetZoomPan()
_lastSourceFit = null;
@@ -774,11 +760,6 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
@OptIn(UnstableApi::class)
fun fitHeight(videoSize: VideoSize? = null) {
Logger.i(TAG, "Video Fit Height")
if (_originalBottomMargin != 0) {
val layoutParams = _videoView.layoutParams as ConstraintLayout.LayoutParams
layoutParams.setMargins(0, 0, 0, _originalBottomMargin)
_videoView.layoutParams = layoutParams
}
var h = videoSize?.height ?: lastVideoSource?.height ?: exoPlayer?.player?.videoSize?.height
?: 0
@@ -819,15 +800,13 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
}
_videoView.resizeMode = _desiredResizeModePortrait
val marginBottom =
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 7f, resources.displayMetrics)
val height = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
_lastSourceFit!!,
resources.displayMetrics
)
val rootParams = LayoutParams(LayoutParams.MATCH_PARENT, (height + marginBottom).toInt())
rootParams.bottomMargin = marginBottom.toInt()
_videoView.setPadding(_videoView.paddingLeft, _videoView.paddingTop, _videoView.paddingRight, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 7.0f, Resources.getSystem().displayMetrics).toInt())
val rootParams = LayoutParams(LayoutParams.MATCH_PARENT, (height).toInt())
_root.layoutParams = rootParams
isFitMode = true
}
@@ -835,9 +814,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
@OptIn(UnstableApi::class)
fun fillHeight(isMiniPlayer: Boolean) {
Logger.i(TAG, "Video Fill Height");
val layoutParams = _videoView.layoutParams as ConstraintLayout.LayoutParams;
_originalBottomMargin =
if (layoutParams.bottomMargin > 0) layoutParams.bottomMargin else _originalBottomMargin;
var layoutParams = _videoView.layoutParams as ConstraintLayout.LayoutParams;
layoutParams.setMargins(0);
_videoView.layoutParams = layoutParams;
_videoView.invalidate();
@@ -848,16 +825,14 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
if(isMiniPlayer){
_videoView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM
_videoView.setPadding(_videoView.paddingLeft, _videoView.paddingTop, _videoView.paddingRight, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 7.0f, Resources.getSystem().displayMetrics).toInt())
} else {
_videoView.setPadding(_videoView.paddingLeft, _videoView.paddingTop, _videoView.paddingRight, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 0.0f, Resources.getSystem().displayMetrics).toInt())
}
isFitMode = false;
}
//Animated Calls
fun setEndPadding(value: Float) {
setPadding(0, 0, value.toInt(), 0)
}
fun updateRotateLock() {
_control_rotate_lock.visibility = View.VISIBLE;
_control_rotate_lock_fullscreen.visibility = View.VISIBLE;
@@ -64,10 +64,6 @@ import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManif
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSHLSManifestAudioSource
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSVideoUrlRangeSource
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalAudioContentSource
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalAudioFileSource
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoContentSource
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoFileSource
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1
@@ -484,8 +480,6 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
is IHLSManifestSource -> { swapVideoSourceHLS(videoSource); true; }
is IVideoUrlWidevineSource -> { swapVideoSourceUrlWidevine(videoSource); true; }
is IVideoUrlSource -> { swapVideoSourceUrl(videoSource); true; }
is LocalVideoFileSource -> { swapVideoSourceLocalFile(videoSource); true; }
is LocalVideoContentSource -> { swapVideoSourceLocalContent(videoSource); true; }
null -> { _lastVideoMediaSource = null; true;}
else -> throw IllegalArgumentException("Unsupported video source [${videoSource.javaClass.simpleName}]");
}
@@ -502,8 +496,6 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
is JSDashManifestRawAudioSource -> swapAudioSourceDashRaw(audioSource, play, resume, swapId);
is IAudioUrlWidevineSource -> { swapAudioSourceUrlWidevine(audioSource); true; }
is IAudioUrlSource -> { swapAudioSourceUrl(audioSource); true; }
is LocalAudioFileSource -> { swapAudioSourceLocalFile(audioSource); true; }
is LocalAudioContentSource -> { swapAudioSourceLocalContent(audioSource); true; }
null -> { _lastAudioMediaSource = null; true; }
else -> throw IllegalArgumentException("Unsupported video source [${audioSource.javaClass.simpleName}]");
}
@@ -522,23 +514,6 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
.createMediaSource(MediaItem.fromUri(Uri.fromFile(file)));
}
@OptIn(UnstableApi::class)
private fun swapVideoSourceLocalFile(videoSource: LocalVideoFileSource) {
Logger.i(TAG, "Loading VideoSource [Local]");
val file = videoSource.file;
if(!file.exists())
throw IllegalArgumentException("File for this video does not exist");
_lastVideoMediaSource = ProgressiveMediaSource.Factory(DefaultDataSource.Factory(context))
.createMediaSource(MediaItem.fromUri(Uri.fromFile(file)));
}
@OptIn(UnstableApi::class)
private fun swapVideoSourceLocalContent(videoSource: LocalVideoContentSource) {
Logger.i(TAG, "Loading VideoSource [Local]");
if(!videoSource.contentUrl.startsWith("content://"))
throw IllegalArgumentException("Not a content uri");
_lastVideoMediaSource = ProgressiveMediaSource.Factory(DefaultDataSource.Factory(context))
.createMediaSource(MediaItem.fromUri(videoSource.contentUrl));
}
@OptIn(UnstableApi::class)
private fun swapVideoSourceUrlRange(videoSource: JSVideoUrlRangeSource) {
Logger.i(TAG, "Loading JSVideoUrlRangeSource");
if(videoSource.hasItag) {
@@ -732,23 +707,6 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
.createMediaSource(MediaItem.fromUri(Uri.fromFile(file)));
}
@OptIn(UnstableApi::class)
private fun swapAudioSourceLocalFile(audioSource: LocalAudioFileSource) {
Logger.i(TAG, "Loading VideoSource [Local]");
val file = audioSource.file;
if(!file.exists())
throw IllegalArgumentException("File for this video does not exist");
_lastAudioMediaSource = ProgressiveMediaSource.Factory(DefaultDataSource.Factory(context))
.createMediaSource(MediaItem.fromUri(Uri.fromFile(file)));
}
@OptIn(UnstableApi::class)
private fun swapAudioSourceLocalContent(audioSource: LocalAudioContentSource) {
Logger.i(TAG, "Loading VideoSource [Local]");
if(!audioSource.contentUrl.startsWith("content://"))
throw IllegalArgumentException("Not a content uri");
_lastAudioMediaSource = ProgressiveMediaSource.Factory(DefaultDataSource.Factory(context))
.createMediaSource(MediaItem.fromUri(audioSource.contentUrl));
}
@OptIn(UnstableApi::class)
private fun swapAudioSourceUrlRange(audioSource: JSAudioUrlRangeSource) {
Logger.i(TAG, "Loading JSAudioUrlRangeSource");
if(audioSource.hasItag) {
-14
View File
@@ -1,14 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="111.96dp"
android:height="114.46dp"
android:viewportWidth="111.96"
android:viewportHeight="114.46">
<path
android:pathData="m84.76,5.58c2.06,-2.06 0.6,-5.58 -2.31,-5.58H3.27C1.46,-0 -0,1.46 -0,3.27V82.45c0,2.91 3.52,4.37 5.58,2.31L20.37,69.98c0.61,-0.61 0.96,-1.45 0.96,-2.31V24.6c0,-1.81 1.46,-3.27 3.27,-3.27h43.07c0.87,0 1.7,-0.34 2.31,-0.96z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
<path
android:pathData="m45.68,73.5v17.18c0,0.68 -0.55,1.23 -1.23,1.23L27.56,91.9c-0.68,0 -1.23,-0.55 -1.23,-1.23v-17.18c0,-0.68 0.55,-1.23 1.23,-1.23h16.89c0.68,0 1.23,0.55 1.23,1.23zM45.68,51.16v17.18c0,0.68 -0.55,1.23 -1.23,1.23L27.56,69.57c-0.68,0 -1.23,-0.55 -1.23,-1.23L26.33,51.16c0,-0.68 0.55,-1.23 1.23,-1.23h16.89c0.68,0 1.23,0.55 1.23,1.23zM67.73,51.16v17.18c0,0.68 -0.55,1.23 -1.23,1.23L49.61,69.57c-0.68,0 -1.23,-0.55 -1.23,-1.23L48.38,51.16c0,-0.68 0.55,-1.23 1.23,-1.23h16.89c0.68,0 1.23,0.55 1.23,1.23zM67.73,28.83v17.18c0,0.68 -0.55,1.23 -1.23,1.23L49.61,47.24c-0.68,0 -1.23,-0.55 -1.23,-1.23L48.38,28.83c0,-0.68 0.55,-1.23 1.23,-1.23h16.89c0.68,0 1.23,0.55 1.23,1.23zM89.77,51.16v17.18c0,0.68 -0.55,1.23 -1.23,1.23h-16.89c-0.68,0 -1.23,-0.55 -1.23,-1.23L70.43,51.16c0,-0.68 0.55,-1.23 1.23,-1.23h16.89c0.68,0 1.23,0.55 1.23,1.23zM67.73,73.5v17.18c0,0.68 -0.55,1.23 -1.23,1.23L49.61,91.9c-0.68,0 -1.23,-0.55 -1.23,-1.23v-17.18c0,-0.68 0.55,-1.23 1.23,-1.23h16.89c0.68,0 1.23,0.55 1.23,1.23zM89.77,28.83v17.18c0,0.68 -0.55,1.23 -1.23,1.23h-16.89c-0.68,0 -1.23,-0.55 -1.23,-1.23L70.43,28.83c0,-0.68 0.55,-1.23 1.23,-1.23h16.89c0.68,0 1.23,0.55 1.23,1.23zM45.68,28.83v17.18c0,0.68 -0.55,1.23 -1.23,1.23L27.56,47.24c-0.68,0 -1.23,-0.55 -1.23,-1.23L26.33,28.83c0,-0.68 0.55,-1.23 1.23,-1.23h16.89c0.68,0 1.23,0.55 1.23,1.23zM45.68,95.83v17.18c0,0.68 -0.55,1.23 -1.23,1.23L27.56,114.24c-0.68,0 -1.23,-0.55 -1.23,-1.23L26.33,95.83c0,-0.68 0.55,-1.23 1.23,-1.23h16.89c0.68,0 1.23,0.55 1.23,1.23zM111.77,28.83v17.18c0,0.68 -0.55,1.23 -1.23,1.23L93.65,47.24c-0.68,0 -1.23,-0.55 -1.23,-1.23L92.43,28.83c0,-0.68 0.55,-1.23 1.23,-1.23L110.55,27.6c0.68,0 1.23,0.55 1.23,1.23z"
android:strokeWidth="0"
android:fillColor="#ffffff"/>
</vector>
@@ -55,7 +55,6 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:layout="@layout/fragview_video_detail"
android:elevation="15dp"
android:visibility="invisible" />
</FrameLayout>
@@ -63,16 +63,6 @@
android:layout_height="wrap_content"
tools:ignore="HardcodedText" />
<EditText
android:id="@+id/dialog_text_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_marginTop="10dp"
android:visibility="gone"
/>
<LinearLayout
android:id="@+id/dialog_buttons"
android:layout_width="match_parent"
@@ -94,10 +94,13 @@
android:layout_width="match_parent"
android:layout_height="0dp" />
<!-- TODO: the padding for the recycler view needs to be the same as the minimized video player and perhaps should be dynamic based on whether the player is mini or closed-->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list_results"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="80dp"
android:clipToPadding="false"
android:orientation="vertical" />
</LinearLayout>
@@ -1,30 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<com.futo.platformplayer.views.containers.SingleViewTouchableMotionLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="@color/transparent"
app:layoutDescription="@xml/videodetail_scene"
app:layout_collapseMode="parallax">
<androidx.cardview.widget.CardView
android:id="@+id/touchContainer"
android:layout_width="match_parent"
android:layout_height="220dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:background="#222222" />
<com.futo.platformplayer.fragment.mainactivity.main.VideoDetailView
android:id="@+id/fragview_videodetail"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="@+id/touchContainer"
app:layout_constraintEnd_toEndOf="@+id/touchContainer"
app:layout_constraintStart_toStartOf="@+id/touchContainer"
app:layout_constraintTop_toTopOf="@+id/touchContainer"
android:nestedScrollingEnabled="false" />
</com.futo.platformplayer.views.containers.SingleViewTouchableMotionLayout>
@@ -1,24 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<com.futo.platformplayer.views.containers.CustomMotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:fitsSystemWindows="false"
android:background="@drawable/bottom_menu_border"
android:background="@color/transparent"
android:id="@+id/videodetail_root"
android:clickable="true">
app:layoutDescription="@xml/videodetail_scene">
<com.futo.platformplayer.views.behavior.TouchInterceptFrameLayout
<FrameLayout
android:id="@+id/layout_player_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="6dp"
android:elevation="2dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent">
android:layout_width="0dp"
android:layout_height="0dp">
<!--this acts as a background-->
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:cardBackgroundColor="@color/black"
android:translationY="-7dp"/>
<com.futo.platformplayer.views.video.FutoVideoPlayer
android:id="@+id/videodetail_player"
@@ -38,39 +40,31 @@
android:visibility="gone"
android:elevation="4dp"
android:layout_marginBottom="6dp" />
</FrameLayout>
<androidx.media3.ui.PlayerControlView
android:id="@+id/videodetail_progress"
android:layout_width="match_parent"
android:layout_height="12dp"
android:layout_gravity="bottom"
android:layout_marginLeft="-6dp"
android:layout_marginRight="-6dp"
android:layout_marginBottom="6dp"
app:show_timeout="-1"
android:elevation="2dp"
app:controller_layout_id="@layout/video_player_ui_bar" />
</com.futo.platformplayer.views.behavior.TouchInterceptFrameLayout>
<androidx.media3.ui.PlayerControlView
android:id="@+id/videodetail_progress"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:show_timeout="-1"
android:elevation="1dp"
android:background="@color/black"
app:controller_layout_id="@layout/video_player_ui_bar" />
<!--Minimized Controls-->
<LinearLayout
android:id="@+id/minimize_controls"
android:orientation="horizontal"
android:layout_width="0dp"
android:layout_height="60dp"
android:layout_height="0dp"
android:paddingBottom="7dp"
android:gravity="center_vertical"
android:paddingStart="10dp"
android:clickable="false"
android:elevation="5dp"
android:alpha="1"
app:layout_constraintTop_toTopOf="@id/layout_player_container"
app:layout_constraintBottom_toBottomOf="@id/layout_player_container"
app:layout_constraintEnd_toEndOf="@id/layout_player_container"
app:layout_constraintWidth_percent="0.7">
android:background="@color/black"
android:orientation="horizontal">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:orientation="vertical">
<!--Video Title-->
@@ -180,16 +174,8 @@
<FrameLayout
android:id="@+id/contentContainer"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="-18dp"
android:elevation="1dp"
app:layout_constraintTop_toBottomOf="@id/layout_player_container"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent">
android:layout_width="0dp"
android:layout_height="0dp">
<FrameLayout
android:id="@+id/videodetail_container_main"
android:layout_width="match_parent"
@@ -394,19 +380,23 @@
android:id="@+id/videodetail_rating"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toStartOf="@id/buttons_pins"
android:layout_marginTop="7dp"
android:layout_marginStart="15dp" />
android:layout_marginStart="15dp"
app:layout_constraintHorizontal_chainStyle="spread_inside"/>
<com.futo.platformplayer.views.pills.RoundButtonGroup
android:id="@+id/buttons_pins"
app:layout_constraintLeft_toRightOf="@id/videodetail_rating"
app:layout_constraintStart_toEndOf="@id/videodetail_rating"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:layout_marginLeft="10dp"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginStart="10dp"
android:layout_width="0dp"
android:layout_height="wrap_content" />
android:layout_height="wrap_content"
app:layout_constraintWidth_max="500dp"
app:layout_constraintHorizontal_chainStyle="spread_inside"/>
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -611,15 +601,13 @@
<FrameLayout
android:id="@+id/videodetail_quality_overview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:elevation="100dp"
android:visibility="gone" />
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="gone"/>
<FrameLayout
android:id="@+id/overlay_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:elevation="100dp"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="gone" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.futo.platformplayer.views.containers.CustomMotionLayout>
+1 -2
View File
@@ -7,8 +7,7 @@
android:layout_height="match_parent"
android:layout_gravity="bottom"
android:layoutDirection="ltr"
android:orientation="vertical"
tools:targetApi="28">
android:orientation="vertical">
<ImageButton
android:id="@+id/button_minimize"
@@ -2,7 +2,7 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layoutDirection="ltr"
android:orientation="vertical">
@@ -10,7 +10,7 @@
<com.futo.platformplayer.views.behavior.TouchInterceptFrameLayout
android:id="@+id/layout_bar"
android:layout_width="match_parent"
android:layout_height="12dp"
android:layout_height="wrap_content"
app:shouldInterceptTouches="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
+4 -5
View File
@@ -7,14 +7,14 @@
android:background="@color/transparent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.media3.ui.PlayerView
android:paddingBottom="7dp"
android:id="@+id/video_player"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:default_artwork="@drawable/placeholder_video_thumbnail"
app:use_artwork="true"
app:use_controller="false"
app:show_buffering="always"
android:layout_marginBottom="6dp" />
app:show_buffering="always"/>
<!--
<androidx.media3.ui.PlayerControlView
android:id="@+id/video_player_bar"
@@ -28,8 +28,7 @@
android:id="@+id/layout_controls_background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#55000000"
android:layout_marginBottom="6dp">
android:background="#55000000">
</FrameLayout>
<FrameLayout
@@ -71,4 +70,4 @@
android:layout_height="match_parent"
android:visibility="gone" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -9,7 +9,7 @@
android:layout_above="@+id/short_player_progress_bar"
android:background="@color/black"
app:default_artwork="@drawable/placeholder_video_thumbnail"
app:resize_mode="zoom"
app:resize_mode="fill"
app:show_buffering="when_playing"
app:use_artwork="true"
app:use_controller="false" />
-15
View File
@@ -704,19 +704,4 @@
<item>Newest</item>
<item>Oldest</item>
</string-array>
<string-array name="app_languages">
<item>النظام</item>
<item>الإنجليزية (EN)</item>
<item>الألمانية (DE)</item>
<item>الإسبانية (ES)</item>
<item>البرتغالية (PT)</item>
<item>الفرنسية (FR)</item>
<item>اليابانية (JA)</item>
<item>الكورية (KO)</item>
<item>الصينية (ZH)</item>
<item>الروسية (RU)</item>
<item>العربية (AR)</item>
<item>الإيطالية (IT)</item>
<item>التركية (TR)</item>
</string-array>
</resources>
-15
View File
@@ -704,19 +704,4 @@
<item>Newest</item>
<item>Oldest</item>
</string-array>
<string-array name="app_languages">
<item>System</item>
<item>Englisch (EN)</item>
<item>Deutsch (DE)</item>
<item>Spanisch (ES)</item>
<item>Portugiesisch (PT)</item>
<item>Französisch (FR)</item>
<item>Japanisch (JA)</item>
<item>Koreanisch (KO)</item>
<item>Chinesisch (ZH)</item>
<item>Russisch (RU)</item>
<item>Arabisch (AR)</item>
<item>Italienisch (IT)</item>
<item>Türkisch (TR)</item>
</string-array>
</resources>
-15
View File
@@ -714,19 +714,4 @@
<item>Newest</item>
<item>Oldest</item>
</string-array>
<string-array name="app_languages">
<item>Sistema</item>
<item>Inglés (EN)</item>
<item>Alemán (DE)</item>
<item>Español (ES)</item>
<item>Portugués (PT)</item>
<item>Francés (FR)</item>
<item>Japonés (JA)</item>
<item>Coreano (KO)</item>
<item>Chino (ZH)</item>
<item>Ruso (RU)</item>
<item>Árabe (AR)</item>
<item>Italiano (IT)</item>
<item>Turco (TR)</item>
</string-array>
</resources>
-15
View File
@@ -712,19 +712,4 @@
<item>Newest</item>
<item>Oldest</item>
</string-array>
<string-array name="app_languages">
<item>Système</item>
<item>Anglais (EN)</item>
<item>Allemand (DE)</item>
<item>Espagnol (ES)</item>
<item>Portugais (PT)</item>
<item>Français (FR)</item>
<item>Japonais (JA)</item>
<item>Coréen (KO)</item>
<item>Chinois (ZH)</item>
<item>Russe (RU)</item>
<item>Arabe (AR)</item>
<item>Italien (IT)</item>
<item>Turc (TR)</item>
</string-array>
</resources>
File diff suppressed because it is too large Load Diff
-15
View File
@@ -704,19 +704,4 @@
<item>Newest</item>
<item>Oldest</item>
</string-array>
<string-array name="app_languages">
<item>システム</item>
<item>英語 (EN)</item>
<item>ドイツ語 (DE)</item>
<item>スペイン語 (ES)</item>
<item>ポルトガル語 (PT)</item>
<item>フランス語 (FR)</item>
<item>日本語 (JA)</item>
<item>韓国語 (KO)</item>
<item>中国語 (ZH)</item>
<item>ロシア語 (RU)</item>
<item>アラビア語 (AR)</item>
<item>イタリア語 (IT)</item>
<item>トルコ語 (TR)</item>
</string-array>
</resources>
-15
View File
@@ -704,19 +704,4 @@
<item>Newest</item>
<item>Oldest</item>
</string-array>
<string-array name="app_languages">
<item>시스템</item>
<item>영어 (EN)</item>
<item>독일어 (DE)</item>
<item>스페인어 (ES)</item>
<item>포르투갈어 (PT)</item>
<item>프랑스어 (FR)</item>
<item>일본어 (JA)</item>
<item>한국어 (KO)</item>
<item>중국어 (ZH)</item>
<item>러시아어 (RU)</item>
<item>아랍어 (AR)</item>
<item>이탈리아어 (IT)</item>
<item>터키어 (TR)</item>
</string-array>
</resources>
-15
View File
@@ -704,19 +704,4 @@
<item>Newest</item>
<item>Oldest</item>
</string-array>
<string-array name="app_languages">
<item>Sistema</item>
<item>Inglês (EN)</item>
<item>Alemão (DE)</item>
<item>Espanhol (ES)</item>
<item>Português (PT)</item>
<item>Francês (FR)</item>
<item>Japonês (JA)</item>
<item>Coreano (KO)</item>
<item>Chinês (ZH)</item>
<item>Russo (RU)</item>
<item>Árabe (AR)</item>
<item>Italiano (IT)</item>
<item>Turco (TR)</item>
</string-array>
</resources>
-15
View File
@@ -704,19 +704,4 @@
<item>Newest</item>
<item>Oldest</item>
</string-array>
<string-array name="app_languages">
<item>Система</item>
<item>Английский (EN)</item>
<item>Немецкий (DE)</item>
<item>Испанский (ES)</item>
<item>Португальский (PT)</item>
<item>Французский (FR)</item>
<item>Японский (JA)</item>
<item>Корейский (KO)</item>
<item>Китайский (ZH)</item>
<item>Русский (RU)</item>
<item>Арабский (AR)</item>
<item>Итальянский (IT)</item>
<item>Турецкий (TR)</item>
</string-array>
</resources>
File diff suppressed because it is too large Load Diff
-15
View File
@@ -704,19 +704,4 @@
<item>Newest</item>
<item>Oldest</item>
</string-array>
<string-array name="app_languages">
<item>系统</item>
<item>英语 (EN)</item>
<item>德语 (DE)</item>
<item>西班牙语 (ES)</item>
<item>葡萄牙语 (PT)</item>
<item>法语 (FR)</item>
<item>日语 (JA)</item>
<item>韩语 (KO)</item>
<item>中文 (ZH)</item>
<item>俄语 (RU)</item>
<item>阿拉伯语 (AR)</item>
<item>意大利语 (IT)</item>
<item>土耳其语 (TR)</item>
</string-array>
</resources>
-14
View File
@@ -82,8 +82,6 @@
<string name="allow_ipv6_description">If casting over IPV6 is allowed, can cause issues on some networks</string>
<string name="allow_ipv4">Allow Link Local IPV4</string>
<string name="allow_ipv4_description">If casting over IPV4 link local is allowed, can cause issues on some networks</string>
<string name="experimental_cast">Experimental</string>
<string name="experimental_cast_description">Use experimental casting backend (requires restart)</string>
<string name="discover">Discover</string>
<string name="find_new_video_sources_to_add">Find new video sources to add</string>
<string name="these_sources_have_been_disabled">These sources have been disabled</string>
@@ -340,8 +338,6 @@
<string name="test_background_worker">Test Background Worker</string>
<string name="test_background_worker_description"></string>
<string name="clear_payment">Clear Payment</string>
<string name="configure_sync_server">Configure Sync Server</string>
<string name="configure_sync_server_description">Allows you to change the Sync Server to a self-hosted one.</string>
<string name="clears_cookies_when_you_log_out">Clears cookies when you log out</string>
<string name="clears_in_app_browser_cookies">Clears in-app browser cookies</string>
<string name="configure_browsing_behavior">Configure browsing behavior</string>
@@ -442,9 +438,6 @@
<string name="delete_watchlist_on_finish_description">After you leave a video that you mostly watched, it will be removed from watch later.</string>
<string name="shorts_pregenerate">Pre-generate shorts sources</string>
<string name="shorts_pregenerate_description">Generates short sources (when applicable) one video ahead</string>
<string name="shorts_fit_video">Fit Shorts Video</string>
<string name="shorts_fit_video_description">Will scale the video to fit the view, instead of filling the view properly.</string>
<string name="shorts_fit_video_warning">This setting will require you to reboot Grayjay.</string>
<string name="seek_offset">Seek duration</string>
<string name="min_playback_speed">Minimum Playback Speed</string>
<string name="min_playback_speed_description">Minimum Available Speed</string>
@@ -480,7 +473,6 @@
<string name="number_of_concurrent_threads_to_multiply_download_speeds_from_throttled_sources">Number of concurrent threads to multiply download speeds from throttled sources</string>
<string name="payment">Payment</string>
<string name="payment_status">Payment Status</string>
<string name="relay_server">Sync Relay Server</string>
<string name="bypass_rotation_prevention">Bypass Rotation Prevention</string>
<string name="playlist_delete_confirmation">Playlist Delete Confirmation</string>
<string name="playlist_delete_confirmation_description">Show confirmation dialog when deleting media from a playlist</string>
@@ -1060,8 +1052,6 @@
<item>Chinese (ZH)</item>
<item>Russian (RU)</item>
<item>Arabic (AR)</item>
<item>Italian (IT)</item>
<item>Turkish (TR)</item>
</string-array>
<string-array name="player_background_behavior">
<item>None</item>
@@ -1106,10 +1096,6 @@
<item>ChromeCast</item>
<item>AirPlay</item>
</string-array>
<string-array name="exp_casting_device_type_array" translatable="false">
<item>FCast</item>
<item>ChromeCast</item>
</string-array>
<string-array name="log_levels">
<item>None</item>
<item>Error</item>
+154 -182
View File
@@ -3,208 +3,180 @@
xmlns:app="http://schemas.android.com/apk/res-auto">
<Transition
android:id="@+id/maximize"
app:constraintSetEnd="@id/expanded"
app:constraintSetStart="@id/collapsed"
app:duration="300"
app:motionInterpolator="easeInOut">
app:duration="300">
<OnSwipe
app:dragDirection="dragUp"
app:maxAcceleration="200"
app:touchAnchorId="@+id/touchContainer"
app:nestedScrollFlags="disableScroll"
app:touchAnchorId="@id/layout_player_container"
app:touchAnchorSide="top" />
<KeyFrameSet>
<!--
<KeyAttribute
android:alpha="0"
app:framePosition="0"
app:motionTarget="@id/contentContainer" />
<KeyAttribute
android:alpha="1"
app:framePosition="100"
app:motionTarget="@id/contentContainer" /> -->
<KeyAttribute
app:framePosition="3"
app:motionTarget="@id/touchContainer">
<CustomAttribute
app:attributeName="cardElevation"
app:customDimension="0dp" />
</KeyAttribute>
<!--Minimize Progress-->
<KeyAttribute
app:framePosition="0"
app:motionTarget="@id/fragview_videodetail">
<CustomAttribute
app:attributeName="MinimizeProgress"
app:customFloatValue="0" />
</KeyAttribute>
<KeyAttribute
app:framePosition="100"
app:motionTarget="@id/fragview_videodetail">
<CustomAttribute
app:attributeName="MinimizeProgress"
app:customFloatValue="1" />
</KeyAttribute>
<!--Controller Alpha-->
<KeyAttribute
app:framePosition="0"
app:motionTarget="@id/fragview_videodetail">
<CustomAttribute
app:attributeName="ControllerAlpha"
app:customFloatValue="0" />
</KeyAttribute>
<KeyAttribute
app:framePosition="100"
app:motionTarget="@id/fragview_videodetail">
<CustomAttribute
app:attributeName="ControllerAlpha"
app:customFloatValue="1" />
</KeyAttribute>
<!--Content Alpha-->
<KeyAttribute
app:framePosition="0"
app:motionTarget="@id/fragview_videodetail">
<CustomAttribute
app:attributeName="ContentAlpha"
app:customFloatValue="0" />
</KeyAttribute>
<KeyAttribute
app:framePosition="30"
app:motionTarget="@id/fragview_videodetail">
<CustomAttribute
app:attributeName="ContentAlpha"
app:customFloatValue="0" />
</KeyAttribute>
<KeyAttribute
app:framePosition="100"
app:motionTarget="@id/fragview_videodetail">
<CustomAttribute
app:attributeName="ContentAlpha"
app:customFloatValue="1" />
</KeyAttribute>
<!--MinimizeControlsAlpha Alpha -->
<KeyAttribute
app:framePosition="0"
app:motionTarget="@id/fragview_videodetail">
<CustomAttribute
app:attributeName="MinimizeControlsAlpha"
app:customFloatValue="1" />
</KeyAttribute>
<KeyAttribute
app:framePosition="20"
app:motionTarget="@id/fragview_videodetail">
<CustomAttribute
app:attributeName="MinimizeControlsAlpha"
app:customFloatValue="0" />
</KeyAttribute>
<KeyAttribute
app:framePosition="100"
app:motionTarget="@id/fragview_videodetail">
<CustomAttribute
app:attributeName="MinimizeControlsAlpha"
app:customFloatValue="0" />
</KeyAttribute>
<!--Padding Right-->
<KeyAttribute
app:framePosition="0"
app:motionTarget="@id/fragview_videodetail">
<CustomAttribute
app:attributeName="VideoMinimize"
app:customFloatValue="1" />
</KeyAttribute>
<KeyAttribute
app:framePosition="20"
app:motionTarget="@id/fragview_videodetail">
<CustomAttribute
app:attributeName="VideoMinimize"
app:customFloatValue="0" />
</KeyAttribute>
<KeyAttribute
app:framePosition="100"
app:motionTarget="@id/fragview_videodetail">
<CustomAttribute
app:attributeName="VideoMinimize"
app:customFloatValue="0" />
</KeyAttribute>
<!--Padding Top-->
<KeyAttribute
app:framePosition="0"
app:motionTarget="@id/fragview_videodetail">
<CustomAttribute
app:attributeName="TopPadding"
app:customDimension="1dp" />
</KeyAttribute>
<KeyAttribute
app:framePosition="100"
app:motionTarget="@id/fragview_videodetail">
<CustomAttribute
app:attributeName="TopPadding"
app:customDimension="0dp" />
</KeyAttribute>
</KeyFrameSet>
<!--pretty sure this isn't doing anything right now-->
<OnClick
app:clickAction="transitionToEnd"
app:targetId="@id/layout_player_container" />
</Transition>
<ConstraintSet android:id="@+id/expanded">
<Transition
android:id="@+id/full_screen"
app:constraintSetEnd="@id/full_screen_gesture"
app:constraintSetStart="@id/expanded"
app:duration="300">
<Constraint
android:id="@id/touchContainer"
android:layout_width="match_parent"
android:layout_height="220dp"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:layout_marginBottom="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Constraint
android:id="@id/fragview_videodetail"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:layout_marginBottom="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</ConstraintSet>
<OnSwipe
app:dragDirection="dragUp"
app:maxAcceleration="200"
app:nestedScrollFlags="disableScroll"
app:onTouchUp="autoCompleteToStart"
app:touchAnchorId="@id/layout_player_container"
app:touchAnchorSide="bottom" />
</Transition>
<ConstraintSet android:id="@+id/collapsed">
<Constraint
android:id="@id/touchContainer"
android:id="@id/layout_player_container"
android:layout_height="60dp"
android:layout_width="match_parent"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:layout_marginBottom="48dp"
android:layout_marginBottom="47dp"
android:elevation="3dp"
android:paddingBottom="6dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/minimize_controls"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintHorizontal_weight="150"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintWidth_max="150dp" />
<Constraint
android:id="@id/contentContainer"
android:elevation="1dp"
android:orientation="vertical"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/layout_player_container" />
<Constraint
android:id="@id/fragview_videodetail"
android:id="@id/minimize_controls"
android:layout_width="0dp"
android:layout_height="60dp"
android:layout_width="match_parent"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:layout_marginBottom="48dp"
app:layout_constraintBottom_toBottomOf="parent"
android:elevation="1dp"
android:visibility="visible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintHorizontal_weight="350"
app:layout_constraintStart_toEndOf="@id/layout_player_container"
app:layout_constraintTop_toTopOf="@id/layout_player_container"
app:layout_constraintWidth_max="350dp" />
<Constraint
android:id="@id/videodetail_progress"
android:layout_height="wrap_content"
android:layout_marginTop="-12dp"
android:background="@color/black"
android:elevation="2dp"
app:layout_constraintEnd_toEndOf="@id/minimize_controls"
app:layout_constraintStart_toStartOf="@id/layout_player_container"
app:layout_constraintTop_toBottomOf="@id/layout_player_container" />
<Constraint
android:id="@id/videodetail_quality_overview"
app:visibilityMode="ignore"/>
<Constraint
android:id="@id/overlay_container"
app:visibilityMode="ignore"/>
</ConstraintSet>
</MotionScene>
<ConstraintSet android:id="@+id/expanded">
<Constraint
android:id="@id/layout_player_container"
android:layout_height="wrap_content"
android:layout_marginBottom="0dp"
android:elevation="2dp"
android:paddingBottom="6dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Constraint
android:id="@id/contentContainer"
android:elevation="2dp"
android:orientation="vertical"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/layout_player_container" />
<Constraint
android:id="@id/minimize_controls"
android:layout_width="0dp"
android:layout_height="60dp"
android:elevation="1dp"
android:visibility="invisible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/layout_player_container"
app:layout_constraintTop_toTopOf="@id/layout_player_container" />
<Constraint
android:id="@id/videodetail_progress"
android:layout_height="wrap_content"
android:layout_marginTop="-12dp"
android:background="@color/transparent"
android:elevation="1dp"
app:layout_constraintEnd_toEndOf="@id/minimize_controls"
app:layout_constraintStart_toStartOf="@id/layout_player_container"
app:layout_constraintTop_toBottomOf="@id/layout_player_container" />
<Constraint
android:id="@id/videodetail_quality_overview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:elevation="100dp"
android:visibility="gone"
app:visibilityMode="ignore"/>
<Constraint
android:id="@id/overlay_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:elevation="100dp"
android:visibility="gone"
app:visibilityMode="ignore"/>
</ConstraintSet>
<ConstraintSet android:id="@+id/full_screen_gesture">
<Constraint
android:id="@id/layout_player_container"
android:layout_height="wrap_content"
android:layout_marginTop="-130dp"
android:layout_marginBottom="0dp"
android:elevation="2dp"
android:paddingBottom="6dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Constraint
android:id="@id/contentContainer"
android:elevation="1dp"
android:orientation="vertical"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/layout_player_container" />
<Constraint
android:id="@id/minimize_controls"
android:layout_width="0dp"
android:layout_height="60dp"
android:elevation="1dp"
android:visibility="invisible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/layout_player_container"
app:layout_constraintTop_toTopOf="@id/layout_player_container" />
<Constraint
android:id="@id/videodetail_progress"
android:layout_height="wrap_content"
android:layout_marginTop="-12dp"
android:background="@color/transparent"
android:elevation="1dp"
app:layout_constraintEnd_toEndOf="@id/minimize_controls"
app:layout_constraintStart_toStartOf="@id/layout_player_container"
app:layout_constraintTop_toBottomOf="@id/layout_player_container" />
</ConstraintSet>
</MotionScene>
-10
View File
@@ -8,16 +8,6 @@
<receiver android:name=".receivers.InstallReceiver" />
<activity android:name=".activities.MainActivity" android:launchMode="singleInstance">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.OPENABLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="video/*" />
<data android:mimeType="audio/*" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />