feat: select transcoding codec in network settings / code: clean up, refactors & rework alot of transcoding stuff / bugfixes: mainly deviceId

This commit is contained in:
nomadics9 2024-07-21 04:12:36 +03:00
commit b5d31a6c72
20 changed files with 387 additions and 374 deletions

View file

@ -47,6 +47,7 @@ import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import com.nomadics9.ananas.core.R as CoreR import com.nomadics9.ananas.core.R as CoreR
import com.nomadics9.ananas.models.VideoQuality
var isControlsLocked: Boolean = false var isControlsLocked: Boolean = false
@ -86,12 +87,10 @@ class PlayerActivity : BasePlayerActivity() {
binding = ActivityPlayerBinding.inflate(layoutInflater) binding = ActivityPlayerBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
val changeQualityButton: ImageButton = findViewById(R.id.btnChangeQuality) val changeQualityButton: ImageButton = findViewById(R.id.btnChangeQuality)
changeQualityButton.setOnClickListener { changeQualityButton.setOnClickListener {
showQualitySelectionDialog() showQualitySelectionDialog()
} }
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
binding.playerView.player = viewModel.player binding.playerView.player = viewModel.player
@ -356,8 +355,7 @@ class PlayerActivity : BasePlayerActivity() {
if (appPreferences.playerTrickplay) { if (appPreferences.playerTrickplay) {
val imagePreview = binding.playerView.findViewById<ImageView>(R.id.image_preview) val imagePreview = binding.playerView.findViewById<ImageView>(R.id.image_preview)
previewScrubListener = previewScrubListener = PreviewScrubListener(
PreviewScrubListener(
imagePreview, imagePreview,
timeBar, timeBar,
viewModel.player, viewModel.player,
@ -439,50 +437,32 @@ class PlayerActivity : BasePlayerActivity() {
try { try {
enterPictureInPictureMode(pipParams()) enterPictureInPictureMode(pipParams())
} catch (_: IllegalArgumentException) { } catch (_: IllegalArgumentException) { }
}
} }
private var selectedIndex = 1 // Default to "Original" (index 1)
private fun showQualitySelectionDialog() { private fun showQualitySelectionDialog() {
val height = viewModel.getOriginalHeight() val originalResolution = viewModel.getOriginalResolution() ?: 0
val qualityEntries = resources.getStringArray(CoreR.array.quality_entries).toList() val qualityEntries = resources.getStringArray(CoreR.array.quality_entries).toList()
val qualityValues = resources.getStringArray(CoreR.array.quality_values).toList() val qualityValues = resources.getStringArray(CoreR.array.quality_values).toList()
// Map entries to values val qualities = qualityEntries.toMutableList()
val qualityMap = qualityEntries.zip(qualityValues).toMap() val closestQuality = VideoQuality.entries
.filter { it != VideoQuality.Auto && it != VideoQuality.Original }
.minByOrNull { kotlin.math.abs(it.height*it.width - originalResolution) }
val qualities: List<String> = if (closestQuality != null) {
when (height) { qualities[1] = "${qualities[1]} (${closestQuality})"
0 -> qualityEntries
in 1001..1999 ->
listOf(
qualityEntries[0],
"${qualityEntries[1]} (1080p)",
qualityEntries[2],
qualityEntries[3],
qualityEntries[4],
qualityEntries[5],
)
in 2000..3000 ->
listOf(
qualityEntries[0],
"${qualityEntries[1]} (4K)",
qualityEntries[2],
qualityEntries[3],
qualityEntries[4],
qualityEntries[5],
)
else -> qualityEntries
} }
MaterialAlertDialogBuilder(this) MaterialAlertDialogBuilder(this)
.setTitle("Select Video Quality") .setTitle(CoreR.string.select_quality)
.setItems(qualities.toTypedArray()) { _, which -> .setSingleChoiceItems(qualities.toTypedArray(), selectedIndex) { dialog, which ->
val selectedQualityEntry = qualities[which] selectedIndex = which
val selectedQualityValue = val selectedQualityValue = qualityValues[which]
qualityMap.entries.find { it.key.contains(selectedQualityEntry.split(" ")[0]) }?.value ?: selectedQualityEntry
viewModel.changeVideoQuality(selectedQualityValue) viewModel.changeVideoQuality(selectedQualityValue)
}.show() dialog.dismiss()
}
.show()
} }
override fun onPictureInPictureModeChanged( override fun onPictureInPictureModeChanged(

View file

@ -172,11 +172,11 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
}else if (!appPreferences.downloadQualityDefault) { }else if (!appPreferences.downloadQualityDefault) {
createPickQualityDialog() createPickQualityDialog()
} else { } else {
download() startDownload()
} }
} }
private fun download(){ private fun startDownload(){
binding.itemActions.downloadButton.setIconResource(AndroidR.color.transparent) binding.itemActions.downloadButton.setIconResource(AndroidR.color.transparent)
binding.itemActions.progressDownload.isIndeterminate = true binding.itemActions.progressDownload.isIndeterminate = true
binding.itemActions.progressDownload.isVisible = true binding.itemActions.progressDownload.isVisible = true
@ -413,8 +413,8 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
} }
private fun createPickQualityDialog() { private fun createPickQualityDialog() {
val qualityEntries = resources.getStringArray(com.nomadics9.ananas.core.R.array.quality_entries) val qualityEntries = resources.getStringArray(CoreR.array.download_quality_entries)
val qualityValues = resources.getStringArray(com.nomadics9.ananas.core.R.array.quality_values) val qualityValues = resources.getStringArray(CoreR.array.download_quality_values)
val quality = appPreferences.downloadQuality val quality = appPreferences.downloadQuality
val currentQualityIndex = qualityValues.indexOf(quality) val currentQualityIndex = qualityValues.indexOf(quality)
var selectedQuality = quality var selectedQuality = quality
@ -428,7 +428,7 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
builder.setPositiveButton("Download") { dialog, _ -> builder.setPositiveButton("Download") { dialog, _ ->
appPreferences.downloadQuality = selectedQuality appPreferences.downloadQuality = selectedQuality
dialog.dismiss() dialog.dismiss()
download() startDownload()
} }
builder.setNegativeButton("Cancel") { dialog, _ -> builder.setNegativeButton("Cancel") { dialog, _ ->
dialog.dismiss() dialog.dismiss()

View file

@ -209,11 +209,11 @@ class MovieFragment : Fragment() {
} else if (!appPreferences.downloadQualityDefault) { } else if (!appPreferences.downloadQualityDefault) {
createPickQualityDialog() createPickQualityDialog()
} else { } else {
download() startDownload()
} }
} }
private fun download() { private fun startDownload() {
binding.itemActions.downloadButton.setIconResource(android.R.color.transparent) binding.itemActions.downloadButton.setIconResource(android.R.color.transparent)
binding.itemActions.progressDownload.isIndeterminate = true binding.itemActions.progressDownload.isIndeterminate = true
binding.itemActions.progressDownload.isVisible = true binding.itemActions.progressDownload.isVisible = true
@ -506,8 +506,8 @@ class MovieFragment : Fragment() {
} }
private fun createPickQualityDialog() { private fun createPickQualityDialog() {
val qualityEntries = resources.getStringArray(CoreR.array.quality_entries) val qualityEntries = resources.getStringArray(CoreR.array.download_quality_entries)
val qualityValues = resources.getStringArray(CoreR.array.quality_values) val qualityValues = resources.getStringArray(CoreR.array.download_quality_values)
val quality = appPreferences.downloadQuality val quality = appPreferences.downloadQuality
val currentQualityIndex = qualityValues.indexOf(quality) val currentQualityIndex = qualityValues.indexOf(quality)
var selectedQuality = quality var selectedQuality = quality
@ -520,7 +520,7 @@ class MovieFragment : Fragment() {
} }
builder.setPositiveButton("Download") { dialog, _ -> builder.setPositiveButton("Download") { dialog, _ ->
appPreferences.downloadQuality = selectedQuality appPreferences.downloadQuality = selectedQuality
download() startDownload()
dialog.dismiss() dialog.dismiss()
} }
builder.setNegativeButton("Cancel") { dialog, _ -> builder.setNegativeButton("Cancel") { dialog, _ ->

View file

@ -73,6 +73,7 @@
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="1" /> android:layout_weight="1" />
<!--TODO: Content Desc to Strings-->
<ImageButton <ImageButton
android:id="@+id/btnChangeQuality" android:id="@+id/btnChangeQuality"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -80,7 +81,7 @@
android:background="@drawable/transparent_circle_background" android:background="@drawable/transparent_circle_background"
android:contentDescription="Quality" android:contentDescription="Quality"
android:padding="16dp" android:padding="16dp"
android:src="@drawable/ic_quality" android:src="@drawable/ic_monitor_play"
android:layout_gravity="end" android:layout_gravity="end"
app:tint="@android:color/white" app:tint="@android:color/white"
/> />

View file

@ -79,15 +79,8 @@ class DownloaderImpl(
), ),
) )
} }
val qualityPreference = appPreferences.downloadQuality!! handleDownload(item, source, storageIndex, trickplayInfo, segments, path)
Timber.d("Quality preference: $qualityPreference") return Pair(-1, null)
return if (qualityPreference != "Original") {
Timber.d("Handling Transcoding download for item: ${item.id}")
handleTranscodeDownload(item, source, storageIndex, trickplayInfo, segments, path, qualityPreference)
} else {
Timber.d("Handling original download for item: ${item.id}")
downloadOriginalItem(item, source, storageIndex, trickplayInfo, segments, path)
}
} catch (e: Exception) { } catch (e: Exception) {
try { try {
val source = jellyfinRepository.getMediaSources(item.id).first { it.id == sourceId } val source = jellyfinRepository.getMediaSources(item.id).first { it.id == sourceId }
@ -108,79 +101,7 @@ class DownloaderImpl(
} }
} }
private suspend fun handleTranscodeDownload( private suspend fun handleDownload(
item: FindroidItem,
source: FindroidSource,
storageIndex: Int,
trickplayInfo: FindroidTrickplayInfo?,
segments: List<FindroidSegment>?,
path: Uri,
quality: String,
): Pair<Long, UiText?> {
val transcodingUrl = getTranscodedUrl(item.id, quality)
when (item) {
is FindroidMovie -> {
database.insertMovie(item.toFindroidMovieDto(appPreferences.currentServer!!))
database.insertSource(source.toFindroidSourceDto(item.id, path.path.orEmpty()))
database.insertUserData(item.toFindroidUserDataDto(jellyfinRepository.getUserId()))
downloadExternalMediaStreams(item, source, storageIndex)
downloadEmbeddedMediaStreams(item, source, storageIndex)
if (trickplayInfo != null) {
downloadTrickplayData(item.id, source.id, trickplayInfo)
}
if (segments != null) {
database.insertSegments(segments.toFindroidSegmentsDto(item.id))
}
val request =
DownloadManager
.Request(transcodingUrl)
.setTitle(item.name)
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
.setDestinationUri(path)
val downloadId = downloadManager.enqueue(request)
database.setSourceDownloadId(source.id, downloadId)
return Pair(downloadId, null)
}
is FindroidEpisode -> {
database.insertShow(
jellyfinRepository
.getShow(item.seriesId)
.toFindroidShowDto(appPreferences.currentServer!!),
)
database.insertSeason(
jellyfinRepository.getSeason(item.seasonId).toFindroidSeasonDto(),
)
database.insertEpisode(item.toFindroidEpisodeDto(appPreferences.currentServer!!))
database.insertSource(source.toFindroidSourceDto(item.id, path.path.orEmpty()))
database.insertUserData(item.toFindroidUserDataDto(jellyfinRepository.getUserId()))
downloadExternalMediaStreams(item, source, storageIndex)
downloadEmbeddedMediaStreams(item, source, storageIndex)
if (trickplayInfo != null) {
downloadTrickplayData(item.id, source.id, trickplayInfo)
}
if (segments != null) {
database.insertSegments(segments.toFindroidSegmentsDto(item.id))
}
val request =
DownloadManager
.Request(transcodingUrl)
.setTitle(item.name)
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
.setDestinationUri(path)
val downloadId = downloadManager.enqueue(request)
database.setSourceDownloadId(source.id, downloadId)
return Pair(downloadId, null)
}
}
return Pair(-1, null)
}
private suspend fun downloadOriginalItem(
item: FindroidItem, item: FindroidItem,
source: FindroidSource, source: FindroidSource,
storageIndex: Int, storageIndex: Int,
@ -200,6 +121,22 @@ class DownloaderImpl(
if (segments != null) { if (segments != null) {
database.insertSegments(segments.toFindroidSegmentsDto(item.id)) database.insertSegments(segments.toFindroidSegmentsDto(item.id))
} }
if (appPreferences.downloadQuality != VideoQuality.Original.toString()) {
downloadEmbeddedMediaStreams(item, source, storageIndex)
val transcodingUrl =
getTranscodedUrl(item.id, appPreferences.downloadQuality!!)
val request =
DownloadManager
.Request(transcodingUrl)
.setTitle(item.name)
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
.setDestinationUri(path)
val downloadId = downloadManager.enqueue(request)
database.setSourceDownloadId(source.id, downloadId)
return Pair(downloadId, null)
} else {
val request = val request =
DownloadManager DownloadManager
.Request(source.path.toUri()) .Request(source.path.toUri())
@ -212,6 +149,7 @@ class DownloaderImpl(
database.setSourceDownloadId(source.id, downloadId) database.setSourceDownloadId(source.id, downloadId)
return Pair(downloadId, null) return Pair(downloadId, null)
} }
}
is FindroidEpisode -> { is FindroidEpisode -> {
database.insertShow( database.insertShow(
@ -232,6 +170,22 @@ class DownloaderImpl(
if (segments != null) { if (segments != null) {
database.insertSegments(segments.toFindroidSegmentsDto(item.id)) database.insertSegments(segments.toFindroidSegmentsDto(item.id))
} }
if (appPreferences.downloadQuality != VideoQuality.Original.toString()) {
downloadEmbeddedMediaStreams(item, source, storageIndex)
val transcodingUrl =
getTranscodedUrl(item.id, appPreferences.downloadQuality!!)
val request =
DownloadManager
.Request(transcodingUrl)
.setTitle(item.name)
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
.setDestinationUri(path)
val downloadId = downloadManager.enqueue(request)
database.setSourceDownloadId(source.id, downloadId)
return Pair(downloadId, null)
} else {
val request = val request =
DownloadManager DownloadManager
.Request(source.path.toUri()) .Request(source.path.toUri())
@ -245,6 +199,7 @@ class DownloaderImpl(
return Pair(downloadId, null) return Pair(downloadId, null)
} }
} }
}
return Pair(-1, null) return Pair(-1, null)
} }
@ -483,8 +438,8 @@ class DownloaderImpl(
mediaSourceId, mediaSourceId,
playSessionId, playSessionId,
VideoQuality.getBitrate(videoQuality), VideoQuality.getBitrate(videoQuality),
VideoQuality.getQualityInt(videoQuality),
"mkv", "mkv",
VideoQuality.getHeight(videoQuality),
) )
return downloadUrl.toUri() return downloadUrl.toUri()

View file

@ -0,0 +1,35 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:pathData="M10,7.75a0.75,0.75 0,0 1,1.142 -0.638l3.664,2.249a0.75,0.75 0,0 1,0 1.278l-3.664,2.25a0.75,0.75 0,0 1,-1.142 -0.64z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="@android:color/white"
android:strokeLineCap="round"/>
<path
android:pathData="M12,17v4"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="@android:color/white"
android:strokeLineCap="round"/>
<path
android:pathData="M8,21h8"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="@android:color/white"
android:strokeLineCap="round"/>
<path
android:pathData="M4,3L20,3A2,2 0,0 1,22 5L22,15A2,2 0,0 1,20 17L4,17A2,2 0,0 1,2 15L2,5A2,2 0,0 1,4 3z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="@android:color/white"
android:strokeLineCap="round"/>
</vector>

View file

@ -1,2 +1,20 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources></resources> <resources>
<string name="add_server_error_outdated">اصدار الخادم قديم: %1$s. الرجاء تحديث الخادم</string>
<string name="add_server_error_not_jellyfin">ليس خادم جيلي فن:%1$s</string>
<string name="add_server_error_version">اصدار الخادم غير مدعوم: %1$s. الرجاء تحديث الخادم</string>
<string name="add_server_error_slow">رد الخادم بطيء: %1$s</string>
<string name="login">تسجيل دخول</string>
<string name="login_error_wrong_username_password">اسم المستخدم او الكلمه السريه غير صحيحه</string>
<string name="select_server">اختر الخادم</string>
<string name="edit_text_server_address_hint">عنوان الخادم</string>
<string name="edit_text_username_hint">اسم المستخدم</string>
<string name="edit_text_password_hint">الكلمه السريه</string>
<string name="button_connect">اتصل</string>
<string name="button_login">تسجيل دخول</string>
<string name="remove_server">حذف الخادم</string>
<string name="add_server">اضافه خادم</string>
<string name="add_server_error_not_found">الخادم غير موجود</string>
<string name="add_server_error_empty_address">عنوان الخادم فاضي</string>
<string name="add_server_error_no_id">الخادم غير معرف بالid , يبدو انه هناك خلل في الخادم</string>
</resources>

View file

@ -26,12 +26,12 @@
<item>opensles</item> <item>opensles</item>
</string-array> </string-array>
<string-array name="quality_entries"> <string-array name="quality_entries">
<item>Auto</item> <item>@string/quality_auto</item>
<item>Original</item> <item>@string/quality_original</item>
<item>1080p - 8Mbps</item> <item>@string/quality_1080p</item>
<item>720p - 2Mbps</item> <item>@string/quality_720p</item>
<item>480p - 1Mbps</item> <item>@string/quality_480p</item>
<item>360p - 800Kbps</item> <item>@string/quality_360p</item>
</string-array> </string-array>
<string-array name="quality_values"> <string-array name="quality_values">
<item>Auto</item> <item>Auto</item>
@ -41,4 +41,22 @@
<item>480p</item> <item>480p</item>
<item>360p</item> <item>360p</item>
</string-array> </string-array>
<string-array name="download_quality_entries">
<item>@string/quality_original</item>
<item>@string/quality_1080p</item>
<item>@string/quality_720p</item>
<item>@string/quality_480p</item>
<item>@string/quality_360p</item>
</string-array>
<string-array name="download_quality_values">
<item>Original</item>
<item>1080p</item>
<item>720p</item>
<item>480p</item>
<item>360p</item>
</string-array>
<string-array name="codecs">
<item>h264</item>
<item>hevc</item>
</string-array>
</resources> </resources>

View file

@ -139,6 +139,7 @@
<string name="settings_request_timeout">Request timeout (ms)</string> <string name="settings_request_timeout">Request timeout (ms)</string>
<string name="settings_connect_timeout">Connect timeout (ms)</string> <string name="settings_connect_timeout">Connect timeout (ms)</string>
<string name="settings_socket_timeout">Socket timeout (ms)</string> <string name="settings_socket_timeout">Socket timeout (ms)</string>
<string name="settings_quality_codec">Transcoding codec</string>
<string name="users">Users</string> <string name="users">Users</string>
<string name="add_user">Add user</string> <string name="add_user">Add user</string>
<string name="pref_player_mpv_hwdec">Hardware decoding</string> <string name="pref_player_mpv_hwdec">Hardware decoding</string>
@ -193,6 +194,15 @@
<string name="unmark_as_played">Unmark as played</string> <string name="unmark_as_played">Unmark as played</string>
<string name="add_to_favorites">Add to favorites</string> <string name="add_to_favorites">Add to favorites</string>
<string name="remove_from_favorites">Remove from favorites</string> <string name="remove_from_favorites">Remove from favorites</string>
<string name="quality_default">Default to selected download quality</string>
<string name="download_quality">Download Quality</string>
<string name="select_quality">Select Video Quality</string>
<string name="quality_auto">Auto</string>
<string name="quality_original">Original</string>
<string name="quality_1080p">1080p - 8Mbps</string>
<string name="quality_720p">720p - 3Mbps</string>
<string name="quality_480p">480p - 1.5Mbps</string>
<string name="quality_360p">360p - 0.8Mbps</string>
<string name="alaskarTV_requests">AlaskarTV Requests</string> <string name="alaskarTV_requests">AlaskarTV Requests</string>
<string name="pref_player_trickplay_gesture">Trick Play in seek gesture</string> <string name="pref_player_trickplay_gesture">Trick Play in seek gesture</string>
<string name="pref_player_trickplay_gesture_summary">Requires \'Seek gesture\' and \'Trick Play\'</string> <string name="pref_player_trickplay_gesture_summary">Requires \'Seek gesture\' and \'Trick Play\'</string>

View file

@ -11,15 +11,13 @@
app:title="@string/download_roaming" /> app:title="@string/download_roaming" />
<ListPreference <ListPreference
android:key="pref_downloads_quality" android:key="pref_downloads_quality"
android:title="Download Quality" android:title="@string/download_quality"
android:defaultValue="Original" android:defaultValue="@string/quality_original"
android:entries="@array/quality_entries" android:entries="@array/download_quality_entries"
android:entryValues="@array/quality_values" android:entryValues="@array/download_quality_values"
android:summary="%s" /> android:summary="%s" />
<SwitchPreferenceCompat <SwitchPreferenceCompat
android:defaultValue="false" android:defaultValue="false"
app:key="pref_downloads_quality_default" app:key="pref_downloads_quality_default"
app:summary="Default to picked Download Quality" /> app:summary="@string/quality_default" />
</PreferenceScreen> </PreferenceScreen>

View file

@ -20,4 +20,11 @@
android:defaultValue="true" android:defaultValue="true"
app:key="pref_auto_offline" app:key="pref_auto_offline"
app:title="@string/turn_on_offline_mode_automatically" /> app:title="@string/turn_on_offline_mode_automatically" />
<ListPreference
app:defaultValue="hevc"
app:key="pref_network_codec"
app:title="@string/settings_quality_codec"
app:useSimpleSummaryProvider="true"
app:entries="@array/codecs"
app:entryValues="@array/codecs" />
</PreferenceScreen> </PreferenceScreen>

View file

@ -2,24 +2,30 @@ package com.nomadics9.ananas.models
enum class VideoQuality( enum class VideoQuality(
val bitrate: Int, val bitrate: Int,
val qualityString: String, val height: Int,
val qualityInt: Int, val width: Int,
val isOriginalQuality: Boolean,
) { ) {
PAuto(1, "Auto", 1080), Auto(10000000, 1080, 1920, false),
POriginal(1000000000, "Original", 1080), Original(1000000000, 1080, 1920, true),
P1080(8000000, "1080p", 1080), P3840(12000000,3840, 2160, false), // Here for future proofing and to calculate original resolution only
P720(2000000, "720p", 720), P1080(8000000, 1080, 1920, false),
P480(1000000, "480p", 480), P720(3000000, 720, 1280, false),
P360(700000, "360p", 360), P480(1500000, 480, 854, false),
; P360(800000, 360, 640, false);
override fun toString(): String = when (this) {
Auto -> "Auto"
Original -> "Original"
P3840 -> "4K"
else -> "${height}p"
}
companion object { companion object {
fun fromString(quality: String): VideoQuality? = entries.find { it.qualityString == quality } fun fromString(quality: String): VideoQuality? = entries.find { it.toString() == quality }
fun getBitrate(quality: VideoQuality): Int = quality.bitrate fun getBitrate(quality: VideoQuality): Int = quality.bitrate
fun getHeight(quality: VideoQuality): Int = quality.height
fun getQualityString(quality: VideoQuality): String = quality.qualityString fun getWidth(quality: VideoQuality): Int = quality.width
fun getIsOriginalQuality(quality: VideoQuality): Boolean = quality.isOriginalQuality
fun getQualityInt(quality: VideoQuality): Int = quality.qualityInt
} }
} }

View file

@ -88,40 +88,21 @@ interface JellyfinRepository {
offline: Boolean = false, offline: Boolean = false,
): List<FindroidEpisode> ): List<FindroidEpisode>
suspend fun getMediaSources( suspend fun getMediaSources(itemId: UUID, includePath: Boolean = false): List<FindroidSource>
itemId: UUID,
includePath: Boolean = false,
): List<FindroidSource>
suspend fun getStreamUrl( suspend fun getStreamUrl(itemId: UUID, mediaSourceId: String, playSessionId: String? = null): String
itemId: UUID,
mediaSourceId: String,
playSessionId: String? = null,
): String
suspend fun getSegmentsTimestamps(itemId: UUID): List<FindroidSegment>? suspend fun getSegmentsTimestamps(itemId: UUID): List<FindroidSegment>?
suspend fun getTrickplayData( suspend fun getTrickplayData(itemId: UUID, width: Int, index: Int): ByteArray?
itemId: UUID,
width: Int,
index: Int,
): ByteArray?
suspend fun postCapabilities() suspend fun postCapabilities()
suspend fun postPlaybackStart(itemId: UUID) suspend fun postPlaybackStart(itemId: UUID)
suspend fun postPlaybackStop( suspend fun postPlaybackStop(itemId: UUID, positionTicks: Long, playedPercentage: Int)
itemId: UUID,
positionTicks: Long,
playedPercentage: Int,
)
suspend fun postPlaybackProgress( suspend fun postPlaybackProgress(itemId: UUID, positionTicks: Long, isPaused: Boolean)
itemId: UUID,
positionTicks: Long,
isPaused: Boolean,
)
suspend fun markAsFavorite(itemId: UUID) suspend fun markAsFavorite(itemId: UUID)
@ -155,8 +136,8 @@ interface JellyfinRepository {
mediaSourceId: String, mediaSourceId: String,
playSessionId: String, playSessionId: String,
videoBitrate: Int, videoBitrate: Int,
maxHeight: Int,
container: String, container: String,
maxHeight: Int,
): String ): String
suspend fun getTranscodedVideoStream( suspend fun getTranscodedVideoStream(
@ -175,4 +156,6 @@ interface JellyfinRepository {
): Response<PlaybackInfoResponse> ): Response<PlaybackInfoResponse>
suspend fun stopEncodingProcess(playSessionId: String) suspend fun stopEncodingProcess(playSessionId: String)
suspend fun getAccessToken(): String?
} }

View file

@ -384,7 +384,7 @@ class JellyfinRepositoryImpl(
playSessionId: String?, playSessionId: String?,
): String = ): String =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
// val deviceId = getDeviceId() val deviceId = getDeviceId()
try { try {
val url = val url =
if (playSessionId != null) { if (playSessionId != null) {
@ -393,7 +393,7 @@ class JellyfinRepositoryImpl(
static = true, static = true,
mediaSourceId = mediaSourceId, mediaSourceId = mediaSourceId,
playSessionId = playSessionId, playSessionId = playSessionId,
// deviceId = deviceId, deviceId = deviceId,
context = EncodingContext.STREAMING, context = EncodingContext.STREAMING,
) )
} else { } else {
@ -401,7 +401,7 @@ class JellyfinRepositoryImpl(
itemId, itemId,
static = true, static = true,
mediaSourceId = mediaSourceId, mediaSourceId = mediaSourceId,
// deviceId = deviceId, deviceId = deviceId,
) )
} }
url url
@ -752,8 +752,8 @@ class JellyfinRepositoryImpl(
mediaSourceId: String, mediaSourceId: String,
playSessionId: String, playSessionId: String,
videoBitrate: Int, videoBitrate: Int,
maxHeight: Int,
container: String, container: String,
maxHeight: Int,
): String { ): String {
val url = val url =
jellyfinApi.videosApi.getVideoStreamByContainerUrl( jellyfinApi.videosApi.getVideoStreamByContainerUrl(
@ -764,9 +764,9 @@ class JellyfinRepositoryImpl(
playSessionId = playSessionId, playSessionId = playSessionId,
videoBitRate = videoBitrate, videoBitRate = videoBitrate,
maxHeight = maxHeight, maxHeight = maxHeight,
audioBitRate = 128000, audioBitRate = 328000,
videoCodec = "hevc", videoCodec = appPreferences.transcodeCodec,
audioCodec = "aac", audioCodec = "aac,ac3,eac3",
container = container, container = container,
startTimeTicks = 0, startTimeTicks = 0,
copyTimestamps = true, copyTimestamps = true,
@ -782,7 +782,7 @@ class JellyfinRepositoryImpl(
playSessionId: String, playSessionId: String,
videoBitrate: Int, videoBitrate: Int,
): String { ): String {
val isAuto = videoBitrate == VideoQuality.getBitrate(VideoQuality.PAuto) val isAuto = videoBitrate == VideoQuality.getBitrate(VideoQuality.Auto)
val url: String val url: String
try { try {
url = url =
@ -795,9 +795,9 @@ class JellyfinRepositoryImpl(
playSessionId = playSessionId, playSessionId = playSessionId,
videoBitRate = videoBitrate, videoBitRate = videoBitrate,
enableAdaptiveBitrateStreaming = false, enableAdaptiveBitrateStreaming = false,
audioBitRate = 128000, audioBitRate = 328000,
videoCodec = "hevc", videoCodec = appPreferences.transcodeCodec,
audioCodec = "aac", audioCodec = "aac,ac3,eac3",
startTimeTicks = 0, startTimeTicks = 0,
copyTimestamps = true, copyTimestamps = true,
subtitleMethod = SubtitleDeliveryMethod.EXTERNAL, subtitleMethod = SubtitleDeliveryMethod.EXTERNAL,
@ -813,8 +813,8 @@ class JellyfinRepositoryImpl(
mediaSourceId = mediaSourceId, mediaSourceId = mediaSourceId,
playSessionId = playSessionId, playSessionId = playSessionId,
enableAdaptiveBitrateStreaming = true, enableAdaptiveBitrateStreaming = true,
videoCodec = "hevc", videoCodec = appPreferences.transcodeCodec,
audioCodec = "aac", audioCodec = "aac,ac3,eac3",
startTimeTicks = 0, startTimeTicks = 0,
copyTimestamps = true, copyTimestamps = true,
subtitleMethod = SubtitleDeliveryMethod.EXTERNAL, subtitleMethod = SubtitleDeliveryMethod.EXTERNAL,
@ -839,4 +839,8 @@ class JellyfinRepositoryImpl(
playSessionId = playSessionId, playSessionId = playSessionId,
) )
} }
override suspend fun getAccessToken(): String? {
return jellyfinApi.api.accessToken
}
} }

View file

@ -336,8 +336,8 @@ class JellyfinRepositoryOfflineImpl(
mediaSourceId: String, mediaSourceId: String,
playSessionId: String, playSessionId: String,
videoBitrate: Int, videoBitrate: Int,
maxHeight: Int,
container: String, container: String,
maxHeight: Int,
): String { ): String {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
@ -364,4 +364,8 @@ class JellyfinRepositoryOfflineImpl(
override suspend fun stopEncodingProcess(playSessionId: String) { override suspend fun stopEncodingProcess(playSessionId: String) {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override suspend fun getAccessToken(): String? {
TODO("Not yet implemented")
}
} }

View file

@ -0,0 +1,20 @@
package com.nomadics9.ananas
import androidx.media3.common.MimeTypes
public fun setSubtitlesMimeTypes(codec: String): String {
return when (codec) {
"subrip" -> MimeTypes.APPLICATION_SUBRIP
"webvtt" -> MimeTypes.TEXT_VTT
"ssa" -> MimeTypes.TEXT_SSA
"pgs" -> MimeTypes.APPLICATION_PGS
"ass" -> MimeTypes.TEXT_SSA
"srt" -> MimeTypes.APPLICATION_SUBRIP
"vtt" -> MimeTypes.TEXT_VTT
"ttml" -> MimeTypes.APPLICATION_TTML
"dfxp" -> MimeTypes.APPLICATION_TTML
"stl" -> MimeTypes.APPLICATION_TTML
"sbv" -> MimeTypes.APPLICATION_SUBRIP
else -> MimeTypes.TEXT_UNKNOWN
}
}

View file

@ -31,6 +31,7 @@ import com.nomadics9.ananas.models.VideoQuality
import com.nomadics9.ananas.mpv.MPVPlayer import com.nomadics9.ananas.mpv.MPVPlayer
import com.nomadics9.ananas.player.video.R import com.nomadics9.ananas.player.video.R
import com.nomadics9.ananas.repository.JellyfinRepository import com.nomadics9.ananas.repository.JellyfinRepository
import com.nomadics9.ananas.setSubtitlesMimeTypes
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -57,12 +58,11 @@ class PlayerActivityViewModel
private val application: Application, private val application: Application,
private val jellyfinRepository: JellyfinRepository, private val jellyfinRepository: JellyfinRepository,
private val appPreferences: AppPreferences, private val appPreferences: AppPreferences,
private val jellyfinApi: JellyfinApi,
private val savedStateHandle: SavedStateHandle, private val savedStateHandle: SavedStateHandle,
) : ViewModel(), ) : ViewModel(),
Player.Listener { Player.Listener {
val player: Player val player: Player
private var originalHeight: Int = 0 private var originalResolution: Int? = null
private val _uiState = private val _uiState =
MutableStateFlow( MutableStateFlow(
@ -191,6 +191,21 @@ class PlayerActivityViewModel
).setSubtitleConfigurations(mediaSubtitles) ).setSubtitleConfigurations(mediaSubtitles)
.build() .build()
mediaItems.add(mediaItem) mediaItems.add(mediaItem)
player.addListener(object : Player.Listener {
override fun onPlaybackStateChanged(state: Int) {
if (state == Player.STATE_READY) {
val videoSize = player.videoSize
val initialHeight = videoSize.height
val initialWidth = videoSize.width
originalResolution = initialHeight * initialWidth
Timber.d("Initial video size: $initialWidth x $initialHeight")
player.removeListener(this)
}
}
})
} }
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) Timber.e(e)
@ -538,110 +553,76 @@ class PlayerActivityViewModel
val currentPosition = player.currentPosition val currentPosition = player.currentPosition
viewModelScope.launch { viewModelScope.launch {
val videoQuality = VideoQuality.fromString(quality)!!
try { try {
val deviceProfile = val videoQuality = VideoQuality.fromString(quality)!!
jellyfinRepository.buildDeviceProfile( val deviceProfile = jellyfinRepository.buildDeviceProfile(VideoQuality.getBitrate(videoQuality), "mkv", EncodingContext.STREAMING)
VideoQuality.getBitrate(videoQuality), val playbackInfo = jellyfinRepository.getPostedPlaybackInfo(currentItem.itemId,true,deviceProfile,VideoQuality.getBitrate(videoQuality))
"ts",
EncodingContext.STREAMING,
)
val playbackInfo =
jellyfinRepository.getPostedPlaybackInfo(
currentItem.itemId,
true,
deviceProfile,
VideoQuality.getBitrate(videoQuality),
)
val playSessionId = playbackInfo.content.playSessionId val playSessionId = playbackInfo.content.playSessionId
if (playSessionId != null) { if (playSessionId != null) {
jellyfinRepository.stopEncodingProcess(playSessionId) jellyfinRepository.stopEncodingProcess(playSessionId)
} }
val mediaSources = jellyfinRepository.getMediaSources(currentItem.itemId, true) val mediaSources = jellyfinRepository.getMediaSources(currentItem.itemId, true)
val externalSubtitles = // TODO: can maybe tidy the sub stuff up
currentItem.externalSubtitles.map { externalSubtitle -> val externalSubtitles = currentItem.externalSubtitles.map { externalSubtitle ->
MediaItem.SubtitleConfiguration MediaItem.SubtitleConfiguration.Builder(externalSubtitle.uri)
.Builder(externalSubtitle.uri)
.setLabel(externalSubtitle.title.ifBlank { application.getString(R.string.external) }) .setLabel(externalSubtitle.title.ifBlank { application.getString(R.string.external) })
.setLanguage(externalSubtitle.language.ifBlank { "Unknown" }) .setLanguage(externalSubtitle.language.ifBlank { "Unknown" })
.setMimeType(externalSubtitle.mimeType) .setMimeType(externalSubtitle.mimeType)
.build() .build()
} }
val embeddedSubtitles = val embeddedSubtitles = mediaSources[currentMediaItemIndex].mediaStreams
mediaSources[currentMediaItemIndex]
.mediaStreams
.filter { it.type == MediaStreamType.SUBTITLE && !it.isExternal && it.path != null } .filter { it.type == MediaStreamType.SUBTITLE && !it.isExternal && it.path != null }
.map { mediaStream -> .map { mediaStream ->
val test = mediaStream.codec
Timber.d("Deliver: %s", test)
var deliveryUrl = mediaStream.path var deliveryUrl = mediaStream.path
Timber.d("Deliverurl: %s", deliveryUrl) Timber.d("Deliverurl: %s", deliveryUrl)
// Not sure if still needed
if (mediaStream.codec == "webvtt") { if (mediaStream.codec == "webvtt") {
deliveryUrl = deliveryUrl?.replace("Stream.srt", "Stream.vtt") deliveryUrl = deliveryUrl?.replace("Stream.srt", "Stream.vtt")}
} MediaItem.SubtitleConfiguration.Builder(Uri.parse(deliveryUrl))
MediaItem.SubtitleConfiguration .setMimeType(setSubtitlesMimeTypes(mediaStream.codec))
.Builder(Uri.parse(deliveryUrl)) .setLanguage(mediaStream.language.ifBlank { "Unknown" })
.setMimeType(
when (mediaStream.codec) {
"subrip" -> MimeTypes.APPLICATION_SUBRIP
"webvtt" -> MimeTypes.TEXT_VTT
"ssa" -> MimeTypes.TEXT_SSA
"pgs" -> MimeTypes.APPLICATION_PGS
"ass" -> MimeTypes.TEXT_SSA // ASS is a subtitle format that is essentially an extension of SSA
"srt" -> MimeTypes.APPLICATION_SUBRIP // SRT is another common name for SubRip
"vtt" -> MimeTypes.TEXT_VTT // VTT is a common extension for WebVTT
"ttml" -> MimeTypes.APPLICATION_TTML // TTML (Timed Text Markup Language)
"dfxp" -> MimeTypes.APPLICATION_TTML // DFXP is a profile of TTML
"stl" -> MimeTypes.APPLICATION_TTML // EBU STL (Subtitling Data Exchange Format)
"sbv" -> MimeTypes.APPLICATION_SUBRIP // YouTube's SBV format is similar to SubRip
else -> MimeTypes.TEXT_UNKNOWN
},
).setLanguage(mediaStream.language.ifBlank { "Unknown" })
.setLabel("Embedded") .setLabel("Embedded")
.build() .build()
}.toMutableList() }
.toMutableList()
val allSubtitles = embeddedSubtitles.apply { addAll(externalSubtitles) }
val url = val allSubtitles =
if (VideoQuality.getQualityString(videoQuality) == "Original") { if (VideoQuality.getIsOriginalQuality(videoQuality)) {
externalSubtitles
}else {
embeddedSubtitles.apply { addAll(externalSubtitles) }
}
val url = if (VideoQuality.getIsOriginalQuality(videoQuality)){
jellyfinRepository.getStreamUrl(currentItem.itemId, currentItem.mediaSourceId, playSessionId) jellyfinRepository.getStreamUrl(currentItem.itemId, currentItem.mediaSourceId, playSessionId)
} else { } else {
val mediaSourceId = mediaSources[currentMediaItemIndex].id val mediaSourceId = mediaSources[currentMediaItemIndex].id
val deviceId = jellyfinApi.api.deviceInfo.id val deviceId = jellyfinRepository.getDeviceId()
Timber.d("deviceid = %s", deviceId) val url = jellyfinRepository.getTranscodedVideoStream(currentItem.itemId, deviceId ,mediaSourceId, playSessionId!!, VideoQuality.getBitrate(videoQuality))
val url =
jellyfinRepository.getTranscodedVideoStream(
currentItem.itemId,
deviceId,
mediaSourceId,
playSessionId!!,
VideoQuality.getBitrate(videoQuality),
)
val uriBuilder = url.toUri().buildUpon() val uriBuilder = url.toUri().buildUpon()
val apiKey = jellyfinApi.api.accessToken val apiKey = jellyfinRepository.getAccessToken()
uriBuilder.appendQueryParameter("api_key",apiKey ) uriBuilder.appendQueryParameter("api_key",apiKey )
val newUri = uriBuilder.build() val newUri = uriBuilder.build()
newUri.toString() newUri.toString()
} }
Timber.e("URI IS %s", url) Timber.e("URI IS %s", url)
val mediaItemBuilder = val mediaItemBuilder = MediaItem.Builder()
MediaItem
.Builder()
.setMediaId(currentItem.itemId.toString()) .setMediaId(currentItem.itemId.toString())
.setUri(url) .setUri(url)
.setSubtitleConfigurations(allSubtitles) .setSubtitleConfigurations(allSubtitles)
.setMediaMetadata( .setMediaMetadata(
MediaMetadata MediaMetadata.Builder()
.Builder()
.setTitle(currentItem.name) .setTitle(currentItem.name)
.build(), .build(),
) )
player.pause() player.pause()
player.setMediaItem(mediaItemBuilder.build()) player.setMediaItem(mediaItemBuilder.build())
player.prepare() player.prepare()
@ -649,15 +630,8 @@ class PlayerActivityViewModel
playWhenReady = true playWhenReady = true
player.play() player.play()
val originalHeight =
mediaSources[currentMediaItemIndex]
.mediaStreams
.filter { it.type == MediaStreamType.VIDEO }
.map { mediaStream -> mediaStream.height }
.first() ?: 1080
// Store the original height
this@PlayerActivityViewModel.originalHeight = originalHeight
//isQualityChangeInProgress = true //isQualityChangeInProgress = true
} catch (e: Exception) { } catch (e: Exception) {
@ -666,8 +640,11 @@ class PlayerActivityViewModel
} }
} }
fun getOriginalHeight(): Int = originalHeight fun getOriginalResolution(): Int? {
return originalResolution
} }
}
sealed interface PlayerEvents { sealed interface PlayerEvents {
data object NavigateBack : PlayerEvents data object NavigateBack : PlayerEvents

View file

@ -18,6 +18,7 @@ import com.nomadics9.ananas.models.PlayerChapter
import com.nomadics9.ananas.models.PlayerItem import com.nomadics9.ananas.models.PlayerItem
import com.nomadics9.ananas.models.TrickplayInfo import com.nomadics9.ananas.models.TrickplayInfo
import com.nomadics9.ananas.repository.JellyfinRepository import com.nomadics9.ananas.repository.JellyfinRepository
import com.nomadics9.ananas.setSubtitlesMimeTypes
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -136,6 +137,7 @@ class PlayerViewModel @Inject internal constructor(
} else { } else {
mediaSources[mediaSourceIndex] mediaSources[mediaSourceIndex]
} }
// Embedded Sub externally for offline playback
val externalSubtitles = if (mediaSource.type.toString() == "LOCAL" ) { val externalSubtitles = if (mediaSource.type.toString() == "LOCAL" ) {
mediaSource.mediaStreams mediaSource.mediaStreams
.filter { mediaStream -> .filter { mediaStream ->
@ -146,13 +148,7 @@ class PlayerViewModel @Inject internal constructor(
mediaStream.title, mediaStream.title,
mediaStream.language, mediaStream.language,
Uri.parse(mediaStream.path!!), Uri.parse(mediaStream.path!!),
when (mediaStream.codec) { setSubtitlesMimeTypes(mediaStream.codec),
"subrip" -> MimeTypes.APPLICATION_SUBRIP
"webvtt" -> MimeTypes.APPLICATION_SUBRIP
"pgs" -> MimeTypes.APPLICATION_PGS
"ass" -> MimeTypes.TEXT_SSA
else -> MimeTypes.TEXT_UNKNOWN
},
) )
} }
}else { }else {
@ -165,13 +161,7 @@ class PlayerViewModel @Inject internal constructor(
mediaStream.title, mediaStream.title,
mediaStream.language, mediaStream.language,
Uri.parse(mediaStream.path!!), Uri.parse(mediaStream.path!!),
when (mediaStream.codec) { setSubtitlesMimeTypes(mediaStream.codec)
"subrip" -> MimeTypes.APPLICATION_SUBRIP
"webvtt" -> MimeTypes.APPLICATION_SUBRIP
"pgs" -> MimeTypes.APPLICATION_PGS
"ass" -> MimeTypes.TEXT_SSA
else -> MimeTypes.TEXT_UNKNOWN
},
) )
} }
} }

View file

@ -121,6 +121,11 @@ constructor(
Constants.NETWORK_DEFAULT_SOCKET_TIMEOUT.toString(), Constants.NETWORK_DEFAULT_SOCKET_TIMEOUT.toString(),
)!!.toLongOrNull() ?: Constants.NETWORK_DEFAULT_SOCKET_TIMEOUT )!!.toLongOrNull() ?: Constants.NETWORK_DEFAULT_SOCKET_TIMEOUT
val transcodeCodec get() = sharedPreferences.getString(
Constants.PREF_NETWORK_CODEC,
Constants.NETWORK_DEFAULT_CODEC,
)
// Cache // Cache
val imageCache get() = sharedPreferences.getBoolean( val imageCache get() = sharedPreferences.getBoolean(
Constants.PREF_IMAGE_CACHE, Constants.PREF_IMAGE_CACHE,

View file

@ -43,6 +43,7 @@ object Constants {
const val PREF_NETWORK_REQUEST_TIMEOUT = "pref_network_request_timeout" const val PREF_NETWORK_REQUEST_TIMEOUT = "pref_network_request_timeout"
const val PREF_NETWORK_CONNECT_TIMEOUT = "pref_network_connect_timeout" const val PREF_NETWORK_CONNECT_TIMEOUT = "pref_network_connect_timeout"
const val PREF_NETWORK_SOCKET_TIMEOUT = "pref_network_socket_timeout" const val PREF_NETWORK_SOCKET_TIMEOUT = "pref_network_socket_timeout"
const val PREF_NETWORK_CODEC = "pref_network_codec"
const val PREF_DOWNLOADS_MOBILE_DATA = "pref_downloads_mobile_data" const val PREF_DOWNLOADS_MOBILE_DATA = "pref_downloads_mobile_data"
const val PREF_DOWNLOADS_ROAMING = "pref_downloads_roaming" const val PREF_DOWNLOADS_ROAMING = "pref_downloads_roaming"
const val PREF_DOWNLOADS_QUALITY = "pref_downloads_quality" const val PREF_DOWNLOADS_QUALITY = "pref_downloads_quality"
@ -63,6 +64,7 @@ object Constants {
const val NETWORK_DEFAULT_REQUEST_TIMEOUT = 30_000L const val NETWORK_DEFAULT_REQUEST_TIMEOUT = 30_000L
const val NETWORK_DEFAULT_CONNECT_TIMEOUT = 6_000L const val NETWORK_DEFAULT_CONNECT_TIMEOUT = 6_000L
const val NETWORK_DEFAULT_SOCKET_TIMEOUT = 10_000L const val NETWORK_DEFAULT_SOCKET_TIMEOUT = 10_000L
const val NETWORK_DEFAULT_CODEC = "h264"
// sorting // sorting
// This values must correspond to a SortString from [SortBy] // This values must correspond to a SortString from [SortBy]