mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 04:52:39 +02:00
Merge branch 'master' into aw/polycentric-profiles
This commit is contained in:
+42
-44
@@ -1,8 +1,8 @@
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.21'
|
||||
id 'org.ajoberstar.grgit' version '5.2.2'
|
||||
id 'org.jetbrains.kotlin.plugin.serialization' version '2.2.21'
|
||||
id 'org.ajoberstar.grgit' version '5.3.3'
|
||||
id 'com.google.protobuf'
|
||||
id 'kotlin-parcelize'
|
||||
id 'com.google.devtools.ksp'
|
||||
@@ -97,7 +97,7 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
minSdk 28
|
||||
targetSdk 35
|
||||
targetSdk 36
|
||||
versionCode gitVersionCode
|
||||
versionName gitVersionName
|
||||
|
||||
@@ -155,82 +155,80 @@ android {
|
||||
|
||||
dependencies {
|
||||
//implementation 'com.google.dagger:dagger:2.48'
|
||||
implementation 'androidx.test:monitor:1.7.2'
|
||||
implementation 'com.google.android.material:material:1.12.0'
|
||||
implementation 'androidx.test:monitor:1.8.0'
|
||||
implementation 'com.google.android.material:material:1.13.0'
|
||||
//annotationProcessor 'com.google.dagger:dagger-compiler:2.48'
|
||||
|
||||
//Core
|
||||
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.core:core-ktx:1.17.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.7.1'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
|
||||
implementation 'androidx.documentfile:documentfile:1.1.0'
|
||||
|
||||
//Images
|
||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
|
||||
implementation 'com.github.bumptech.glide:glide:4.16.0'
|
||||
annotationProcessor 'com.github.bumptech.glide:compiler:5.0.5'
|
||||
implementation 'com.github.bumptech.glide:glide:5.0.5'
|
||||
|
||||
//Async
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2"
|
||||
|
||||
//HTTP
|
||||
implementation "com.squareup.okhttp3:okhttp:4.11.0"
|
||||
implementation "com.squareup.okhttp3:okhttp:5.3.0"
|
||||
|
||||
//JSON
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2" //Used for structured json
|
||||
implementation 'com.google.code.gson:gson:2.10.1' //Used for complex/anonymous cases like during development conversions (eg. V8RemoteObject)
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0" //Used for structured json
|
||||
implementation 'com.google.code.gson:gson:2.13.2' //Used for complex/anonymous cases like during development conversions (eg. V8RemoteObject)
|
||||
|
||||
//JS
|
||||
implementation("com.caoccao.javet:javet-android:3.0.2")
|
||||
//implementation 'com.caoccao.javet:javet-v8-android:4.1.4' //Change after extensive testing the freezing edge cases are solved.
|
||||
implementation 'com.caoccao.javet:javet-v8-android:5.0.1'
|
||||
|
||||
//Exoplayer
|
||||
implementation 'androidx.media3:media3-exoplayer:1.2.1'
|
||||
implementation 'androidx.media3:media3-exoplayer-dash:1.2.1'
|
||||
implementation 'androidx.media3:media3-ui:1.2.1'
|
||||
implementation 'androidx.media3:media3-exoplayer-hls:1.2.1'
|
||||
implementation 'androidx.media3:media3-exoplayer-rtsp:1.2.1'
|
||||
implementation 'androidx.media3:media3-exoplayer-smoothstreaming:1.2.1'
|
||||
implementation 'androidx.media3:media3-transformer:1.2.1'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.6'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.7.6'
|
||||
implementation 'androidx.media:media:1.7.0'
|
||||
implementation 'androidx.media3:media3-exoplayer:1.8.0'
|
||||
implementation 'androidx.media3:media3-exoplayer-dash:1.8.0'
|
||||
implementation 'androidx.media3:media3-ui:1.8.0'
|
||||
implementation 'androidx.media3:media3-exoplayer-hls:1.8.0'
|
||||
implementation 'androidx.media3:media3-exoplayer-rtsp:1.8.0'
|
||||
implementation 'androidx.media3:media3-exoplayer-smoothstreaming:1.8.0'
|
||||
implementation 'androidx.media3:media3-transformer:1.8.0'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.9.6'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.9.6'
|
||||
implementation 'androidx.media:media:1.7.1'
|
||||
|
||||
//Other
|
||||
implementation 'org.jsoup:jsoup:1.15.3'
|
||||
implementation 'org.jsoup:jsoup:1.21.2'
|
||||
implementation 'com.google.android.flexbox:flexbox:3.0.0'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
implementation fileTree(dir: 'aar', include: ['*.aar'])
|
||||
implementation 'com.arthenica:smart-exception-java:0.2.1'
|
||||
implementation 'org.jetbrains.kotlin:kotlin-reflect:1.9.0'
|
||||
implementation 'org.jetbrains.kotlin:kotlin-reflect:2.2.0'
|
||||
implementation 'com.github.dhaval2404:imagepicker:2.1'
|
||||
implementation 'com.google.zxing:core:3.4.1'
|
||||
implementation 'com.google.zxing:core:3.5.3'
|
||||
implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
|
||||
implementation 'com.caverock:androidsvg-aar:1.4'
|
||||
|
||||
//Protobuf
|
||||
implementation 'com.google.protobuf:protobuf-javalite:3.25.1'
|
||||
implementation 'com.google.protobuf:protobuf-javalite:4.33.0'
|
||||
|
||||
implementation 'com.polycentric.core:app:1.0'
|
||||
implementation 'com.futo.futopay:app:1.0'
|
||||
implementation 'androidx.work:work-runtime-ktx:2.9.0'
|
||||
implementation 'androidx.concurrent:concurrent-futures-ktx:1.1.0'
|
||||
implementation 'androidx.work:work-runtime-ktx:2.11.0'
|
||||
implementation 'androidx.concurrent:concurrent-futures-ktx:1.3.0'
|
||||
|
||||
//Database
|
||||
implementation("androidx.room:room-runtime:2.6.1")
|
||||
annotationProcessor("androidx.room:room-compiler:2.6.1")
|
||||
ksp("androidx.room:room-compiler:2.6.1")
|
||||
implementation("androidx.room:room-ktx:2.6.1")
|
||||
implementation("androidx.room:room-runtime:2.8.3")
|
||||
ksp("androidx.room:room-compiler:2.8.3")
|
||||
implementation("androidx.room:room-ktx:2.8.3")
|
||||
|
||||
//Payment
|
||||
implementation 'com.stripe:stripe-android:20.35.1'
|
||||
implementation 'com.stripe:stripe-android:22.0.0'
|
||||
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
|
||||
testImplementation "org.jetbrains.kotlin:kotlin-test:1.8.22"
|
||||
testImplementation "org.xmlunit:xmlunit-core:2.9.1"
|
||||
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'
|
||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2'
|
||||
testImplementation "org.jetbrains.kotlin:kotlin-test:2.0.21"
|
||||
testImplementation "org.xmlunit:xmlunit-core:2.11.0"
|
||||
testImplementation "org.mockito:mockito-core:5.20.0"
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.3.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0'
|
||||
|
||||
//Rust casting SDK
|
||||
implementation('org.futo.gitlab.videostreaming.fcast-sdk-jitpack:sender-sdk-minimal:0.3.1') {
|
||||
|
||||
@@ -408,6 +408,10 @@ class Settings : FragmentedStorageFileJson() {
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
@FormField(R.string.sticky_subtitles, FieldForm.TOGGLE, R.string.sticky_subtitles_description, -1)
|
||||
var stickySubtitles: Boolean = true;
|
||||
|
||||
@FormField(R.string.prefer_original_audio, FieldForm.TOGGLE, R.string.prefer_original_audio_description, -1)
|
||||
var preferOriginalAudio: Boolean = true;
|
||||
|
||||
|
||||
@@ -107,10 +107,9 @@ class AddSourceActivity : AppCompatActivity() {
|
||||
onNewIntent(intent);
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
var url = intent?.dataString;
|
||||
|
||||
var url = intent.dataString;
|
||||
if(url == null)
|
||||
UIDialogs.showDialog(this, R.drawable.ic_error, getString(R.string.no_valid_url_provided), null, null,
|
||||
0, UIDialogs.Action(getString(R.string.ok), { finish() }, UIDialogs.ActionStyle.PRIMARY));
|
||||
|
||||
@@ -708,17 +708,13 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
_wasStopped = true;
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent);
|
||||
handleIntent(intent);
|
||||
}
|
||||
|
||||
private fun handleIntent(intent: Intent?) {
|
||||
if (intent == null)
|
||||
return;
|
||||
private fun handleIntent(intent: Intent) {
|
||||
Logger.i(TAG, "handleIntent started by " + intent.action);
|
||||
|
||||
|
||||
var targetData: String? = null;
|
||||
|
||||
when (intent.action) {
|
||||
|
||||
@@ -54,14 +54,16 @@ interface IPlatformChannelContent : IPlatformContent {
|
||||
val subscribers: Long?
|
||||
}
|
||||
|
||||
open class JSChannelContent : JSContent, IPlatformChannelContent {
|
||||
override val contentType: ContentType get() = ContentType.CHANNEL
|
||||
override val thumbnail: String?
|
||||
override val subscribers: Long?
|
||||
open class JSChannelContent(
|
||||
config: SourcePluginConfig,
|
||||
obj: V8ValueObject
|
||||
) : JSContent(config, obj), IPlatformChannelContent {
|
||||
|
||||
constructor(config: SourcePluginConfig, obj: V8ValueObject) : super(config, obj) {
|
||||
val contextName = "Channel";
|
||||
thumbnail = obj.getOrDefault<String>(config, "thumbnail", contextName, null)
|
||||
subscribers = if(obj.has("subscribers")) obj.getOrThrow(config,"subscribers", contextName) else null
|
||||
}
|
||||
}
|
||||
final override val contentType: ContentType = ContentType.CHANNEL
|
||||
|
||||
override val thumbnail: String? =
|
||||
_content.getOrDefault<String>(_pluginConfig, "thumbnail", "Channel", null)
|
||||
|
||||
override val subscribers: Long? =
|
||||
_content.getOrDefault<Long>(_pluginConfig, "subscribers", "Channel", null)?.toLong()
|
||||
}
|
||||
|
||||
+11
-21
@@ -6,25 +6,15 @@ import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
open class PlatformComment : IPlatformComment {
|
||||
override val contextUrl: String;
|
||||
override val author: PlatformAuthorLink;
|
||||
override val message: String;
|
||||
override val rating: IRating;
|
||||
override val date: OffsetDateTime;
|
||||
open class PlatformComment(
|
||||
override val contextUrl: String,
|
||||
override val author: PlatformAuthorLink,
|
||||
override val message: String,
|
||||
override val rating: IRating,
|
||||
override val date: OffsetDateTime,
|
||||
override val replyCount: Int? = null
|
||||
) : IPlatformComment {
|
||||
|
||||
override val replyCount: Int?;
|
||||
|
||||
constructor(contextUrl: String, author: PlatformAuthorLink, msg: String, rating: IRating, date: OffsetDateTime, replyCount: Int? = null) {
|
||||
this.contextUrl = contextUrl;
|
||||
this.author = author;
|
||||
this.message = msg;
|
||||
this.rating = rating;
|
||||
this.date = date;
|
||||
this.replyCount = replyCount;
|
||||
}
|
||||
|
||||
override fun getReplies(client: IPlatformClient): IPager<IPlatformComment> {
|
||||
return NoCommentsPager();
|
||||
}
|
||||
}
|
||||
override fun getReplies(client: IPlatformClient): IPager<IPlatformComment> =
|
||||
NoCommentsPager()
|
||||
}
|
||||
|
||||
+1
@@ -41,6 +41,7 @@ class HLSVariantSubtitleUrlSource(
|
||||
override val format: String,
|
||||
) : ISubtitleSource {
|
||||
override val hasFetch: Boolean = false
|
||||
override val language: String? = null
|
||||
|
||||
override fun getSubtitles(): String? {
|
||||
return null
|
||||
|
||||
+4
-1
@@ -9,13 +9,15 @@ class LocalSubtitleSource : ISubtitleSource {
|
||||
override val name: String;
|
||||
override val url: String?;
|
||||
override val format: String?;
|
||||
override val language: String?
|
||||
override val hasFetch: Boolean get() = false;
|
||||
|
||||
val filePath: String;
|
||||
|
||||
constructor(name: String, format: String?, filePath: String) {
|
||||
constructor(name: String, language: String?, format: String?, filePath: String) {
|
||||
this.name = name;
|
||||
this.format = format;
|
||||
this.language = language
|
||||
this.filePath = filePath;
|
||||
this.url = Uri.fromFile(File(filePath)).toString();
|
||||
}
|
||||
@@ -32,6 +34,7 @@ class LocalSubtitleSource : ISubtitleSource {
|
||||
fun fromSource(source: SubtitleRawSource, path: String): LocalSubtitleSource {
|
||||
return LocalSubtitleSource(
|
||||
source.name,
|
||||
source.language,
|
||||
source.format,
|
||||
path
|
||||
);
|
||||
|
||||
+1
@@ -6,6 +6,7 @@ import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
||||
@kotlinx.serialization.Serializable
|
||||
class SubtitleRawSource(
|
||||
override val name: String,
|
||||
override val language: String?,
|
||||
override val format: String?,
|
||||
val _subtitles: String,
|
||||
override val url: String? = null,
|
||||
|
||||
+1
@@ -7,6 +7,7 @@ interface ISubtitleSource {
|
||||
val url: String?;
|
||||
val format: String?;
|
||||
val hasFetch: Boolean;
|
||||
val language: String?
|
||||
|
||||
fun getSubtitles(): String?;
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ open class JSClient : IPlatformClient {
|
||||
|
||||
override val id: String get() = config.id;
|
||||
override val name: String get() = config.name;
|
||||
override val icon: ImageVariable;
|
||||
override val icon: ImageVariable get() = StatePlatform.instance.getPlatformIcon(config.id) ?: ImageVariable(config.absoluteIconUrl, null, null)
|
||||
override var capabilities: PlatformClientCapabilities = PlatformClientCapabilities();
|
||||
|
||||
private var _busyAction = "";
|
||||
@@ -147,7 +147,6 @@ open class JSClient : IPlatformClient {
|
||||
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String? = null) {
|
||||
this._context = context;
|
||||
this.config = descriptor.config;
|
||||
icon = StatePlatform.instance.getPlatformIcon(config.id) ?: ImageVariable(config.absoluteIconUrl, null, null);
|
||||
this.descriptor = descriptor;
|
||||
_injectedSaveState = saveState;
|
||||
_auth = descriptor.getAuth();
|
||||
@@ -178,7 +177,6 @@ open class JSClient : IPlatformClient {
|
||||
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String?, script: String, withoutCredentials: Boolean = false) {
|
||||
this._context = context;
|
||||
this.config = descriptor.config;
|
||||
icon = StatePlatform.instance.getPlatformIcon(config.id) ?: ImageVariable(config.absoluteIconUrl, null, null);
|
||||
this.descriptor = descriptor;
|
||||
_injectedSaveState = saveState;
|
||||
if(!withoutCredentials)
|
||||
|
||||
+16
-11
@@ -23,17 +23,22 @@ import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.getOrThrowNullableList
|
||||
import com.futo.platformplayer.states.StateDeveloper
|
||||
|
||||
open class JSArticle : JSContent, IPlatformArticle, IPluginSourced {
|
||||
final override val contentType: ContentType get() = ContentType.ARTICLE;
|
||||
open class JSArticle(
|
||||
config: SourcePluginConfig,
|
||||
obj: V8ValueObject
|
||||
) : JSContent(config, obj), IPlatformArticle, IPluginSourced {
|
||||
|
||||
override val summary: String;
|
||||
override val thumbnails: Thumbnails?;
|
||||
final override val contentType: ContentType = ContentType.ARTICLE
|
||||
|
||||
constructor(config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) {
|
||||
val contextName = "PlatformArticle";
|
||||
override val summary: String =
|
||||
obj.getOrDefault<String>(config, "summary", "PlatformArticle", "") ?: ""
|
||||
|
||||
summary = _content.getOrDefault(config, "summary", contextName, "") ?: "";
|
||||
thumbnails = Thumbnails.fromV8(config, _content.getOrThrow(config, "thumbnails", contextName));
|
||||
|
||||
}
|
||||
}
|
||||
override val thumbnails: Thumbnails? =
|
||||
if (obj.has("thumbnails"))
|
||||
Thumbnails.fromV8(
|
||||
config,
|
||||
obj.getOrThrow<V8ValueObject>(config, "thumbnails", "PlatformArticle")
|
||||
)
|
||||
else
|
||||
null
|
||||
}
|
||||
|
||||
+24
-23
@@ -24,36 +24,37 @@ import com.futo.platformplayer.getOrThrowNullableList
|
||||
import com.futo.platformplayer.invokeV8
|
||||
import com.futo.platformplayer.states.StateDeveloper
|
||||
|
||||
open class JSArticleDetails : JSContent, IPlatformArticleDetails, IPluginSourced, IPlatformContentDetails {
|
||||
final override val contentType: ContentType get() = ContentType.ARTICLE;
|
||||
open class JSArticleDetails(
|
||||
private val client: JSClient,
|
||||
obj: V8ValueObject
|
||||
) : JSContent(client.config, obj), IPlatformArticleDetails, IPluginSourced, IPlatformContentDetails {
|
||||
|
||||
private val _hasGetComments: Boolean;
|
||||
private val _hasGetContentRecommendations: Boolean;
|
||||
final override val contentType: ContentType = ContentType.ARTICLE
|
||||
|
||||
override val rating: IRating;
|
||||
private val _hasGetComments: Boolean = _content.has("getComments")
|
||||
private val _hasGetContentRecommendations: Boolean = _content.has("getContentRecommendations")
|
||||
|
||||
override val summary: String;
|
||||
override val thumbnails: Thumbnails?;
|
||||
override val segments: List<IJSArticleSegment>;
|
||||
override val rating: IRating =
|
||||
obj.getOrDefault<V8ValueObject>(client.config, "rating", "PlatformArticle", null)
|
||||
?.let { IRating.fromV8(client.config, it, "PlatformArticle") }
|
||||
?: RatingLikes(0)
|
||||
|
||||
constructor(client: JSClient, obj: V8ValueObject): super(client.config, obj) {
|
||||
val contextName = "PlatformArticle";
|
||||
override val summary: String =
|
||||
_content.getOrThrow(client.config, "summary", "PlatformArticle")
|
||||
|
||||
rating = obj.getOrDefault<V8ValueObject>(client.config, "rating", contextName, null)?.let { IRating.fromV8(client.config, it, contextName) } ?: RatingLikes(0);
|
||||
summary = _content.getOrThrow(client.config, "summary", contextName);
|
||||
if(_content.has("thumbnails"))
|
||||
thumbnails = Thumbnails.fromV8(client.config, _content.getOrThrow(client.config, "thumbnails", contextName));
|
||||
override val thumbnails: Thumbnails? =
|
||||
if (_content.has("thumbnails"))
|
||||
Thumbnails.fromV8(
|
||||
client.config,
|
||||
_content.getOrThrow(client.config, "thumbnails", "PlatformArticle")
|
||||
)
|
||||
else
|
||||
thumbnails = null;
|
||||
null
|
||||
|
||||
|
||||
segments = (obj.getOrThrowNullableList<V8ValueObject>(client.config, "segments", contextName)
|
||||
?.map { fromV8Segment(client, it) }
|
||||
?.filterNotNull() ?: listOf());
|
||||
|
||||
_hasGetComments = _content.has("getComments");
|
||||
_hasGetContentRecommendations = _content.has("getContentRecommendations");
|
||||
}
|
||||
override val segments: List<IJSArticleSegment> =
|
||||
obj.getOrThrowNullableList<V8ValueObject>(client.config, "segments", "PlatformArticle")
|
||||
?.mapNotNull { fromV8Segment(client, it) }
|
||||
?: emptyList()
|
||||
|
||||
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? {
|
||||
if(!_hasGetComments || _content.isClosed)
|
||||
|
||||
+34
-36
@@ -16,51 +16,49 @@ import java.time.LocalDateTime
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
|
||||
open class JSContent : IPlatformContent, IPluginSourced {
|
||||
protected val _pluginConfig: SourcePluginConfig;
|
||||
protected val _content : V8ValueObject;
|
||||
open class JSContent(
|
||||
protected val _pluginConfig: SourcePluginConfig,
|
||||
protected val _content: V8ValueObject
|
||||
) : IPlatformContent, IPluginSourced {
|
||||
|
||||
protected val _hasGetDetails: Boolean;
|
||||
override val contentType: ContentType = ContentType.UNKNOWN
|
||||
|
||||
override val contentType: ContentType get() = ContentType.UNKNOWN;
|
||||
protected val _hasGetDetails: Boolean = _content.has("getDetails")
|
||||
|
||||
override val id: PlatformID;
|
||||
override val name: String;
|
||||
override val author: PlatformAuthorLink;
|
||||
override val datetime: OffsetDateTime?;
|
||||
override val id: PlatformID =
|
||||
PlatformID.fromV8(_pluginConfig, _content.getOrThrow(_pluginConfig, "id", CTX))
|
||||
|
||||
override val url: String;
|
||||
override val shareUrl: String;
|
||||
override val name: String =
|
||||
HtmlCompat.fromHtml(
|
||||
_content.getOrThrow<String>(_pluginConfig, "name", CTX).decodeUnicode(),
|
||||
HtmlCompat.FROM_HTML_MODE_LEGACY
|
||||
).toString()
|
||||
|
||||
override val sourceConfig: SourcePluginConfig get() = _pluginConfig;
|
||||
override val author: PlatformAuthorLink =
|
||||
_content.getOrDefault<V8ValueObject>(_pluginConfig, "author", CTX, null)
|
||||
?.let { PlatformAuthorLink.fromV8(_pluginConfig, it) }
|
||||
?: PlatformAuthorLink.UNKNOWN
|
||||
|
||||
constructor(config: SourcePluginConfig, obj: V8ValueObject) {
|
||||
_pluginConfig = config;
|
||||
_content = obj;
|
||||
private val _epoch: Long? =
|
||||
_content.getOrDefault<Long>(_pluginConfig, "datetime", CTX, null)?.toLong()
|
||||
|
||||
val contextName = "PlatformContent";
|
||||
override val datetime: OffsetDateTime? =
|
||||
_epoch?.takeIf { it != 0L }?.let {
|
||||
OffsetDateTime.of(LocalDateTime.ofEpochSecond(it, 0, ZoneOffset.UTC), ZoneOffset.UTC)
|
||||
}
|
||||
|
||||
id = PlatformID.fromV8(_pluginConfig, _content.getOrThrow(config, "id", contextName));
|
||||
name = HtmlCompat.fromHtml(_content.getOrThrow<String>(config, "name", contextName).decodeUnicode(), HtmlCompat.FROM_HTML_MODE_LEGACY).toString();
|
||||
override val url: String =
|
||||
_content.getOrThrow<String>(_pluginConfig, "url", CTX)
|
||||
|
||||
val authorObj = _content.getOrDefault<V8ValueObject>(config, "author", contextName, null);
|
||||
if(authorObj != null)
|
||||
author = PlatformAuthorLink.fromV8(_pluginConfig, authorObj);
|
||||
else
|
||||
author = PlatformAuthorLink.UNKNOWN;
|
||||
override val shareUrl: String =
|
||||
_content.getOrDefault<String>(_pluginConfig, "shareUrl", CTX, null) ?: url
|
||||
|
||||
val datetimeInt = _content.getOrDefault<Int>(config, "datetime", contextName, null)?.toLong();
|
||||
if(datetimeInt == null || datetimeInt == 0.toLong())
|
||||
datetime = null;
|
||||
else
|
||||
datetime = OffsetDateTime.of(LocalDateTime.ofEpochSecond(datetimeInt, 0, ZoneOffset.UTC), ZoneOffset.UTC);
|
||||
url = _content.getOrThrow(config, "url", contextName);
|
||||
shareUrl = _content.getOrDefault<String>(config, "shareUrl", contextName, null) ?: url;
|
||||
override val sourceConfig: SourcePluginConfig
|
||||
get() = _pluginConfig
|
||||
|
||||
_hasGetDetails = _content.has("getDetails");
|
||||
fun getUnderlyingObject(): V8ValueObject? = _content
|
||||
|
||||
companion object {
|
||||
private const val CTX = "PlatformContent"
|
||||
}
|
||||
|
||||
fun getUnderlyingObject(): V8ValueObject? {
|
||||
return _content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
-10
@@ -6,14 +6,16 @@ import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
|
||||
open class JSPlaylist : JSContent, IPlatformPlaylist {
|
||||
override val contentType: ContentType get() = ContentType.PLAYLIST;
|
||||
override val thumbnail: String?;
|
||||
override val videoCount: Int;
|
||||
open class JSPlaylist(
|
||||
config: SourcePluginConfig,
|
||||
obj: V8ValueObject
|
||||
) : JSContent(config, obj), IPlatformPlaylist {
|
||||
|
||||
constructor(config: SourcePluginConfig, obj: V8ValueObject) : super(config, obj) {
|
||||
val contextName = "Playlist";
|
||||
thumbnail = obj.getOrDefault(config, "thumbnail", contextName, null);
|
||||
videoCount = obj.getOrDefault(config, "videoCount", contextName, -1)!!;
|
||||
}
|
||||
}
|
||||
override val contentType: ContentType = ContentType.PLAYLIST
|
||||
|
||||
override val thumbnail: String? =
|
||||
_content.getOrDefault<String>(_pluginConfig, "thumbnail", "Playlist", null)
|
||||
|
||||
override val videoCount: Int =
|
||||
_content.getOrDefault<Int>(_pluginConfig, "videoCount", "Playlist", null)?.toInt() ?: -1
|
||||
}
|
||||
|
||||
+2
@@ -22,6 +22,7 @@ class JSSubtitleSource : ISubtitleSource {
|
||||
override val name: String;
|
||||
override val url: String?;
|
||||
override val format: String?;
|
||||
override val language: String?
|
||||
override val hasFetch: Boolean;
|
||||
|
||||
constructor(config: SourcePluginConfig, v8Value: V8ValueObject) {
|
||||
@@ -29,6 +30,7 @@ class JSSubtitleSource : ISubtitleSource {
|
||||
|
||||
val context = "JSSubtitles";
|
||||
name = v8Value.getOrThrow(config, "name", context, false);
|
||||
language = v8Value.getOrThrow(config, "language", context, false);
|
||||
url = v8Value.getOrThrow(config, "url", context, true);
|
||||
format = v8Value.getOrThrow(config, "format", context, true);
|
||||
hasFetch = v8Value.has("getSubtitles");
|
||||
|
||||
+31
-30
@@ -8,43 +8,44 @@ import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
open class JSAudioUrlSource : IAudioUrlSource, JSSource {
|
||||
override val name: String;
|
||||
override val bitrate : Int;
|
||||
override val container : String;
|
||||
override val codec: String;
|
||||
private val url : String;
|
||||
open class JSAudioUrlSource(
|
||||
plugin: JSClient,
|
||||
obj: V8ValueObject
|
||||
) : JSSource(TYPE_AUDIOURL, plugin, obj), IAudioUrlSource {
|
||||
|
||||
override val language: String;
|
||||
private val ctx = "AudioUrlSource"
|
||||
private val cfg = plugin.config
|
||||
|
||||
override val duration: Long?;
|
||||
override val bitrate: Int =
|
||||
_obj.getOrThrow<Int>(cfg, "bitrate", ctx)
|
||||
|
||||
override var priority: Boolean = false;
|
||||
override val container: String =
|
||||
_obj.getOrThrow<String>(cfg, "container", ctx)
|
||||
|
||||
override var original: Boolean = false;
|
||||
override val codec: String =
|
||||
_obj.getOrThrow<String>(cfg, "codec", ctx)
|
||||
|
||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_AUDIOURL, plugin, obj) {
|
||||
val contextName = "AudioUrlSource";
|
||||
val config = plugin.config;
|
||||
private val url: String =
|
||||
_obj.getOrThrow<String>(cfg, "url", ctx)
|
||||
|
||||
bitrate = _obj.getOrThrow(config, "bitrate", contextName);
|
||||
container = _obj.getOrThrow(config, "container", contextName);
|
||||
codec = _obj.getOrThrow(config, "codec", contextName);
|
||||
url = _obj.getOrThrow(config, "url", contextName);
|
||||
language = _obj.getOrThrow(config, "language", contextName);
|
||||
duration = _obj.getOrDefault(config, "duration", contextName, null);
|
||||
override val language: String =
|
||||
_obj.getOrThrow<String>(cfg, "language", ctx)
|
||||
|
||||
name = _obj.getOrDefault(config, "name", contextName, "${container} ${bitrate}") ?: "${container} ${bitrate}";
|
||||
override val duration: Long? =
|
||||
_obj.getOrDefault<Long>(cfg, "duration", ctx, null)?.toLong()
|
||||
|
||||
priority = if(_obj.has("priority")) obj.getOrThrow(config, "priority", contextName) else false;
|
||||
original = if(_obj.has("original")) obj.getOrThrow(config, "original", contextName) else false;
|
||||
}
|
||||
override val name: String =
|
||||
_obj.getOrDefault<String>(cfg, "name", ctx, null)
|
||||
?: "$container $bitrate"
|
||||
|
||||
override fun getAudioUrl() : String {
|
||||
return url;
|
||||
}
|
||||
override var priority: Boolean =
|
||||
if (_obj.has("priority")) _obj.getOrThrow<Boolean>(cfg, "priority", ctx) else false
|
||||
|
||||
override fun toString(): String {
|
||||
return "(name=$name, container=$container, bitrate=$bitrate, codec=$codec, url=$url, language=$language, duration=$duration)";
|
||||
}
|
||||
}
|
||||
override var original: Boolean =
|
||||
if (_obj.has("original")) _obj.getOrThrow<Boolean>(cfg, "original", ctx) else false
|
||||
|
||||
override fun getAudioUrl(): String = url
|
||||
|
||||
override fun toString(): String =
|
||||
"(name=$name, container=$container, bitrate=$bitrate, codec=$codec, url=$url, language=$language, duration=$duration)"
|
||||
}
|
||||
|
||||
+41
-31
@@ -31,42 +31,52 @@ interface IJSDashManifestRawSource {
|
||||
fun generateAsync(scope: CoroutineScope): Deferred<String?>;
|
||||
fun generate(): String?;
|
||||
}
|
||||
open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSource, IStreamMetaDataSource {
|
||||
override val container : String;
|
||||
override val name : String;
|
||||
override val width: Int;
|
||||
override val height: Int;
|
||||
override val codec: String;
|
||||
override val bitrate: Int?;
|
||||
override val duration: Long;
|
||||
override val priority: Boolean;
|
||||
open class JSDashManifestRawSource(
|
||||
plugin: JSClient,
|
||||
obj: V8ValueObject
|
||||
) : JSSource(TYPE_DASH_RAW, plugin, obj), IVideoSource, IJSDashManifestRawSource, IStreamMetaDataSource {
|
||||
|
||||
val url: String?;
|
||||
override var manifest: String?;
|
||||
private val ctx = "DashRawSource"
|
||||
private val cfg = plugin.config
|
||||
|
||||
override val hasGenerate: Boolean;
|
||||
val canMerge: Boolean;
|
||||
override val container: String =
|
||||
_obj.getOrDefault<String>(cfg, "container", ctx, null) ?: "application/dash+xml"
|
||||
|
||||
override var streamMetaData: StreamMetaData? = null;
|
||||
override val name: String =
|
||||
_obj.getOrThrow<String>(cfg, "name", ctx)
|
||||
|
||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH_RAW, plugin, obj) {
|
||||
val contextName = "DashRawSource";
|
||||
val config = plugin.config;
|
||||
name = _obj.getOrThrow(config, "name", contextName);
|
||||
url = _obj.getOrThrow(config, "url", contextName);
|
||||
container = _obj.getOrDefault<String>(config, "container", contextName, null) ?: "application/dash+xml";
|
||||
manifest = _obj.getOrDefault<String>(config, "manifest", contextName, null);
|
||||
width = _obj.getOrDefault(config, "width", contextName, 0) ?: 0;
|
||||
height = _obj.getOrDefault(config, "height", contextName, 0) ?: 0;
|
||||
codec = _obj.getOrDefault(config, "codec", contextName, "") ?: "";
|
||||
bitrate = _obj.getOrDefault(config, "bitrate", contextName, 0) ?: 0;
|
||||
duration = _obj.getOrDefault(config, "duration", contextName, 0) ?: 0;
|
||||
priority = _obj.getOrDefault(config, "priority", contextName, false) ?: false;
|
||||
canMerge = _obj.getOrDefault(config, "canMerge", contextName, false) ?: false;
|
||||
hasGenerate = _obj.has("generate");
|
||||
}
|
||||
override val width: Int =
|
||||
_obj.getOrDefault<Int>(cfg, "width", ctx, null)?.toInt() ?: 0
|
||||
|
||||
private var _pregenerate: V8Deferred<String?>? = null;
|
||||
override val height: Int =
|
||||
_obj.getOrDefault<Int>(cfg, "height", ctx, null)?.toInt() ?: 0
|
||||
|
||||
override val codec: String =
|
||||
_obj.getOrDefault<String>(cfg, "codec", ctx, "") ?: ""
|
||||
|
||||
override val bitrate: Int? =
|
||||
_obj.getOrDefault<Int>(cfg, "bitrate", ctx, null)?.toInt()
|
||||
|
||||
override val duration: Long =
|
||||
_obj.getOrDefault<Long>(cfg, "duration", ctx, 0)?.toLong() ?: 0L
|
||||
|
||||
override val priority: Boolean =
|
||||
_obj.getOrDefault<Boolean>(cfg, "priority", ctx, false) ?: false
|
||||
|
||||
val url: String? =
|
||||
_obj.getOrDefault<String>(cfg, "url", ctx, null)
|
||||
|
||||
override var manifest: String? =
|
||||
_obj.getOrDefault<String>(cfg, "manifest", ctx, null)
|
||||
|
||||
override val hasGenerate: Boolean = _obj.has("generate")
|
||||
|
||||
val canMerge: Boolean =
|
||||
_obj.getOrDefault<Boolean>(cfg, "canMerge", ctx, false) ?: false
|
||||
|
||||
override var streamMetaData: StreamMetaData? = null
|
||||
|
||||
private var _pregenerate: V8Deferred<String?>? = null
|
||||
fun pregenerateAsync(scope: CoroutineScope): V8Deferred<String?>? {
|
||||
_pregenerate = generateAsync(scope);
|
||||
return _pregenerate;
|
||||
|
||||
+35
-30
@@ -5,42 +5,47 @@ import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrNull
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
open class JSVideoUrlSource : IVideoUrlSource, JSSource {
|
||||
override val width : Int;
|
||||
override val height : Int;
|
||||
override val container : String;
|
||||
override val codec: String;
|
||||
override val name : String;
|
||||
override val bitrate : Int;
|
||||
override val duration: Long;
|
||||
private val url : String;
|
||||
open class JSVideoUrlSource(
|
||||
plugin: JSClient,
|
||||
obj: V8ValueObject
|
||||
) : JSSource(TYPE_VIDEOURL, plugin, obj), IVideoUrlSource {
|
||||
|
||||
override var priority: Boolean = false;
|
||||
private val ctx = "JSVideoUrlSource"
|
||||
private val cfg = plugin.config
|
||||
|
||||
constructor(plugin: JSClient, obj: V8ValueObject): super(TYPE_VIDEOURL, plugin, obj) {
|
||||
val contextName = "JSVideoUrlSource";
|
||||
val config = plugin.config;
|
||||
override val width: Int =
|
||||
_obj.getOrThrow<Int>(cfg, "width", ctx)
|
||||
|
||||
width = _obj.getOrThrow(config, "width", contextName);
|
||||
height = _obj.getOrThrow(config, "height", contextName);
|
||||
container = _obj.getOrThrow(config, "container", contextName);
|
||||
codec = _obj.getOrThrow(config, "codec", contextName);
|
||||
name = _obj.getOrThrow(config, "name", contextName);
|
||||
bitrate = _obj.getOrThrow(config, "bitrate", contextName);
|
||||
duration = _obj.getOrThrow<Int>(config, "duration", contextName).toLong();
|
||||
url = _obj.getOrThrow(config, "url", contextName);
|
||||
override val height: Int =
|
||||
_obj.getOrThrow<Int>(cfg, "height", ctx)
|
||||
|
||||
priority = obj.getOrNull(config, "priority", contextName) ?: false;
|
||||
}
|
||||
override val container: String =
|
||||
_obj.getOrThrow<String>(cfg, "container", ctx)
|
||||
|
||||
override fun getVideoUrl() : String {
|
||||
return url;
|
||||
}
|
||||
override val codec: String =
|
||||
_obj.getOrThrow<String>(cfg, "codec", ctx)
|
||||
|
||||
override fun toString(): String {
|
||||
return "(width=$width, height=$height, container=$container, codec=$codec, name=$name, bitrate=$bitrate, duration=$duration, url=$url)"
|
||||
}
|
||||
}
|
||||
override val name: String =
|
||||
_obj.getOrThrow<String>(cfg, "name", ctx)
|
||||
|
||||
override val bitrate: Int =
|
||||
_obj.getOrThrow<Int>(cfg, "bitrate", ctx)
|
||||
|
||||
override val duration: Long =
|
||||
_obj.getOrThrow<Long>(cfg, "duration", ctx)
|
||||
|
||||
private val url: String =
|
||||
_obj.getOrThrow<String>(cfg, "url", ctx)
|
||||
|
||||
override var priority: Boolean =
|
||||
_obj.getOrDefault<Boolean>(cfg, "priority", ctx, false) ?: false
|
||||
|
||||
override fun getVideoUrl(): String = url
|
||||
|
||||
override fun toString(): String =
|
||||
"(width=$width, height=$height, container=$container, codec=$codec, name=$name, bitrate=$bitrate, duration=$duration, url=$url)"
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
||||
_buttonClose.setOnClickListener { dismiss(); };
|
||||
_buttonDisconnect.setOnClickListener {
|
||||
try {
|
||||
StateCasting.instance.stopVideo()
|
||||
StateCasting.instance.activeDevice?.disconnect()
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Active device failed to disconnect: $e")
|
||||
|
||||
@@ -4,6 +4,8 @@ import android.content.Context
|
||||
import com.caoccao.javet.exceptions.JavetCompilationException
|
||||
import com.caoccao.javet.exceptions.JavetException
|
||||
import com.caoccao.javet.exceptions.JavetExecutionException
|
||||
import com.caoccao.javet.interfaces.IJavetEntityError
|
||||
import com.caoccao.javet.interfaces.IJavetEntityMap
|
||||
import com.caoccao.javet.interop.V8Host
|
||||
import com.caoccao.javet.interop.V8Runtime
|
||||
import com.caoccao.javet.values.V8Value
|
||||
@@ -409,6 +411,12 @@ class V8Plugin {
|
||||
return _runtimeMap.getOrDefault(runtime, null);
|
||||
}
|
||||
|
||||
private fun ctxString(ctx: Any?, key: String): String? = when (ctx) {
|
||||
is Map<*, *> -> ctx[key]?.toString()
|
||||
is V8ValueObject -> if (ctx.has(key)) ctx.getString(key) else null
|
||||
else -> null
|
||||
}
|
||||
|
||||
fun <T: Any?> catchScriptErrors(config: IV8PluginConfig, context: String, code: String? = null, handle: ()->T): T {
|
||||
var codeStripped = code;
|
||||
if(codeStripped != null) { //TODO: Improve code stripped
|
||||
@@ -442,37 +450,6 @@ class V8Plugin {
|
||||
throw ScriptCompilationException(config, "Compilation: [${context}]: ${scriptEx.message}\n(${scriptEx.scriptingError.lineNumber})[${scriptEx.scriptingError.startColumn}-${scriptEx.scriptingError.endColumn}]: ${scriptEx.scriptingError.sourceLine}", null, codeStripped);
|
||||
}
|
||||
catch(executeEx: JavetExecutionException) {
|
||||
val obj = executeEx.scriptingError?.context
|
||||
if(obj != null && obj.containsKey("plugin_type") == true) {
|
||||
val pluginType = obj["plugin_type"].toString();
|
||||
|
||||
//Captcha
|
||||
if (pluginType == "CaptchaRequiredException") {
|
||||
throw ScriptCaptchaRequiredException(config,
|
||||
obj["url"]?.toString(),
|
||||
obj["body"]?.toString(),
|
||||
executeEx, executeEx.scriptingError?.stack, codeStripped);
|
||||
}
|
||||
|
||||
//Reload Required
|
||||
if (pluginType == "ReloadRequiredException") {
|
||||
throw ScriptReloadRequiredException(config,
|
||||
obj["msg"]?.toString(),
|
||||
obj["reloadData"]?.toString(),
|
||||
executeEx, executeEx.scriptingError?.stack, codeStripped);
|
||||
}
|
||||
|
||||
//Others
|
||||
throwExceptionFromV8(
|
||||
config,
|
||||
pluginType,
|
||||
(extractJSExceptionMessage(executeEx) ?: ""),
|
||||
executeEx,
|
||||
executeEx.scriptingError?.stack,
|
||||
codeStripped
|
||||
);
|
||||
}
|
||||
/* //Required for newer V8 versions
|
||||
if(executeEx.scriptingError?.context is IJavetEntityError) {
|
||||
val obj = executeEx.scriptingError?.context as IJavetEntityError
|
||||
if(obj.context.containsKey("plugin_type") == true) {
|
||||
@@ -506,7 +483,6 @@ class V8Plugin {
|
||||
}
|
||||
|
||||
}
|
||||
*/
|
||||
throw ScriptExecutionException(config, extractJSExceptionMessage(executeEx) ?: "", null, executeEx.scriptingError?.stack, codeStripped);
|
||||
}
|
||||
catch(ex: Exception) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.content.Intent
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.WindowManager
|
||||
import android.view.animation.AccelerateInterpolator
|
||||
import android.view.animation.OvershootInterpolator
|
||||
import android.widget.FrameLayout
|
||||
@@ -214,6 +215,16 @@ class ShortView : FrameLayout {
|
||||
}
|
||||
}
|
||||
|
||||
player.onPlayChanged.subscribe {
|
||||
if (it) {
|
||||
Logger.i(TAG, "Keep screen on set because isPlaying")
|
||||
fragment.activity?.window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
} else {
|
||||
Logger.i(TAG, "Keep screen on cleared because not isPlaying")
|
||||
fragment.activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
}
|
||||
}
|
||||
|
||||
onPlayingToggled.subscribe { playing ->
|
||||
if (playing) {
|
||||
playPauseIcon.setImageResource(R.drawable.ic_play)
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.view.LayoutInflater
|
||||
import android.view.SoundEffectConstants
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
@@ -309,6 +310,12 @@ class ShortsFragment : MainFragment() {
|
||||
customViewAdapter?.previousShownView?.stop()
|
||||
}
|
||||
|
||||
override fun onDestroyMainView() {
|
||||
super.onDestroyMainView()
|
||||
Logger.i(TAG, "Keep screen on cleared because onDestroyMainView fragment")
|
||||
activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ShortsFragment"
|
||||
|
||||
|
||||
+49
-32
@@ -244,6 +244,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
private val _buttonSubscribe: SubscribeButton;
|
||||
|
||||
private val _buttonPins: RoundButtonGroup;
|
||||
private var _loaderGameVisible = false
|
||||
//private val _buttonMore: RoundButton;
|
||||
|
||||
var preventPictureInPicture: Boolean = false
|
||||
@@ -261,7 +262,6 @@ class VideoDetailView : ConstraintLayout {
|
||||
private val _textSkip: TextView;
|
||||
private val _textResume: TextView;
|
||||
private val _layoutResume: LinearLayout;
|
||||
private var _jobHideResume: Job? = null;
|
||||
private val _layoutPlayerContainer: TouchInterceptFrameLayout;
|
||||
private val _layoutChangeBottomSection: LinearLayout;
|
||||
|
||||
@@ -336,7 +336,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
!StateCasting.instance.isCasting &&
|
||||
Settings.instance.playback.isBackgroundPictureInPicture() &&
|
||||
!isAudioOnlyUserAction &&
|
||||
isPlaying
|
||||
(isPlaying || _loaderGameVisible)
|
||||
|
||||
val onShouldEnterPictureInPictureChanged = Event0();
|
||||
|
||||
@@ -357,6 +357,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
Pair(-5 * 60, 30), //around 5 minutes, try every 30 seconds
|
||||
Pair(0, 10) //around live, try every 10 seconds
|
||||
);
|
||||
private var _subtitleLanguage: String? = null
|
||||
|
||||
@androidx.annotation.OptIn(UnstableApi::class)
|
||||
constructor(context: Context, attrs : AttributeSet? = null) : super(context, attrs) {
|
||||
@@ -548,6 +549,16 @@ class VideoDetailView : ConstraintLayout {
|
||||
_buttonMore = buttonMore;
|
||||
updateMoreButtons();
|
||||
|
||||
val handleLoaderGameVisibilityChanged = { b: Boolean ->
|
||||
_loaderGameVisible = b
|
||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
onShouldEnterPictureInPictureChanged.emit()
|
||||
}
|
||||
updateResumeVisibilityFor(lastPositionMilliseconds)
|
||||
}
|
||||
_player.loaderGameVisibilityChanged.subscribe(handleLoaderGameVisibilityChanged)
|
||||
_cast.loaderGameVisibilityChanged.subscribe(handleLoaderGameVisibilityChanged)
|
||||
|
||||
_channelButton.setOnClickListener {
|
||||
if (video is TutorialFragment.TutorialVideo) {
|
||||
return@setOnClickListener
|
||||
@@ -872,11 +883,6 @@ class VideoDetailView : ConstraintLayout {
|
||||
|
||||
_layoutResume.setOnClickListener {
|
||||
handleSeek(_historicalPosition * 1000);
|
||||
|
||||
val job = _jobHideResume;
|
||||
_jobHideResume = null;
|
||||
job?.cancel();
|
||||
|
||||
_layoutResume.visibility = View.GONE;
|
||||
};
|
||||
|
||||
@@ -1255,10 +1261,6 @@ class VideoDetailView : ConstraintLayout {
|
||||
MediaControlReceiver.onCloseReceived.remove(this);
|
||||
MediaControlReceiver.onBackgroundReceived.remove(this);
|
||||
MediaControlReceiver.onSeekToReceived.remove(this);
|
||||
|
||||
val job = _jobHideResume;
|
||||
_jobHideResume = null;
|
||||
job?.cancel();
|
||||
}
|
||||
|
||||
//Video Setters
|
||||
@@ -1780,26 +1782,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
TAG,
|
||||
"Historical position: $_historicalPosition, last position: $lastPositionMilliseconds"
|
||||
);
|
||||
if (_historicalPosition > 60 && video.duration - _historicalPosition > 5 && Math.abs(
|
||||
_historicalPosition - lastPositionMilliseconds / 1000
|
||||
) > 5.0
|
||||
) {
|
||||
_layoutResume.visibility = View.VISIBLE;
|
||||
_textResume.text = "Resume at ${_historicalPosition.toHumanTime(false)}";
|
||||
|
||||
_jobHideResume = fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
delay(8000);
|
||||
_layoutResume.visibility = View.GONE;
|
||||
_textResume.text = "";
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to set resume changes.", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_layoutResume.visibility = View.GONE;
|
||||
_textResume.text = "";
|
||||
}
|
||||
updateResumeVisibilityFor(lastPositionMilliseconds)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1845,6 +1828,35 @@ class VideoDetailView : ConstraintLayout {
|
||||
_taskLoadRecommendations.run(videoDetail.url)
|
||||
}
|
||||
}
|
||||
|
||||
private fun shouldShowResume(positionMs: Long): Boolean {
|
||||
if (_loaderGameVisible) return false
|
||||
val v = video ?: return false
|
||||
val resumeS = _historicalPosition
|
||||
val durS = v.duration
|
||||
|
||||
if (_overlay_loading.visibility == View.VISIBLE) return false
|
||||
if (resumeS <= 60) return false
|
||||
if (durS - resumeS <= 5) return false
|
||||
|
||||
val posMs = positionMs
|
||||
val resumeMs = resumeS * 1000
|
||||
val durMs = durS * 1000L
|
||||
val inFirstFewSeconds = posMs < 8_000
|
||||
val notYetReachedResume = (resumeMs - posMs) > 5_000
|
||||
return inFirstFewSeconds && notYetReachedResume && durMs > 0
|
||||
}
|
||||
|
||||
private fun updateResumeVisibilityFor(positionMs: Long) {
|
||||
val visible = shouldShowResume(positionMs)
|
||||
if (visible) {
|
||||
_layoutResume.visibility = View.VISIBLE
|
||||
_textResume.text = "Resume at ${_historicalPosition.toHumanTime(false)}"
|
||||
} else {
|
||||
_layoutResume.visibility = View.GONE
|
||||
_textResume.text = ""
|
||||
}
|
||||
}
|
||||
fun loadVODChat(video: IPlatformVideoDetails) {
|
||||
_liveChat?.stop();
|
||||
_container_content_liveChat.cancel();
|
||||
@@ -1964,7 +1976,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
try {
|
||||
val videoSource = _lastVideoSource ?: _player.getPreferredVideoSource(video, Settings.instance.playback.getCurrentPreferredQualityPixelCount());
|
||||
val audioSource = _lastAudioSource ?: _player.getPreferredAudioSource(video, Settings.instance.playback.getPrimaryLanguage(context));
|
||||
val subtitleSource = _lastSubtitleSource ?: (if(video is VideoLocal) video.subtitlesSources.firstOrNull() else null);
|
||||
val subtitleSource = _lastSubtitleSource ?: (if (Settings.instance.playback.stickySubtitles) _player.getPreferredSubtitleSource(video, _subtitleLanguage) else null) ?: (if(video is VideoLocal) video.subtitlesSources.firstOrNull() else null);
|
||||
Logger.i(TAG, "loadCurrentVideo(videoSource=$videoSource, audioSource=$audioSource, subtitleSource=$subtitleSource, resumePositionMs=$resumePositionMs)")
|
||||
|
||||
if(videoSource == null && audioSource == null) {
|
||||
@@ -2648,6 +2660,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
}
|
||||
}
|
||||
_lastSubtitleSource = toSet;
|
||||
_subtitleLanguage = toSet?.language
|
||||
}
|
||||
|
||||
private fun handleUnavailableVideo(msg: String? = null) {
|
||||
@@ -2805,6 +2818,8 @@ class VideoDetailView : ConstraintLayout {
|
||||
_overlay_loading.visibility = View.GONE;
|
||||
(_overlay_loading_spinner.drawable as Animatable?)?.stop()
|
||||
}
|
||||
|
||||
updateResumeVisibilityFor(lastPositionMilliseconds)
|
||||
}
|
||||
|
||||
//UI Actions
|
||||
@@ -3095,6 +3110,8 @@ class VideoDetailView : ConstraintLayout {
|
||||
handleSeek(55000);
|
||||
}
|
||||
}
|
||||
|
||||
updateResumeVisibilityFor(positionMilliseconds)
|
||||
}
|
||||
|
||||
private fun updateTracker(positionMs: Long, isPlaying: Boolean, forceUpdate: Boolean = false) {
|
||||
|
||||
@@ -19,6 +19,8 @@ import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSour
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IWidevineSource
|
||||
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSAudioUrlRangeSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource
|
||||
@@ -134,6 +136,62 @@ class VideoHelper {
|
||||
return bestSource;
|
||||
}
|
||||
|
||||
fun selectBestSubtitleSource(sources: Iterable<ISubtitleSource>, preferredLanguage: String?): ISubtitleSource? {
|
||||
if (preferredLanguage.isNullOrBlank()) return null
|
||||
|
||||
val prefTag = normalizeTag(preferredLanguage)
|
||||
val prefPrimary = primarySubtag(prefTag) ?: return null
|
||||
|
||||
var best: ISubtitleSource? = null
|
||||
var bestKey: Quad<Int, Int, String, String>? = null
|
||||
|
||||
for (src in sources) {
|
||||
val raw = src.language ?: continue
|
||||
val tag = normalizeTag(raw)
|
||||
val primary = primarySubtag(tag) ?: continue
|
||||
|
||||
val score = when {
|
||||
tag.equals(prefTag, ignoreCase = true) -> 0
|
||||
primary.equals(prefPrimary, ignoreCase = true) && findRegion(tag) == null -> 1
|
||||
primary.equals(prefPrimary, ignoreCase = true) -> 2
|
||||
else -> 3
|
||||
}
|
||||
if (score >= 3) continue
|
||||
|
||||
val key = Quad(score, src.name.length, tag.lowercase(), src.name)
|
||||
if (bestKey == null || key < bestKey!!) {
|
||||
bestKey = key
|
||||
best = src
|
||||
}
|
||||
}
|
||||
return best
|
||||
}
|
||||
|
||||
private fun normalizeTag(tag: String): String = tag.trim().replace('_', '-')
|
||||
private fun primarySubtag(tag: String): String? = tag.split('-').firstOrNull { it.isNotBlank() }?.lowercase()
|
||||
|
||||
private fun findRegion(tag: String): String? {
|
||||
val parts = tag.split('-').drop(1) // skip primary language
|
||||
for (p in parts) {
|
||||
val isAlpha2 = p.length == 2 && p[0].isLetter() && p[1].isLetter()
|
||||
val isNumeric3 = p.length == 3 && p.all { it.isDigit() }
|
||||
if (isAlpha2 || isNumeric3) return p.uppercase()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private data class Quad<A : Comparable<A>, B : Comparable<B>, C : Comparable<C>, D : Comparable<D>>(
|
||||
val a: A, val b: B, val c: C, val d: D
|
||||
) : Comparable<Quad<A, B, C, D>> {
|
||||
override fun compareTo(other: Quad<A, B, C, D>): Int =
|
||||
when {
|
||||
a != other.a -> a.compareTo(other.a)
|
||||
b != other.b -> b.compareTo(other.b)
|
||||
c != other.c -> c.compareTo(other.c)
|
||||
else -> d.compareTo(other.d)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
fun convertItagSourceToChunkedDashSource(videoSource: JSVideoUrlRangeSource) : Pair<MediaSource, String> {
|
||||
val urlToUse = videoSource.getVideoUrl();
|
||||
|
||||
@@ -439,7 +439,7 @@ class StateDownloads {
|
||||
} else {
|
||||
throw NotImplementedError("Unsuported scheme");
|
||||
}
|
||||
return if (subtitles != null) SubtitleRawSource(subtitle.name, subtitle.format, subtitles!!) else null;
|
||||
return if (subtitles != null) SubtitleRawSource(subtitle.name, subtitle.language, subtitle.format, subtitles!!) else null;
|
||||
}
|
||||
|
||||
fun cleanupDownloads(): Pair<Int, Long> {
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
package com.futo.platformplayer.views.adapters
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.view.GestureDetector
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
@@ -11,6 +16,7 @@ import androidx.constraintlayout.widget.ConstraintLayout
|
||||
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.api.media.models.comments.IPlatformComment
|
||||
import com.futo.platformplayer.api.media.models.comments.LazyComment
|
||||
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
||||
@@ -97,6 +103,15 @@ class CommentViewHolder : ViewHolder {
|
||||
StatePolycentric.instance.updateLikeMap(c.reference, args.hasLiked, args.hasDisliked)
|
||||
};
|
||||
|
||||
_layoutComment.setOnLongClickListener {
|
||||
val clipboard = viewGroup.context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val text = comment?.message.orEmpty()
|
||||
val clip = ClipData.newPlainText("Comment", text)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
UIDialogs.toast(viewGroup.context, "Copied", false)
|
||||
true
|
||||
}
|
||||
|
||||
_creatorThumbnail.onClick.subscribe {
|
||||
val c = comment ?: return@subscribe;
|
||||
onAuthorClick.emit(c);
|
||||
@@ -120,7 +135,7 @@ class CommentViewHolder : ViewHolder {
|
||||
onDelete.emit(c);
|
||||
}
|
||||
|
||||
_textBody.setPlatformPlayerLinkMovementMethod(viewGroup.context);
|
||||
_textBody.setPlatformPlayerLinkMovementMethod(viewGroup.context)
|
||||
}
|
||||
|
||||
fun bind(comment: IPlatformComment, readonly: Boolean) {
|
||||
|
||||
@@ -68,6 +68,7 @@ class CastView : ConstraintLayout {
|
||||
val onPrevious = Event0();
|
||||
val onNext = Event0();
|
||||
val onTimeJobTimeChanged_s = Event1<Long>()
|
||||
val loaderGameVisibilityChanged = Event1<Boolean>();
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
|
||||
@@ -90,6 +91,7 @@ class CastView : ConstraintLayout {
|
||||
_gestureControlView = findViewById(R.id.gesture_control);
|
||||
_loaderGame = findViewById(R.id.loader_overlay)
|
||||
_loaderGame.visibility = View.GONE
|
||||
loaderGameVisibilityChanged.emit(false)
|
||||
|
||||
_gestureControlView.fullScreenGestureEnabled = false
|
||||
_gestureControlView.setupTouchArea();
|
||||
@@ -319,15 +321,18 @@ class CastView : ConstraintLayout {
|
||||
if (isLoading) {
|
||||
_loaderGame.visibility = View.VISIBLE
|
||||
_loaderGame.startLoader()
|
||||
loaderGameVisibilityChanged.emit(true)
|
||||
} else {
|
||||
_loaderGame.visibility = View.GONE
|
||||
_loaderGame.stopAndResetLoader()
|
||||
loaderGameVisibilityChanged.emit(false)
|
||||
}
|
||||
}
|
||||
|
||||
fun setLoading(expectedDurationMs: Int) {
|
||||
_loaderGame.visibility = View.VISIBLE
|
||||
_loaderGame.startLoader(expectedDurationMs.toLong())
|
||||
loaderGameVisibilityChanged.emit(true)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -60,6 +60,10 @@ class FutoShortPlayer(context: Context, attrs: AttributeSet? = null) :
|
||||
if (events.containsAny(Player.EVENT_PLAYBACK_STATE_CHANGED)) {
|
||||
onPlaybackStateChanged.emit(player.playbackState)
|
||||
}
|
||||
|
||||
if (events.containsAny(Player.EVENT_IS_PLAYING_CHANGED)) {
|
||||
onPlayChanged.emit(player.isPlaying)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -164,6 +164,8 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
|
||||
private val _loaderGame: TargetTapLoaderView
|
||||
|
||||
val loaderGameVisibilityChanged = Event1<Boolean>();
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
constructor(context: Context, attrs: AttributeSet? = null) : super(PLAYER_STATE_NAME, context, attrs) {
|
||||
LayoutInflater.from(context).inflate(R.layout.video_view, this, true);
|
||||
@@ -206,6 +208,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
|
||||
_loaderGame = findViewById(R.id.loader_overlay)
|
||||
_loaderGame.visibility = View.GONE
|
||||
loaderGameVisibilityChanged.emit(false)
|
||||
|
||||
_control_chapter.setOnClickListener {
|
||||
_currentChapter?.let {
|
||||
@@ -894,15 +897,18 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
if (isLoading) {
|
||||
_loaderGame.visibility = View.VISIBLE
|
||||
_loaderGame.startLoader()
|
||||
loaderGameVisibilityChanged.emit(true)
|
||||
} else {
|
||||
_loaderGame.visibility = View.GONE
|
||||
_loaderGame.stopAndResetLoader()
|
||||
loaderGameVisibilityChanged.emit(false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setLoading(expectedDurationMs: Int) {
|
||||
_loaderGame.visibility = View.VISIBLE
|
||||
_loaderGame.startLoader(expectedDurationMs.toLong())
|
||||
loaderGameVisibilityChanged.emit(true)
|
||||
}
|
||||
|
||||
override fun switchToVideoMode() {
|
||||
|
||||
@@ -914,6 +914,10 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
return VideoHelper.selectBestAudioSource(video.video, PREFERED_AUDIO_CONTAINERS, preferredLanguage);
|
||||
}
|
||||
|
||||
fun getPreferredSubtitleSource(video: IPlatformVideoDetails, preferredLanguage: String?): ISubtitleSource? {
|
||||
return VideoHelper.selectBestSubtitleSource(video.subtitles, preferredLanguage);
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun loadSelectedSources(play: Boolean, resume: Boolean): Boolean {
|
||||
val sourceVideo = if(!isAudioMode || _lastAudioMediaSource == null) _lastVideoMediaSource else null;
|
||||
|
||||
@@ -505,6 +505,8 @@
|
||||
<string name="preferred_preview_quality">Preferred Preview Quality</string>
|
||||
<string name="preferred_preview_quality_description">Default quality while previewing a video in a feed</string>
|
||||
<string name="primary_language">Primary Language</string>
|
||||
<string name="sticky_subtitles">Persist Subtitles</string>
|
||||
<string name="sticky_subtitles_description">Once a subtitle language is selected, search for a best match once a new video is played</string>
|
||||
<string name="prefer_original_audio">Prefer Original Audio</string>
|
||||
<string name="prefer_original_audio_description">Use original audio instead of preferred language when it is known</string>
|
||||
<string name="default_comment_section">Default Comment Section</string>
|
||||
|
||||
+5
-5
@@ -1,8 +1,8 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
plugins {
|
||||
id 'com.android.application' version '8.5.2' apply false
|
||||
id 'com.android.library' version '8.5.2' apply false
|
||||
id 'org.jetbrains.kotlin.android' version '1.9.0' apply false
|
||||
id 'com.google.protobuf' version '0.9.4' apply false
|
||||
id 'com.google.devtools.ksp' version '1.9.0-1.0.13' apply false
|
||||
id 'com.android.application' version '8.13.0' apply false
|
||||
id 'com.android.library' version '8.13.0' apply false
|
||||
id 'org.jetbrains.kotlin.android' version '2.2.21' apply false
|
||||
id 'com.google.protobuf' version '0.9.5' apply false
|
||||
id 'com.google.devtools.ksp' version '2.2.21-2.0.4' apply false
|
||||
}
|
||||
|
||||
+1
-1
Submodule dep/futopay updated: 224d69764c...6857c8f0bc
+1
-1
Submodule dep/polycentricandroid updated: 8ee5a329e2...642747fae8
+1
-1
@@ -1,6 +1,6 @@
|
||||
#Fri Nov 11 13:25:09 CET 2022
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
Reference in New Issue
Block a user