mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 04:52:39 +02:00
Added setting for persisting subtitles across multiple videos when the same language exists.
This commit is contained in:
@@ -408,6 +408,10 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
else -> null
|
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)
|
@FormField(R.string.prefer_original_audio, FieldForm.TOGGLE, R.string.prefer_original_audio_description, -1)
|
||||||
var preferOriginalAudio: Boolean = true;
|
var preferOriginalAudio: Boolean = true;
|
||||||
|
|
||||||
|
|||||||
+1
@@ -41,6 +41,7 @@ class HLSVariantSubtitleUrlSource(
|
|||||||
override val format: String,
|
override val format: String,
|
||||||
) : ISubtitleSource {
|
) : ISubtitleSource {
|
||||||
override val hasFetch: Boolean = false
|
override val hasFetch: Boolean = false
|
||||||
|
override val language: String? = null
|
||||||
|
|
||||||
override fun getSubtitles(): String? {
|
override fun getSubtitles(): String? {
|
||||||
return null
|
return null
|
||||||
|
|||||||
+4
-1
@@ -9,13 +9,15 @@ class LocalSubtitleSource : ISubtitleSource {
|
|||||||
override val name: String;
|
override val name: String;
|
||||||
override val url: String?;
|
override val url: String?;
|
||||||
override val format: String?;
|
override val format: String?;
|
||||||
|
override val language: String?
|
||||||
override val hasFetch: Boolean get() = false;
|
override val hasFetch: Boolean get() = false;
|
||||||
|
|
||||||
val filePath: String;
|
val filePath: String;
|
||||||
|
|
||||||
constructor(name: String, format: String?, filePath: String) {
|
constructor(name: String, language: String?, format: String?, filePath: String) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.format = format;
|
this.format = format;
|
||||||
|
this.language = language
|
||||||
this.filePath = filePath;
|
this.filePath = filePath;
|
||||||
this.url = Uri.fromFile(File(filePath)).toString();
|
this.url = Uri.fromFile(File(filePath)).toString();
|
||||||
}
|
}
|
||||||
@@ -32,6 +34,7 @@ class LocalSubtitleSource : ISubtitleSource {
|
|||||||
fun fromSource(source: SubtitleRawSource, path: String): LocalSubtitleSource {
|
fun fromSource(source: SubtitleRawSource, path: String): LocalSubtitleSource {
|
||||||
return LocalSubtitleSource(
|
return LocalSubtitleSource(
|
||||||
source.name,
|
source.name,
|
||||||
|
source.language,
|
||||||
source.format,
|
source.format,
|
||||||
path
|
path
|
||||||
);
|
);
|
||||||
|
|||||||
+1
@@ -6,6 +6,7 @@ import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
|||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
class SubtitleRawSource(
|
class SubtitleRawSource(
|
||||||
override val name: String,
|
override val name: String,
|
||||||
|
override val language: String?,
|
||||||
override val format: String?,
|
override val format: String?,
|
||||||
val _subtitles: String,
|
val _subtitles: String,
|
||||||
override val url: String? = null,
|
override val url: String? = null,
|
||||||
|
|||||||
+1
@@ -7,6 +7,7 @@ interface ISubtitleSource {
|
|||||||
val url: String?;
|
val url: String?;
|
||||||
val format: String?;
|
val format: String?;
|
||||||
val hasFetch: Boolean;
|
val hasFetch: Boolean;
|
||||||
|
val language: String?
|
||||||
|
|
||||||
fun getSubtitles(): String?;
|
fun getSubtitles(): String?;
|
||||||
|
|
||||||
|
|||||||
+2
@@ -22,6 +22,7 @@ class JSSubtitleSource : ISubtitleSource {
|
|||||||
override val name: String;
|
override val name: String;
|
||||||
override val url: String?;
|
override val url: String?;
|
||||||
override val format: String?;
|
override val format: String?;
|
||||||
|
override val language: String?
|
||||||
override val hasFetch: Boolean;
|
override val hasFetch: Boolean;
|
||||||
|
|
||||||
constructor(config: SourcePluginConfig, v8Value: V8ValueObject) {
|
constructor(config: SourcePluginConfig, v8Value: V8ValueObject) {
|
||||||
@@ -29,6 +30,7 @@ class JSSubtitleSource : ISubtitleSource {
|
|||||||
|
|
||||||
val context = "JSSubtitles";
|
val context = "JSSubtitles";
|
||||||
name = v8Value.getOrThrow(config, "name", context, false);
|
name = v8Value.getOrThrow(config, "name", context, false);
|
||||||
|
language = v8Value.getOrThrow(config, "language", context, false);
|
||||||
url = v8Value.getOrThrow(config, "url", context, true);
|
url = v8Value.getOrThrow(config, "url", context, true);
|
||||||
format = v8Value.getOrThrow(config, "format", context, true);
|
format = v8Value.getOrThrow(config, "format", context, true);
|
||||||
hasFetch = v8Value.has("getSubtitles");
|
hasFetch = v8Value.has("getSubtitles");
|
||||||
|
|||||||
+3
-1
@@ -357,6 +357,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
Pair(-5 * 60, 30), //around 5 minutes, try every 30 seconds
|
Pair(-5 * 60, 30), //around 5 minutes, try every 30 seconds
|
||||||
Pair(0, 10) //around live, try every 10 seconds
|
Pair(0, 10) //around live, try every 10 seconds
|
||||||
);
|
);
|
||||||
|
private var _subtitleLanguage: String? = null
|
||||||
|
|
||||||
@androidx.annotation.OptIn(UnstableApi::class)
|
@androidx.annotation.OptIn(UnstableApi::class)
|
||||||
constructor(context: Context, attrs : AttributeSet? = null) : super(context, attrs) {
|
constructor(context: Context, attrs : AttributeSet? = null) : super(context, attrs) {
|
||||||
@@ -1975,7 +1976,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
try {
|
try {
|
||||||
val videoSource = _lastVideoSource ?: _player.getPreferredVideoSource(video, Settings.instance.playback.getCurrentPreferredQualityPixelCount());
|
val videoSource = _lastVideoSource ?: _player.getPreferredVideoSource(video, Settings.instance.playback.getCurrentPreferredQualityPixelCount());
|
||||||
val audioSource = _lastAudioSource ?: _player.getPreferredAudioSource(video, Settings.instance.playback.getPrimaryLanguage(context));
|
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)")
|
Logger.i(TAG, "loadCurrentVideo(videoSource=$videoSource, audioSource=$audioSource, subtitleSource=$subtitleSource, resumePositionMs=$resumePositionMs)")
|
||||||
|
|
||||||
if(videoSource == null && audioSource == null) {
|
if(videoSource == null && audioSource == null) {
|
||||||
@@ -2659,6 +2660,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
_lastSubtitleSource = toSet;
|
_lastSubtitleSource = toSet;
|
||||||
|
_subtitleLanguage = toSet?.language
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleUnavailableVideo(msg: String? = null) {
|
private fun handleUnavailableVideo(msg: String? = null) {
|
||||||
|
|||||||
@@ -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.IVideoSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
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.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.models.video.IPlatformVideoDetails
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSAudioUrlRangeSource
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSAudioUrlRangeSource
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource
|
||||||
@@ -134,6 +136,62 @@ class VideoHelper {
|
|||||||
return bestSource;
|
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)
|
@OptIn(UnstableApi::class)
|
||||||
fun convertItagSourceToChunkedDashSource(videoSource: JSVideoUrlRangeSource) : Pair<MediaSource, String> {
|
fun convertItagSourceToChunkedDashSource(videoSource: JSVideoUrlRangeSource) : Pair<MediaSource, String> {
|
||||||
val urlToUse = videoSource.getVideoUrl();
|
val urlToUse = videoSource.getVideoUrl();
|
||||||
|
|||||||
@@ -439,7 +439,7 @@ class StateDownloads {
|
|||||||
} else {
|
} else {
|
||||||
throw NotImplementedError("Unsuported scheme");
|
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> {
|
fun cleanupDownloads(): Pair<Int, Long> {
|
||||||
|
|||||||
@@ -914,6 +914,10 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
|||||||
return VideoHelper.selectBestAudioSource(video.video, PREFERED_AUDIO_CONTAINERS, preferredLanguage);
|
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)
|
@OptIn(UnstableApi::class)
|
||||||
private fun loadSelectedSources(play: Boolean, resume: Boolean): Boolean {
|
private fun loadSelectedSources(play: Boolean, resume: Boolean): Boolean {
|
||||||
val sourceVideo = if(!isAudioMode || _lastAudioMediaSource == null) _lastVideoMediaSource else null;
|
val sourceVideo = if(!isAudioMode || _lastAudioMediaSource == null) _lastVideoMediaSource else null;
|
||||||
|
|||||||
@@ -505,6 +505,8 @@
|
|||||||
<string name="preferred_preview_quality">Preferred Preview Quality</string>
|
<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="preferred_preview_quality_description">Default quality while previewing a video in a feed</string>
|
||||||
<string name="primary_language">Primary Language</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">Prefer Original Audio</string>
|
||||||
<string name="prefer_original_audio_description">Use original audio instead of preferred language when it is known</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>
|
<string name="default_comment_section">Default Comment Section</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user