diff --git a/app/build.gradle b/app/build.gradle index 343b6ecd..17bc00ec 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -171,4 +171,7 @@ dependencies { implementation 'com.facebook.shimmer:shimmer:0.5.0' implementation "androidx.tvprovider:tvprovider:1.0.0" + + // used for subtitle decoding https://github.com/albfernandez/juniversalchardet + implementation 'com.github.albfernandez:juniversalchardet:2.4.0' } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt index 1e28fde6..339553b5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt @@ -394,6 +394,7 @@ abstract class AbstractPlayerFragment( override fun onDestroy() { playerEventListener = null keyEventListener = null + canEnterPipMode = false SubtitlesFragment.applyStyleEvent -= ::onSubStyleChanged keepScreenOn(false) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomSubtitleDecoderFactory.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomSubtitleDecoderFactory.kt index fdacf067..f727e8c0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomSubtitleDecoderFactory.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomSubtitleDecoderFactory.kt @@ -1,6 +1,8 @@ package com.lagradost.cloudstream3.ui.player +import android.content.Context import android.util.Log +import androidx.preference.PreferenceManager import com.google.android.exoplayer2.Format import com.google.android.exoplayer2.text.SubtitleDecoder import com.google.android.exoplayer2.text.SubtitleDecoderFactory @@ -11,13 +13,29 @@ import com.google.android.exoplayer2.text.subrip.SubripDecoder import com.google.android.exoplayer2.text.ttml.TtmlDecoder import com.google.android.exoplayer2.text.webvtt.WebvttDecoder import com.google.android.exoplayer2.util.MimeTypes +import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError +import org.mozilla.universalchardet.UniversalDetector import java.nio.ByteBuffer - class CustomDecoder : SubtitleDecoder { companion object { + fun updateForcedEncoding(context: Context) { + val settingsManager = PreferenceManager.getDefaultSharedPreferences(context) + val value = settingsManager.getString( + context.getString(R.string.subtitles_encoding_key), + null + ) + overrideEncoding = if (value.isNullOrBlank()) { + null + } else { + value + } + } + + private const val UTF_8 = "UTF-8" private const val TAG = "CustomDecoder" + private var overrideEncoding: String? = null var regexSubtitlesToRemoveCaptions = false val bloatRegex = listOf( @@ -65,18 +83,67 @@ class CustomDecoder : SubtitleDecoder { if (realDecoder == null) { inputBuffer.data?.let { data -> // this way we read the subtitle file and decide what decoder to use instead of relying on mimetype + Log.i(TAG, "Got data from queueInputBuffer") - val pos = data.position() - data.position(0) - val arr = ByteArray(minOf(data.remaining(), 100)) - data.get(arr) - data.position(pos) + var (str, charset) = try { + data.position(0) + val fullDataArr = ByteArray(data.remaining()) + data.get(fullDataArr) + val encoding = try { + val encoding = overrideEncoding ?: run { + val detector = UniversalDetector() + detector.handleData(fullDataArr, 0, fullDataArr.size) + detector.dataEnd() + + detector.detectedCharset // "windows-1256" + } + + Log.i( + TAG, + "Detected encoding with charset $encoding and override = $overrideEncoding" + ) + encoding ?: UTF_8 + } catch (e: Exception) { + Log.e(TAG, "Failed to detect encoding throwing error") + logError(e) + UTF_8 + } + + var (fullStr, charset) = try { + val set = charset(encoding) + Pair(String(fullDataArr, set), set) + } catch (e: Exception) { + Log.e(TAG, "Failed to parse using encoding $encoding") + logError(e) + Pair(fullDataArr.decodeToString(), charset(UTF_8)) + } + + bloatRegex.forEach { rgx -> + fullStr = fullStr.replace(rgx, "\n") + } + + fullStr.replace(Regex("(\r\n|\r|\n){2,}"), "\n") + // fullStr = "1\n00:00:01,616 --> 00:00:40,200\n" + + // "تــــرجــمة" + + Log.i( + TAG, + "Encoded Text start: " + fullStr.substring( + 0, + minOf(fullStr.length, 300) + ) + ) + Pair(fullStr, charset) + } catch (e: Exception) { + Log.e(TAG, "Failed to parse text returning plain data") + logError(e) + return + } //https://emptycharacter.com/ //https://www.fileformat.info/info/unicode/char/200b/index.htm - val str = trimStr(arr.decodeToString()) - Log.i(TAG, "Got data from queueInputBuffer") - Log.i(TAG, "first string is >>>$str<<<") + //val str = trimStr(arr.decodeToString()) + //Log.i(TAG, "first string is >>>$str<<<") if (str.isNotEmpty()) { //https://github.com/LagradOst/CloudStream-2/blob/ddd774ee66810137ff7bd65dae70bcf3ba2d2489/CloudStreamForms/CloudStreamForms/Script/MainChrome.cs#L388 realDecoder = when { @@ -93,33 +160,21 @@ class CustomDecoder : SubtitleDecoder { TAG, "Decoder selected: $realDecoder" ) - val decoder = realDecoder - if (decoder != null) { + realDecoder?.let { decoder -> decoder.dequeueInputBuffer()?.let { buff -> if (regexSubtitlesToRemoveCaptions && decoder::class.java != SsaDecoder::class.java) { - try { - data.position(0) - val fullDataArr = ByteArray(data.remaining()) - data.get(fullDataArr) - var fullStr = trimStr(fullDataArr.decodeToString()) - - bloatRegex.forEach { rgx -> - fullStr = fullStr.replace(rgx, "\n") - } - captionRegex.forEach { rgx -> - fullStr = fullStr.replace(rgx, "\n") - } - fullStr.replace(Regex("(\r\n|\r|\n){2,}"), "\n") - - buff.data = ByteBuffer.wrap(fullStr.toByteArray()) - } catch (e: Exception) { - data.position(pos) - buff.data = data + captionRegex.forEach { rgx -> + str = str.replace(rgx, "\n") } - } else { - buff.data = data } + + buff.data = ByteBuffer.wrap(str.toByteArray(charset = charset)) + decoder.queueInputBuffer(buff) + Log.i( + TAG, + "Decoder queueInputBuffer successfully" + ) } CS3IPlayer.requestSubtitleUpdate?.invoke() } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt index 5a3d3f7e..68053cb6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt @@ -15,7 +15,6 @@ import androidx.core.view.isVisible import androidx.lifecycle.ViewModelProvider import androidx.preference.PreferenceManager import com.google.android.exoplayer2.util.MimeTypes -import com.google.android.material.button.MaterialButton import com.hippo.unifile.UniFile import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull @@ -24,6 +23,7 @@ import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.observe +import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.updateForcedEncoding import com.lagradost.cloudstream3.ui.player.PlayerSubtitleHelper.Companion.toSubtitleMimeType import com.lagradost.cloudstream3.ui.result.ResultEpisode import com.lagradost.cloudstream3.ui.result.ResultFragment @@ -32,11 +32,13 @@ import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSet import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.Coroutines.ioSafe +import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage import kotlinx.android.synthetic.main.fragment_player.* import kotlinx.android.synthetic.main.player_custom_layout.* +import kotlinx.android.synthetic.main.player_select_source_and_subs.* import kotlinx.coroutines.Job class GeneratorPlayer : FullScreenPlayer() { @@ -241,14 +243,8 @@ class GeneratorPlayer : FullScreenPlayer() { val sourceDialog = sourceBuilder.create() selectSourceDialog = sourceDialog sourceDialog.show() - val providerList = - sourceDialog.findViewById(R.id.sort_providers)!! - val subtitleList = - sourceDialog.findViewById(R.id.sort_subtitles)!! - val applyButton = - sourceDialog.findViewById(R.id.apply_btt)!! - val cancelButton = - sourceDialog.findViewById(R.id.cancel_btt)!! + val providerList = sourceDialog.sort_providers + val subtitleList = sourceDialog.sort_subtitles val footer: TextView = layoutInflater.inflate(R.layout.sort_bottom_footer_add_choice, null) as TextView @@ -314,11 +310,56 @@ class GeneratorPlayer : FullScreenPlayer() { subtitleList.setItemChecked(which, true) } - cancelButton.setOnClickListener { + sourceDialog.cancel_btt?.setOnClickListener { sourceDialog.dismissSafe(activity) } - applyButton.setOnClickListener { + sourceDialog.subtitles_encoding_format?.apply { + val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx) + + val prefNames = ctx.resources.getStringArray(R.array.subtitles_encoding_list) + val prefValues = ctx.resources.getStringArray(R.array.subtitles_encoding_values) + + val value = settingsManager.getString( + ctx.getString(R.string.subtitles_encoding_key), + null + ) + val index = prefValues.indexOf(value) + text = prefNames[if(index == -1) 0 else index] + } + + sourceDialog.subtitles_click_settings?.setOnClickListener { + val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx) + + val prefNames = ctx.resources.getStringArray(R.array.subtitles_encoding_list) + val prefValues = ctx.resources.getStringArray(R.array.subtitles_encoding_values) + + val currentPrefMedia = + settingsManager.getString( + getString(R.string.subtitles_encoding_key), + null + ) + + val index = prefValues.indexOf(currentPrefMedia) + sourceDialog.dismissSafe(activity) + activity?.showDialog( + prefNames.toList(), + if(index == -1) 0 else index, + ctx.getString(R.string.subtitles_encoding), + true, + {}) { + settingsManager.edit() + .putString( + ctx.getString(R.string.subtitles_encoding_key), + prefValues[it] + ) + .apply() + println("FORCED ENCODING: ${prefValues[it]}") + updateForcedEncoding(ctx) + } + } + + sourceDialog.apply_btt?.setOnClickListener { var init = false if (sourceIndex != startSource) { init = true @@ -477,15 +518,24 @@ class GeneratorPlayer : FullScreenPlayer() { ): SubtitleData? { val langCode = preferredAutoSelectSubtitles ?: return null val lang = SubtitleHelper.fromTwoLettersToLanguage(langCode) ?: return null - - if (settings) - subtitles.firstOrNull { sub -> - val t = sub.name.replace(Regex("[^A-Za-z]"), " ").trim() - t == lang || t.startsWith("$lang ") - || t == langCode - }?.let { sub -> - return sub + if (downloads) { + return subtitles.firstOrNull { sub -> + (sub.origin == SubtitleOrigin.DOWNLOADED_FILE && sub.name == context?.getString( + R.string.default_subtitles + )) } + } + + sortSubs(subtitles).firstOrNull { sub -> + val t = sub.name.replace(Regex("[^A-Za-z]"), " ").trim() + (settings || (downloads && sub.origin == SubtitleOrigin.DOWNLOADED_FILE)) && t == lang || t.startsWith( + "$lang " + ) || t == langCode + }?.let { sub -> + return sub + } + + // post check in case both did not catch anything if (downloads) { return subtitles.firstOrNull { sub -> (sub.origin == SubtitleOrigin.DOWNLOADED_FILE || sub.name == context?.getString( @@ -493,10 +543,11 @@ class GeneratorPlayer : FullScreenPlayer() { )) } } + return null } - private fun autoSelectFromSettings() { + private fun autoSelectFromSettings(): Boolean { // auto select subtitle based of settings val langCode = preferredAutoSelectSubtitles @@ -506,29 +557,34 @@ class GeneratorPlayer : FullScreenPlayer() { if (setSubtitles(sub)) { player.reloadPlayer(ctx) player.handleEvent(CSPlayerEvent.Play) + return true } } } } + return false } - private fun autoSelectFromDownloads() { + private fun autoSelectFromDownloads(): Boolean { if (player.getCurrentPreferredSubtitle() == null) { getAutoSelectSubtitle(currentSubs, settings = false, downloads = true)?.let { sub -> context?.let { ctx -> if (setSubtitles(sub)) { player.reloadPlayer(ctx) player.handleEvent(CSPlayerEvent.Play) + return true } } } } + return false } private fun autoSelectSubtitles() { normalSafeApiCall { - autoSelectFromSettings() - autoSelectFromDownloads() + if (!autoSelectFromSettings()) { + autoSelectFromDownloads() + } } } @@ -567,7 +623,11 @@ class GeneratorPlayer : FullScreenPlayer() { if (season == null) " - ${ctx.getString(R.string.episode)} $episode" else - " \"${ctx.getString(R.string.season_short)}${season}:${ctx.getString(R.string.episode_short)}${episode}\"" + " \"${ctx.getString(R.string.season_short)}${season}:${ + ctx.getString( + R.string.episode_short + ) + }${episode}\"" else "") + if (subName.isNullOrBlank() || subName == headerName) "" else " - $subName" } else { "" @@ -649,6 +709,7 @@ class GeneratorPlayer : FullScreenPlayer() { val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx) titleRez = settingsManager.getInt(getString(R.string.prefer_limit_title_rez_key), 3) limitTitle = settingsManager.getInt(getString(R.string.prefer_limit_title_key), 0) + updateForcedEncoding(ctx) } unwrapBundle(savedInstanceState) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index 9bbd3fd8..f12a3d91 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -61,6 +61,7 @@ enum class Qualities(var value: Int) { 0 -> "Auto" Unknown.value -> "" P2160.value -> "4K" + null -> "" else -> "${qual}p" } } diff --git a/app/src/main/res/layout/player_select_source_and_subs.xml b/app/src/main/res/layout/player_select_source_and_subs.xml index c99ff6ff..1efa00be 100644 --- a/app/src/main/res/layout/player_select_source_and_subs.xml +++ b/app/src/main/res/layout/player_select_source_and_subs.xml @@ -62,19 +62,37 @@ android:orientation="horizontal" tools:ignore="UseCompoundDrawables"> - + android:layout_marginTop="10dp" + android:orientation="horizontal" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + + + + + \ No newline at end of file + app:drawableStartCompat="@drawable/ic_baseline_add_24" + app:drawableTint="?attr/textColor" /> \ No newline at end of file diff --git a/app/src/main/res/values/array.xml b/app/src/main/res/values/array.xml index 2571f779..2347f792 100644 --- a/app/src/main/res/values/array.xml +++ b/app/src/main/res/values/array.xml @@ -1,5 +1,5 @@ - + @id/cast_button_type_rewind_30_seconds @id/cast_button_type_play_pause_toggle @@ -269,4 +269,94 @@ Amoled Light + + + + @string/automatic + Universal (UTF-8) + Universal (UTF-16) + Universal (big endian UTF-16) + Universal (little endian UTF-16) + Universal, Chinese (GB18030) + Western European (Latin-9) + Western European (Windows-1252) + Western European (IBM 00850) + Eastern European (Latin-2) + Eastern European (Windows-1250) + Esperanto (Latin-3) + Nordic (Latin-6) + Cyrillic (Windows-1251) + Russian (KOI8-R) + Ukrainian (KOI8-U) + Arabic (ISO 8859-6) + Arabic (Windows-1256) + Greek (ISO 8859-7) + Greek (Windows-1253) + Hebrew (ISO 8859-8) + Hebrew (Windows-1255) + Turkish (ISO 8859-9) + Turkish (Windows-1254) + Thai (TIS 620-2533/ISO 8859-11) + Thai (Windows-874) + Baltic (Latin-7) + Baltic (Windows-1257) + Celtic (Latin-8) + South-Eastern European (Latin-10) + Simplified Chinese (ISO-2022-CN-EXT) + Simplified Chinese Unix (EUC-CN) + Japanese (7-bits JIS/ISO-2022-JP-2) + Japanese Unix (EUC-JP) + Japanese (Shift JIS) + Korean (EUC-KR/CP949) + Korean (ISO-2022-KR) + Traditional Chinese (Big5) + Traditional Chinese Unix (EUC-TW) + Hong-Kong Supplementary (HKSCS) + Vietnamese (VISCII) + Vietnamese (Windows-1258) + + + + UTF-8 + UTF-16 + UTF-16BE + UTF-16LE + GB18030 + ISO-8859-15 + Windows-1252 + IBM850 + ISO-8859-2 + Windows-1250 + ISO-8859-3 + ISO-8859-10 + Windows-1251 + KOI8-R + KOI8-U + ISO-8859-6 + Windows-1256 + ISO-8859-7 + Windows-1253 + ISO-8859-8 + Windows-1255 + ISO-8859-9 + Windows-1254 + ISO-8859-11 + Windows-874 + ISO-8859-13 + Windows-1257 + ISO-8859-14 + ISO-8859-16 + ISO-2022-CN-EXT + EUC-CN + ISO-2022-JP-2 + EUC-JP + Shift_JIS + CP949 + ISO-2022-KR + Big5 + ISO-2022-TW + Big5-HKSCS + VISCII + Windows-1258 + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4569994a..22d7002b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -51,6 +51,7 @@ show_logcat_key bottom_title_key poster_ui_key + subtitles_encoding_key %d %s | %sMB @@ -418,6 +419,7 @@ Provider languages App Layout Preferred media + Subtitle encoding Preferred media and language User interface