Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(player): Add better auto sub select #1706

Merged
merged 6 commits into from
Jul 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
package eu.kanade.presentation.more.settings

import android.content.Context
import android.os.Build
import android.os.Environment
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.vector.ImageVector
import eu.kanade.core.preference.asState
import eu.kanade.tachiyomi.data.track.Tracker
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.coroutines.CoroutineScope
import tachiyomi.core.common.storage.openFileDescriptor
import tachiyomi.domain.storage.service.StorageManager
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.FileOutputStream
import tachiyomi.core.common.preference.Preference as PreferenceData

sealed class Preference {
Expand Down Expand Up @@ -151,6 +161,44 @@ sealed class Preference {
val canBeBlank: Boolean = false,
) : PreferenceItem<String>()

/**
* A [PreferenceItem] for editing MPV config files.
* If [fileName] is not null, it will update this file in the config directory.
*/
data class MPVConfPreference(
val pref: PreferenceData<String>,
val scope: CoroutineScope,
val context: Context,
val fileName: String? = null,
override val title: String,
override val subtitle: String? = pref.asState(scope).value
.lines().take(2)
.joinToString(
separator = "\n",
postfix = if (pref.asState(scope).value.lines().size > 2) "\n..." else "",
),
override val icon: ImageVector? = null,
override val enabled: Boolean = true,
override val onValueChanged: suspend (newValue: String) -> Boolean = { newValue ->
if (fileName != null) {
val storageManager: StorageManager = Injekt.get()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && Environment.isExternalStorageManager()) {
val inputFile = storageManager.getMPVConfigDirectory()
?.createFile(fileName)
inputFile?.openFileDescriptor(context, "rwt")?.fileDescriptor
?.let {
FileOutputStream(it).bufferedWriter().use { writer ->
writer.write(newValue)
}
}
pref.set(newValue)
}
}
true
},
val canBeBlank: Boolean = true,
) : PreferenceItem<String>()

/**
* A [PreferenceItem] for individual tracker.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,22 @@ internal fun PreferenceItem(
canBeBlank = item.canBeBlank,
)
}
is Preference.PreferenceItem.MPVConfPreference -> {
val values by item.pref.collectAsState()
EditTextPreferenceWidget(
title = item.title,
subtitle = item.subtitle,
icon = item.icon,
value = values,
onConfirm = {
val accepted = item.onValueChanged(it)
if (accepted) item.pref.set(it)
accepted
},
singleLine = false,
canBeBlank = item.canBeBlank,
)
}
is Preference.PreferenceItem.TrackerPreference -> {
val isLoggedIn by item.tracker.let { tracker ->
tracker.isLoggedInFlow.collectAsState(tracker.isLoggedIn)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package eu.kanade.presentation.more.settings.screen

import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
import android.os.Build
Expand All @@ -10,13 +9,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext
import eu.kanade.core.preference.asState
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences
import eu.kanade.tachiyomi.ui.player.viewer.VideoDebanding
import kotlinx.collections.immutable.toImmutableMap
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.domain.storage.service.StorageManager
import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
Expand All @@ -25,60 +22,35 @@ object AdvancedPlayerSettingsScreen : SearchableSettings {
@Composable
override fun getTitleRes() = MR.strings.pref_category_player_advanced

@SuppressLint("InlinedApi")
@Composable
override fun getPreferences(): List<Preference> {
val playerPreferences = remember { Injekt.get<PlayerPreferences>() }
val scope = rememberCoroutineScope()
val context = LocalContext.current
val mpvConf = playerPreferences.mpvConf()
val mpvInput = playerPreferences.mpvInput()
val storageManager: StorageManager = Injekt.get()
val subSelectConf = playerPreferences.subSelectConf()

return listOf(
Preference.PreferenceItem.MultiLineEditTextPreference(
Preference.PreferenceItem.MPVConfPreference(
pref = mpvConf,
title = context.stringResource(MR.strings.pref_mpv_conf),
subtitle = mpvConf.asState(scope).value
.lines().take(2)
.joinToString(
separator = "\n",
postfix = if (mpvConf.asState(scope).value.lines().size > 2) "\n..." else "",
),
onValueChanged = {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && Environment.isExternalStorageManager()) {
val inputFile = storageManager.getMPVConfigDirectory()
?.createFile("mpv.conf")
inputFile?.openOutputStream()?.bufferedWriter().use { writer ->
writer?.write(it)
}
mpvConf.set(it)
}
true
},
canBeBlank = true,
fileName = "mpv.conf",
scope = scope,
context = context,
),
Preference.PreferenceItem.MultiLineEditTextPreference(
Preference.PreferenceItem.MPVConfPreference(
pref = mpvInput,
title = context.stringResource(MR.strings.pref_mpv_input),
subtitle = mpvInput.asState(scope).value
.lines().take(2)
.joinToString(
separator = "\n",
postfix = if (mpvInput.asState(scope).value.lines().size > 2) "\n..." else "",
),
onValueChanged = {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && Environment.isExternalStorageManager()) {
val inputFile = storageManager.getMPVConfigDirectory()
?.createFile("input.conf")
inputFile?.openOutputStream()?.bufferedWriter().use { writer ->
writer?.write(it)
}
mpvInput.set(it)
}
true
},
canBeBlank = true,
fileName = "input.conf",
scope = scope,
context = context,
),
Preference.PreferenceItem.MPVConfPreference(
pref = subSelectConf,
title = context.stringResource(MR.strings.pref_sub_select_conf),
scope = scope,
context = context,
),
Preference.PreferenceItem.SwitchPreference(
title = context.stringResource(MR.strings.pref_gpu_next_title),
Expand Down
143 changes: 87 additions & 56 deletions app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ import eu.kanade.tachiyomi.ui.player.viewer.VideoDebanding
import eu.kanade.tachiyomi.util.AniSkipApi
import eu.kanade.tachiyomi.util.SkipType
import eu.kanade.tachiyomi.util.Stamp
import eu.kanade.tachiyomi.util.SubtitleSelect
import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.system.powerManager
import eu.kanade.tachiyomi.util.system.toShareIntent
Expand Down Expand Up @@ -1738,50 +1739,111 @@ class PlayerActivity : BaseActivity() {
}
}

private val subtitleSelect = SubtitleSelect(playerPreferences)

private fun selectSubtitle(subtitleTracks: List<Track>, index: Int, embedded: Boolean = false) {
val offset = if (embedded) 0 else 1
streams.subtitle.index = index + offset
val tracks = player.tracks.getValue("sub")
val selectedLoadedTrack = tracks.firstOrNull {
it.name == subtitleTracks[index].url ||
it.mpvId.toString() == subtitleTracks[index].url
}
selectedLoadedTrack?.let { player.sid = it.mpvId }
?: MPVLib.command(
arrayOf(
"sub-add",
subtitleTracks[index].url,
"select",
subtitleTracks[index].url,
),
)
}

// TODO: exception java.util.ConcurrentModificationException:
// UPDATE: MAY HAVE BEEN FIXED
// at java.lang.Object java.util.ArrayList$Itr.next() (ArrayList.java:860)
// at void eu.kanade.tachiyomi.ui.player.PlayerActivity.fileLoaded() (PlayerActivity.kt:1874)
// at void eu.kanade.tachiyomi.ui.player.PlayerActivity.event(int) (PlayerActivity.kt:1566)
// at void is.xyz.mpv.MPVLib.event(int) (MPVLib.java:86)
@SuppressLint("SourceLockedOrientationActivity")
internal suspend fun fileLoaded() {
setMpvMediaTitle()
val localLangName = LocaleHelper.getSimpleLocaleDisplayName()
clearTracks()
player.loadTracks()
setupSubtitleTracks()
setupAudioTracks()

viewModel.viewModelScope.launchUI {
if (playerPreferences.adjustOrientationVideoDimensions().get()) {
if ((player.videoW ?: 1) / (player.videoH ?: 1) >= 1) {
[email protected] =
playerPreferences.defaultPlayerOrientationLandscape().get()

switchControlsOrientation(true)
} else {
[email protected] =
playerPreferences.defaultPlayerOrientationPortrait().get()

switchControlsOrientation(false)
}
}

viewModel.mutableState.update {
it.copy(isLoadingEpisode = false)
}
}
// aniSkip stuff
waitingAniSkip = playerPreferences.waitingTimeAniSkip().get()
runBlocking {
if (aniSkipEnable) {
aniSkipInterval = viewModel.aniSkipResponse(player.duration)
aniSkipInterval?.let {
aniskipStamps = it
updateChapters(it, player.duration)
}
}
}
}

private fun setupSubtitleTracks() {
streams.subtitle.tracks += player.tracks.getOrElse("sub") { emptyList() }
.drop(1).map { track ->
Track(track.mpvId.toString(), track.name)
}.toTypedArray()
streams.audio.tracks += player.tracks.getOrElse("audio") { emptyList() }
.drop(1).map { track ->
Track(track.mpvId.toString(), track.name)
}.toTypedArray()
if (hadPreviousSubs) {
streams.subtitle.tracks.getOrNull(streams.subtitle.index)?.let { sub ->
MPVLib.command(arrayOf("sub-add", sub.url, "select", sub.url))
}
} else {
currentVideoList?.getOrNull(streams.quality.index)
?.subtitleTracks?.let { tracks ->
val langIndex = tracks.indexOfFirst {
it.lang.contains(localLangName, true)
}
val requestedLanguage = if (langIndex == -1) 0 else langIndex
tracks.getOrNull(requestedLanguage)?.let { sub ->
hadPreviousSubs = true
streams.subtitle.index = requestedLanguage + 1
MPVLib.command(arrayOf("sub-add", sub.url, "select", sub.url))
}
} ?: run {
val mpvSub = player.tracks.getOrElse("sub") { emptyList() }
.firstOrNull { player.sid == it.mpvId }
streams.subtitle.index = mpvSub?.let {
streams.subtitle.tracks.indexOfFirst { it.url == mpvSub.mpvId.toString() }
}?.coerceAtLeast(0) ?: 0
}
return
}
val subtitleTracks = currentVideoList?.getOrNull(streams.quality.index)
?.subtitleTracks?.takeIf { it.isNotEmpty() }

subtitleTracks?.let { tracks ->
val preferredIndex = subtitleSelect.getPreferredSubtitleIndex(tracks) ?: 0
hadPreviousSubs = true
selectSubtitle(tracks, preferredIndex)
} ?: let {
val tracks = streams.subtitle.tracks.toList()
val preferredIndex = subtitleSelect.getPreferredSubtitleIndex(tracks)
?: let {
val mpvSub = player.tracks["sub"]?.firstOrNull { player.sid == it.mpvId }
mpvSub?.let {
streams.subtitle.tracks.indexOfFirst { it.url == mpvSub.mpvId.toString() }
}?.coerceAtLeast(0) ?: 0
}
selectSubtitle(tracks, preferredIndex, embedded = true)
}
}

private fun setupAudioTracks() {
val localLangName = LocaleHelper.getSimpleLocaleDisplayName()

streams.audio.tracks += player.tracks.getOrElse("audio") { emptyList() }
.drop(1).map { track ->
Track(track.mpvId.toString(), track.name)
}.toTypedArray()

if (hadPreviousAudio) {
streams.audio.tracks.getOrNull(streams.audio.index)?.let { audio ->
MPVLib.command(arrayOf("audio-add", audio.url, "select", audio.url))
Expand All @@ -1806,37 +1868,6 @@ class PlayerActivity : BaseActivity() {
}?.coerceAtLeast(0) ?: 0
}
}

viewModel.viewModelScope.launchUI {
if (playerPreferences.adjustOrientationVideoDimensions().get()) {
if ((player.videoW ?: 1) / (player.videoH ?: 1) >= 1) {
[email protected] =
playerPreferences.defaultPlayerOrientationLandscape().get()

switchControlsOrientation(true)
} else {
[email protected] =
playerPreferences.defaultPlayerOrientationPortrait().get()

switchControlsOrientation(false)
}
}

viewModel.mutableState.update {
it.copy(isLoadingEpisode = false)
}
}
// aniSkip stuff
waitingAniSkip = playerPreferences.waitingTimeAniSkip().get()
runBlocking {
if (aniSkipEnable) {
aniSkipInterval = viewModel.aniSkipResponse(player.duration)
aniSkipInterval?.let {
aniskipStamps = it
updateChapters(it, player.duration)
}
}
}
}

private fun setMpvMediaTitle() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ class PlayerPreferences(

fun mpvInput() = preferenceStore.getString("pref_mpv_input", "")

fun subSelectConf() = preferenceStore.getString("pref_sub_select_conf", "")

fun defaultPlayerOrientationType() = preferenceStore.getInt(
"pref_default_player_orientation_type_key",
10,
Expand Down
Loading
Loading