From 737577a15a719d881394d2e872b2dabafc92b87b Mon Sep 17 00:00:00 2001 From: Xilin Jia <6257601+XilinJia@users.noreply.github.com> Date: Mon, 18 Nov 2024 21:57:07 +0100 Subject: [PATCH] 6.14.2 commit --- README.md | 4 +- app/build.gradle | 4 +- .../playback/cast/CastEnabledActivity.kt | 3 +- .../podcini/playback/base/LocalMediaPlayer.kt | 27 +-- .../podcini/playback/base/MediaPlayerBase.kt | 9 +- .../playback/service/PlaybackService.kt | 39 +++-- .../mdiq/podcini/storage/database/RealmDB.kt | 2 +- .../podcini/storage/model/EpisodeMedia.kt | 11 +- .../podcini/storage/model/FeedPreferences.kt | 34 +++- .../ac/mdiq/podcini/ui/compose/Composables.kt | 37 ----- .../ui/fragment/FeedSettingsFragment.kt | 155 +++++++++++++----- app/src/main/res/values/strings.xml | 2 + .../playback/cast/CastEnabledActivity.kt | 3 +- .../podcini/playback/cast/CastMediaPlayer.kt | 107 ++++++------ .../mdiq/podcini/playback/cast/CastUtils.kt | 10 +- .../podcini/playback/cast/MediaInfoCreator.kt | 3 +- changelog.md | 6 + .../android/en-US/changelogs/3020301.txt | 5 + gradle/libs.versions.toml | 4 - 19 files changed, 270 insertions(+), 195 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/3020301.txt diff --git a/README.md b/README.md index 77a71b74..ae95ce75 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ An open source podcast instrument, attuned to Puccini ![Puccini](./images/Puccin [F-Droid](https://f-droid.org/packages/ac.mdiq.podcini.R/) [Amazon](https://www.amazon.com/%E8%B4%BE%E8%A5%BF%E6%9E%97-Podcini-R/dp/B0D9WR8P13) +#### The play app of Podcini.R 6.14 allows casting audio-only Youtube media to a Chromecast speaker #### Podcini.R 6.10 allows creating synthetic podcast and shelving any episdes to any synthetic podcasts #### Podcini.R 6.5 as a major step forward brings YouTube contents in the app. Channels can be searched, received from share, subscribed. Podcasts, playlists as well as single media from Youtube and YT Music can be shared to Podcini. For more see the Youtube section below or the changelogs That means finally: [Nessun dorma](https://www.youtube.com/watch?v=cWc7vYjgnTs) @@ -18,7 +19,8 @@ That means finally: [Nessun dorma](https://www.youtube.com/watch?v=cWc7vYjgnTs) #### If you need to cast to an external speaker, you should install the "play" apk, not the "free" apk, that's about the difference between the two. #### Podcini.R requests for permission for unrestricted background activities for uninterrupted background play of a playlist. For more see [this issue](https://github.com/XilinJia/Podcini/issues/88) #### If you intend to sync through a server, be cautious as it's not well tested with Podcini. Welcome any ideas and contribution on this. -#### If you are migrating from Podcini version 5, please read the migrationTo5.md file for migration instructions. + +If you are migrating from Podcini version 5, please read the migrationTo5.md file for migration instructions. This project was developed from a fork of [AntennaPod]() as of Feb 5 2024. diff --git a/app/build.gradle b/app/build.gradle index 59c56dfb..03561da4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -26,8 +26,8 @@ android { vectorDrawables.useSupportLibrary false vectorDrawables.generatedDensities = [] - versionCode 3020300 - versionName "6.14.1" + versionCode 3020301 + versionName "6.14.2" applicationId "ac.mdiq.podcini.R" def commit = "" diff --git a/app/src/free/kotlin/ac/mdiq/podcini/playback/cast/CastEnabledActivity.kt b/app/src/free/kotlin/ac/mdiq/podcini/playback/cast/CastEnabledActivity.kt index aed13e03..cf57740c 100644 --- a/app/src/free/kotlin/ac/mdiq/podcini/playback/cast/CastEnabledActivity.kt +++ b/app/src/free/kotlin/ac/mdiq/podcini/playback/cast/CastEnabledActivity.kt @@ -5,8 +5,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.compose.runtime.Composable /** - * Activity that allows for showing the MediaRouter button whenever there's a cast device in the - * network. + * Activity that allows for showing the MediaRouter button whenever there's a cast device in the network. */ abstract class CastEnabledActivity : AppCompatActivity() { val TAG = this::class.simpleName ?: "Anonymous" diff --git a/app/src/main/kotlin/ac/mdiq/podcini/playback/base/LocalMediaPlayer.kt b/app/src/main/kotlin/ac/mdiq/podcini/playback/base/LocalMediaPlayer.kt index 2d9c933f..c1862f93 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/playback/base/LocalMediaPlayer.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/playback/base/LocalMediaPlayer.kt @@ -136,14 +136,14 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP bufferingUpdateListener = null } - private fun setAudioStreamType(i: Int) { - val a = exoPlayer!!.audioAttributes - val b = AudioAttributes.Builder() - b.setContentType(i) - b.setFlags(a.flags) - b.setUsage(a.usage) - exoPlayer?.setAudioAttributes(b.build(), true) - } +// private fun setAudioStreamType(i: Int) { +// val a = exoPlayer!!.audioAttributes +// val b = AudioAttributes.Builder() +// b.setContentType(i) +// b.setFlags(a.flags) +// b.setUsage(a.usage) +// exoPlayer?.setAudioAttributes(b.build(), true) +// } /** * Starts or prepares playback of the specified Playable object. If another Playable object is already being played, the currently playing @@ -208,7 +208,8 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP val metadata = buildMetadata(curMedia!!) try { callback.ensureMediaInfoLoaded(curMedia!!) - callback.onMediaChanged(false) + // TODO: test + callback.onMediaChanged(true) setPlaybackParams(getCurrentPlaybackSpeed(curMedia), isSkipSilence) CoroutineScope(Dispatchers.IO).launch { when { @@ -484,7 +485,13 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP status = PlayerStatus.STOPPED return } - setAudioStreamType(C.AUDIO_CONTENT_TYPE_SPEECH) + val i = (curMedia as? EpisodeMedia)?.episode?.feed?.preferences?.audioType?: C.AUDIO_CONTENT_TYPE_SPEECH + val a = exoPlayer!!.audioAttributes + val b = AudioAttributes.Builder() + b.setContentType(i) + b.setFlags(a.flags) + b.setUsage(a.usage) + exoPlayer?.setAudioAttributes(b.build(), true) setMediaPlayerListeners() } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/playback/base/MediaPlayerBase.kt b/app/src/main/kotlin/ac/mdiq/podcini/playback/base/MediaPlayerBase.kt index 6fbab07b..da9768f1 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/playback/base/MediaPlayerBase.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/playback/base/MediaPlayerBase.kt @@ -125,7 +125,7 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont } @Throws(IllegalArgumentException::class, IllegalStateException::class) - protected fun setDataSource(metadata: MediaMetadata, media: EpisodeMedia) { + protected open fun setDataSource(metadata: MediaMetadata, media: EpisodeMedia) { Logd(TAG, "setDataSource1 called") val url = media.getStreamUrl() ?: return val preferences = media.episodeOrFetch()?.feed?.preferences @@ -185,8 +185,7 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont private fun setSourceCredentials(user: String?, password: String?) { if (!user.isNullOrEmpty() && !password.isNullOrEmpty()) { if (httpDataSourceFactory == null) - httpDataSourceFactory = OkHttpDataSource.Factory(PodciniHttpClient.getHttpClient() as okhttp3.Call.Factory) - .setUserAgent(ClientConfig.USER_AGENT) + httpDataSourceFactory = OkHttpDataSource.Factory(PodciniHttpClient.getHttpClient() as okhttp3.Call.Factory).setUserAgent(ClientConfig.USER_AGENT) val requestProperties = HashMap() requestProperties["Authorization"] = HttpCredentialEncoder.encode(user, password, "ISO-8859-1") @@ -211,7 +210,7 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont * @param preferVideoOnlyStreams if video-only streams should preferred when both video-only streams and normal video streams are available * @return the sorted list */ - private fun getSortedStreamVideosList(videoStreams: List?, videoOnlyStreams: List?, ascendingOrder: Boolean, + protected fun getSortedStreamVideosList(videoStreams: List?, videoOnlyStreams: List?, ascendingOrder: Boolean, preferVideoOnlyStreams: Boolean): List { val videoStreamsOrdered = if (preferVideoOnlyStreams) listOf(videoStreams, videoOnlyStreams) else listOf(videoOnlyStreams, videoStreams) val allInitialStreams = videoStreamsOrdered.filterNotNull().flatten().toList() @@ -228,7 +227,7 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont } } - private fun getFilteredAudioStreams(audioStreams: List?): List { + protected fun getFilteredAudioStreams(audioStreams: List?): List { if (audioStreams == null) return listOf() val collectedStreams = mutableSetOf() for (stream in audioStreams) { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt b/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt index 566d1e7a..7bc06e86 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt @@ -65,6 +65,7 @@ import ac.mdiq.podcini.util.IntentUtils.sendLocalBroadcast import ac.mdiq.podcini.util.Logd import android.annotation.SuppressLint import android.app.Notification +import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.app.PendingIntent.FLAG_IMMUTABLE @@ -220,8 +221,8 @@ class PlaybackService : MediaLibraryService() { private val taskManagerCallback: TaskManager.PSTMCallback = object : TaskManager.PSTMCallback { override fun positionSaverTick() { + Logd(TAG, "positionSaverTick currentPosition: $curPosition, currentPlaybackSpeed: $curSpeed") if (curPosition != prevPosition) { -// Log.d(TAG, "positionSaverTick currentPosition: $currentPosition, currentPlaybackSpeed: $currentPlaybackSpeed") if (curMedia != null) EventFlow.postEvent(FlowEvent.PlaybackPositionEvent(curMedia, curPosition, curDuration)) skipEndingIfNecessary() persistCurrentPosition(true, null, Playable.INVALID_TIME) @@ -356,15 +357,6 @@ class PlaybackService : MediaLibraryService() { if (ended || smartMarkAsPlayed || autoSkipped || (skipped && !shouldSkipKeepEpisode)) { Logd(TAG, "onPostPlayback ended: $ended smartMarkAsPlayed: $smartMarkAsPlayed autoSkipped: $autoSkipped skipped: $skipped") // only mark the item as played if we're not keeping it anyways - -// item = setPlayStateSync(PlayState.PLAYED.code, item!!, ended || (skipped && smartMarkAsPlayed), false) -// if (playable is EpisodeMedia && (ended || skipped || playingNext)) { -// item = upsert(item!!) { -// it.media?.playbackCompletionDate = Date() -// } -// EventFlow.postEvent(FlowEvent.HistoryEvent()) -// } - if (playable !is EpisodeMedia) item = setPlayStateSync(PlayState.PLAYED.code, item!!, ended || (skipped && smartMarkAsPlayed), false) else { @@ -784,12 +776,9 @@ class PlaybackService : MediaLibraryService() { val keycode = intent?.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1) ?: -1 val customAction = intent?.getStringExtra(MediaButtonReceiver.EXTRA_CUSTOM_ACTION) val hardwareButton = intent?.getBooleanExtra(MediaButtonReceiver.EXTRA_HARDWAREBUTTON, false) == true - val keyEvent: KeyEvent? = if (Build.VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) - intent?.getParcelableExtra(EXTRA_KEY_EVENT, KeyEvent::class.java) - else { - @Suppress("DEPRECATION") - intent?.getParcelableExtra(EXTRA_KEY_EVENT) - } + val keyEvent: KeyEvent? = if (Build.VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) intent?.getParcelableExtra(EXTRA_KEY_EVENT, KeyEvent::class.java) + else intent?.getParcelableExtra(EXTRA_KEY_EVENT) + val playable = curMedia Log.d(TAG, "onStartCommand flags=$flags startId=$startId keycode=$keycode keyEvent=$keyEvent customAction=$customAction hardwareButton=$hardwareButton action=${intent?.action.toString()} ${playable?.getEpisodeTitle()}") if (keycode == -1 && playable == null && customAction == null) { @@ -817,6 +806,19 @@ class PlaybackService : MediaLibraryService() { return super.onStartCommand(intent, flags, startId) } playable != null -> { + if (Build.VERSION.SDK_INT >= 26) { + val CHANNEL_ID = "podcini playback service" + val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager + if (notificationManager.getNotificationChannel(CHANNEL_ID) == null) { + val channel = NotificationChannel(CHANNEL_ID, "Title", NotificationManager.IMPORTANCE_LOW).apply { + setSound(null, null) + enableVibration(false) + } + notificationManager.createNotificationChannel(channel) + } + val notification = NotificationCompat.Builder(this, CHANNEL_ID).setContentTitle("").setContentText("").build() + startForeground(1, notification) + } recreateMediaSessionIfNeeded() Logd(TAG, "onStartCommand status: $status") val allowStreamThisTime = intent?.getBooleanExtra(EXTRA_ALLOW_STREAM_THIS_TIME, false) == true @@ -825,7 +827,8 @@ class PlaybackService : MediaLibraryService() { if (allowStreamAlways) isAllowMobileStreaming = true startPlaying(allowStreamThisTime) // return super.onStartCommand(intent, flags, startId) - return START_NOT_STICKY +// return START_NOT_STICKY + return START_STICKY } else -> Logd(TAG, "onStartCommand case when not (keycode != -1 and playable != null)") } @@ -1163,7 +1166,7 @@ class PlaybackService : MediaLibraryService() { } else duration_ = playable?.getDuration() ?: Playable.INVALID_TIME if (position != Playable.INVALID_TIME && duration_ != Playable.INVALID_TIME && playable != null) { -// Log.d(TAG, "Saving current position to $position $duration") + Logd(TAG, "persistCurrentPosition to $position $duration_ ${playable.getEpisodeTitle()}") playable.setPosition(position) playable.setLastPlayedTime(System.currentTimeMillis()) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/RealmDB.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/RealmDB.kt index 1657a23c..1b289857 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/RealmDB.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/RealmDB.kt @@ -40,7 +40,7 @@ object RealmDB { SubscriptionLog::class, Chapter::class)) .name("Podcini.realm") - .schemaVersion(32) + .schemaVersion(33) .migration({ mContext -> val oldRealm = mContext.oldRealm // old realm using the previous schema val newRealm = mContext.newRealm // new realm using the new schema diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeMedia.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeMedia.kt index f0f4887b..b5fcf58f 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeMedia.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeMedia.kt @@ -300,12 +300,7 @@ class EpisodeMedia: EmbeddedRealmObject, Playable { override fun onPlaybackPause(context: Context) { Logd(TAG, "onPlaybackPause $position $duration") - if (position > startPosition) { -// playedDuration = playedDurationWhenStarted + position - startPosition -// playedDurationWhenStarted = playedDuration - playedDuration = playedDurationWhenStarted + position - startPosition -// playedDurationWhenStarted = playedDuration - } + if (position > startPosition) playedDuration = playedDurationWhenStarted + position - startPosition timeSpent = timeSpentOnStart + (System.currentTimeMillis() - startTime).toInt() startPosition = position } @@ -321,9 +316,7 @@ class EpisodeMedia: EmbeddedRealmObject, Playable { override fun setChapters(chapters: List) { if (episode != null) { episode!!.chapters.clear() - for (c in chapters) { - c.episode = episode - } + for (c in chapters) c.episode = episode episode!!.chapters.addAll(chapters) } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/FeedPreferences.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/FeedPreferences.kt index 22328045..e6aa14a7 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/FeedPreferences.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/FeedPreferences.kt @@ -5,14 +5,12 @@ import ac.mdiq.podcini.playback.base.InTheatre.curQueue import ac.mdiq.podcini.playback.base.VideoMode import ac.mdiq.podcini.storage.database.RealmDB.realm import ac.mdiq.podcini.storage.model.VolumeAdaptionSetting.Companion.fromInteger +import androidx.media3.common.C import io.realm.kotlin.ext.realmSetOf import io.realm.kotlin.types.EmbeddedRealmObject import io.realm.kotlin.types.RealmSet import io.realm.kotlin.types.annotations.Ignore -/** - * Contains preferences for a single feed. - */ class FeedPreferences : EmbeddedRealmObject { var feedID: Long = 0L @@ -50,6 +48,15 @@ class FeedPreferences : EmbeddedRealmObject { } var autoDelete: Int = AutoDeleteAction.GLOBAL.code + @Ignore + var audioTypeSetting: AudioType = AudioType.SPEECH + get() = AudioType.fromCode(audioType) + set(value) { + field = value + audioType = field.code + } + var audioType: Int = AudioType.SPEECH.code + @Ignore var volumeAdaptionSetting: VolumeAdaptionSetting = VolumeAdaptionSetting.OFF get() = fromInteger(volumeAdaption) @@ -132,7 +139,7 @@ class FeedPreferences : EmbeddedRealmObject { autoDLInclude = value?.includeFilterRaw ?: "" autoDLExclude = value?.excludeFilterRaw ?: "" autoDLMinDuration = value?.minimalDurationFilter ?: -1 - markExcludedPlayed = value?.markExcludedPlayed ?: false + markExcludedPlayed = value?.markExcludedPlayed == true } var autoDLInclude: String? = "" var autoDLExclude: String? = "" @@ -140,8 +147,7 @@ class FeedPreferences : EmbeddedRealmObject { var markExcludedPlayed: Boolean = false var autoDLMaxEpisodes: Int = 3 - - var countingPlayed: Boolean = true + var countingPlayed: Boolean = true // relates to autoDLMaxEpisodes @Ignore var autoDLPolicy: AutoDownloadPolicy = AutoDownloadPolicy.ONLY_NEW @@ -215,6 +221,22 @@ class FeedPreferences : EmbeddedRealmObject { } } + enum class AudioType(val code: Int, val tag: String) { + UNKNOWN(C.AUDIO_CONTENT_TYPE_UNKNOWN, "Unknown"), + SPEECH(C.AUDIO_CONTENT_TYPE_SPEECH, "Speech"), + MUSIC(C.AUDIO_CONTENT_TYPE_MUSIC, "Music"), + MOVIE(C.AUDIO_CONTENT_TYPE_MOVIE, "Movie"); + + companion object { + fun fromCode(code: Int): AudioType { + return enumValues().firstOrNull { it.code == code } ?: SPEECH + } + fun fromTag(tag: String): AudioType { + return enumValues().firstOrNull { it.tag == tag } ?: SPEECH + } + } + } + enum class AVQuality(val code: Int, val tag: String) { GLOBAL(0, "Global"), LOW(1, "Low"), diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/Composables.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/Composables.kt index 82d1b222..0dd765f2 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/Composables.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/Composables.kt @@ -217,40 +217,3 @@ fun NonlazyGrid(columns: Int, itemCount: Int, modifier: Modifier = Modifier, con } } } - -@Composable -fun AutoCompleteTextField(suggestions: List) { - var text by remember { mutableStateOf("") } - var filteredSuggestions by remember { mutableStateOf(suggestions) } - var showSuggestions by remember { mutableStateOf(false) } - - Column { - TextField(value = text, onValueChange = { - text = it - filteredSuggestions = suggestions.filter { item -> - item.contains(text, ignoreCase = true) - } - showSuggestions = text.isNotEmpty() && filteredSuggestions.isNotEmpty() - }, - placeholder = { Text("Type something...") }, - keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done), - keyboardActions = KeyboardActions( - onDone = { - } - ), - modifier = Modifier.fillMaxWidth() - ) - - if (showSuggestions) { - LazyColumn(modifier = Modifier.fillMaxWidth().heightIn(min = 0.dp, max = 200.dp)) { - items(filteredSuggestions.size) { index -> - Text(text = filteredSuggestions[index], modifier = Modifier.clickable(onClick = { - text = filteredSuggestions[index] - showSuggestions = false - }).padding(8.dp)) - - } - } - } - } -} diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedSettingsFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedSettingsFragment.kt index e190b7d2..ac74a06e 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedSettingsFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedSettingsFragment.kt @@ -8,6 +8,7 @@ import ac.mdiq.podcini.net.feed.FeedUpdateManager.runOnce import ac.mdiq.podcini.playback.base.VideoMode import ac.mdiq.podcini.playback.base.VideoMode.Companion.videoModeTags import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownload +import ac.mdiq.podcini.storage.database.Feeds.getTags import ac.mdiq.podcini.storage.database.Feeds.persistFeedPreferences import ac.mdiq.podcini.storage.database.RealmDB.realm import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk @@ -39,16 +40,20 @@ import androidx.appcompat.app.AlertDialog import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog @@ -115,6 +120,22 @@ class FeedSettingsFragment : Fragment() { Text(text = stringResource(R.string.keep_updated_summary), style = MaterialTheme.typography.bodyMedium, color = textColor) } } + Column { + var showDialog by remember { mutableStateOf(false) } + var selectedOption by remember { mutableStateOf(feed?.preferences?.audioTypeSetting?.tag ?: FeedPreferences.AudioType.SPEECH.tag) } + if (showDialog) SetAudioType(selectedOption = selectedOption, onDismissRequest = { showDialog = false }) + Row(Modifier.fillMaxWidth()) { + Icon(ImageVector.vectorResource(id = R.drawable.baseline_audiotrack_24), "", tint = textColor) + Spacer(modifier = Modifier.width(20.dp)) + Text(text = stringResource(R.string.pref_feed_audio_type), style = MaterialTheme.typography.titleLarge, color = textColor, + modifier = Modifier.clickable(onClick = { + selectedOption = feed!!.preferences?.audioTypeSetting?.tag ?: FeedPreferences.AudioType.SPEECH.tag + showDialog = true + }) + ) + } + Text(text = stringResource(R.string.pref_feed_audio_type_sum), style = MaterialTheme.typography.bodyMedium, color = textColor) + } if ((feed?.id?:0) >= MAX_NATURAL_SYNTHETIC_ID && feed?.hasVideoMedia == true) { // video mode Column { @@ -240,11 +261,14 @@ class FeedSettingsFragment : Fragment() { } // tags Column { + var showDialog by remember { mutableStateOf(false) } + if (showDialog) TagSettingDialog(onDismiss = { showDialog = false }) Row(Modifier.fillMaxWidth()) { Icon(ImageVector.vectorResource(id = R.drawable.ic_tag), "", tint = textColor) Spacer(modifier = Modifier.width(20.dp)) Text(text = stringResource(R.string.feed_tags_label), style = MaterialTheme.typography.titleLarge, color = textColor, modifier = Modifier.clickable(onClick = { +// showDialog = true val dialog = TagSettingsDialog.newInstance(listOf(feed!!)) dialog.show(parentFragmentManager, TagSettingsDialog.TAG) }) @@ -633,35 +657,20 @@ class FeedSettingsFragment : Fragment() { } @Composable - private fun SetAudioQuality(selectedOption: String, onDismissRequest: () -> Unit) { + private fun SetAudioType(selectedOption: String, onDismissRequest: () -> Unit) { var selected by remember {mutableStateOf(selectedOption)} Dialog(onDismissRequest = { onDismissRequest() }) { Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) { Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { - FeedPreferences.AVQuality.entries.forEach { option -> + FeedPreferences.AudioType.entries.forEach { option -> Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { Checkbox(checked = option.tag == selected, onCheckedChange = { isChecked -> selected = option.tag if (isChecked) Logd(TAG, "$option is checked") - when (selected) { - FeedPreferences.AVQuality.LOW.tag -> { - feed = upsertBlk(feed!!) { it.preferences?.audioQuality = FeedPreferences.AVQuality.LOW.code } - onDismissRequest() - } - FeedPreferences.AVQuality.MEDIUM.tag -> { - feed = upsertBlk(feed!!) { it.preferences?.audioQuality = FeedPreferences.AVQuality.MEDIUM.code } - onDismissRequest() - } - FeedPreferences.AVQuality.HIGH.tag -> { - feed = upsertBlk(feed!!) { it.preferences?.audioQuality = FeedPreferences.AVQuality.HIGH.code } - onDismissRequest() - } - else -> { - feed = upsertBlk(feed!!) { it.preferences?.audioQuality = FeedPreferences.AVQuality.GLOBAL.code } - onDismissRequest() - } - } + val type = FeedPreferences.AudioType.fromTag(selected) + feed = upsertBlk(feed!!) { it.preferences?.audioType = type.code } + onDismissRequest() } ) Text(option.tag) @@ -672,6 +681,30 @@ class FeedSettingsFragment : Fragment() { } } + @Composable + private fun SetAudioQuality(selectedOption: String, onDismissRequest: () -> Unit) { + var selected by remember {mutableStateOf(selectedOption)} + Dialog(onDismissRequest = { onDismissRequest() }) { + Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) { + Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { + FeedPreferences.AVQuality.entries.forEach { option -> + Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + Checkbox(checked = option.tag == selected, + onCheckedChange = { isChecked -> + selected = option.tag + if (isChecked) Logd(TAG, "$option is checked") + val type = FeedPreferences.AVQuality.fromTag(selected) + feed = upsertBlk(feed!!) { it.preferences?.audioQuality = type.code } + onDismissRequest() + }) + Text(option.tag) + } + } + } + } + } + } + @Composable private fun SetVideoQuality(selectedOption: String, onDismissRequest: () -> Unit) { var selected by remember {mutableStateOf(selectedOption)} @@ -684,26 +717,10 @@ class FeedSettingsFragment : Fragment() { onCheckedChange = { isChecked -> selected = option.tag if (isChecked) Logd(TAG, "$option is checked") - when (selected) { - FeedPreferences.AVQuality.LOW.tag -> { - feed = upsertBlk(feed!!) { it.preferences?.videoQuality = FeedPreferences.AVQuality.LOW.code } - onDismissRequest() - } - FeedPreferences.AVQuality.MEDIUM.tag -> { - feed = upsertBlk(feed!!) { it.preferences?.videoQuality = FeedPreferences.AVQuality.MEDIUM.code } - onDismissRequest() - } - FeedPreferences.AVQuality.HIGH.tag -> { - feed = upsertBlk(feed!!) { it.preferences?.videoQuality = FeedPreferences.AVQuality.HIGH.code } - onDismissRequest() - } - else -> { - feed = upsertBlk(feed!!) { it.preferences?.videoQuality = FeedPreferences.AVQuality.GLOBAL.code } - onDismissRequest() - } - } - } - ) + val type = FeedPreferences.AVQuality.fromTag(selected) + feed = upsertBlk(feed!!) { it.preferences?.videoQuality = type.code } + onDismissRequest() + }) Text(option.tag) } } @@ -712,6 +729,62 @@ class FeedSettingsFragment : Fragment() { } } + @OptIn(ExperimentalLayoutApi::class) + @Composable + fun TagSettingDialog(onDismiss: () -> Unit) { + Dialog(onDismissRequest = onDismiss) { + val suggestions = remember { getTags() } + Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) { + Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { + var text by remember { mutableStateOf("") } + var filteredSuggestions by remember { mutableStateOf(suggestions) } + var showSuggestions by remember { mutableStateOf(false) } + var tags = remember { mutableStateListOf() } + Column { + FlowRow { + tags.forEach { + FilterChip(onClick = { }, label = { Text(text) }, selected = false, + trailingIcon = { Icon(imageVector = Icons.Filled.Close, contentDescription = "Close icon", modifier = Modifier.size(FilterChipDefaults.IconSize).clickable( + onClick = { + })) }) + } + } + TextField(value = text, onValueChange = { + text = it + filteredSuggestions = suggestions.filter { item -> + item.contains(text, ignoreCase = true) + } + showSuggestions = text.isNotEmpty() && filteredSuggestions.isNotEmpty() + }, + placeholder = { Text("Type something...") }, + keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done), + keyboardActions = KeyboardActions( + onDone = { + tags.add(text) + } + ), + modifier = Modifier.fillMaxWidth() + ) + if (showSuggestions) { + LazyColumn(modifier = Modifier.fillMaxWidth().heightIn(min = 0.dp, max = 200.dp)) { + items(filteredSuggestions.size) { index -> + Text(text = filteredSuggestions[index], modifier = Modifier.clickable(onClick = { + text = filteredSuggestions[index] + showSuggestions = false + }).padding(8.dp)) + + } + } + } + } + Button(onClick = { + onDismiss() + }) { Text("Confirm") } + } + } + } + } + @Composable fun AuthenticationDialog(onDismiss: () -> Unit) { Dialog(onDismissRequest = onDismiss) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 12d5e3fb..99e03afe 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -538,6 +538,8 @@ Auto skip Skip introductions and ending credits. Associated + Audio type + Either Speech, Music or Movie for improved processing. Audio quality Global generally equals high quality except when prefLowQualityMedia is set for metered network. Quality setting here takes precedence over the setting of prefLowQualityMedia for metered network. Video quality diff --git a/app/src/play/kotlin/ac/mdiq/podcini/playback/cast/CastEnabledActivity.kt b/app/src/play/kotlin/ac/mdiq/podcini/playback/cast/CastEnabledActivity.kt index f30755d5..0d6e7d4f 100644 --- a/app/src/play/kotlin/ac/mdiq/podcini/playback/cast/CastEnabledActivity.kt +++ b/app/src/play/kotlin/ac/mdiq/podcini/playback/cast/CastEnabledActivity.kt @@ -22,8 +22,7 @@ import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.GoogleApiAvailability /** - * Activity that allows for showing the MediaRouter button whenever there's a cast device in the - * network. + * Activity that allows for showing the MediaRouter button whenever there's a cast device in the network. */ abstract class CastEnabledActivity : AppCompatActivity() { private var canCast by mutableStateOf(false) diff --git a/app/src/play/kotlin/ac/mdiq/podcini/playback/cast/CastMediaPlayer.kt b/app/src/play/kotlin/ac/mdiq/podcini/playback/cast/CastMediaPlayer.kt index 3788b110..5e7631b1 100644 --- a/app/src/play/kotlin/ac/mdiq/podcini/playback/cast/CastMediaPlayer.kt +++ b/app/src/play/kotlin/ac/mdiq/podcini/playback/cast/CastMediaPlayer.kt @@ -1,23 +1,22 @@ package ac.mdiq.podcini.playback.cast +import ac.mdiq.podcini.net.utils.NetworkUtils.isNetworkRestricted import ac.mdiq.podcini.playback.base.InTheatre.curMedia import ac.mdiq.podcini.playback.base.MediaPlayerBase import ac.mdiq.podcini.playback.base.MediaPlayerCallback import ac.mdiq.podcini.playback.base.PlayerStatus import ac.mdiq.podcini.preferences.UserPreferences.isSkipSilence -import ac.mdiq.podcini.storage.model.EpisodeMedia -import ac.mdiq.podcini.storage.model.MediaType -import ac.mdiq.podcini.storage.model.Playable -import ac.mdiq.podcini.storage.model.RemoteMedia -import ac.mdiq.podcini.util.Logd +import ac.mdiq.podcini.preferences.UserPreferences.prefLowQualityMedia +import ac.mdiq.podcini.storage.model.* import ac.mdiq.podcini.util.EventFlow import ac.mdiq.podcini.util.FlowEvent +import ac.mdiq.podcini.util.Logd import android.annotation.SuppressLint import android.app.UiModeManager -import android.bluetooth.BluetoothClass.Service.AUDIO import android.content.Context import android.content.res.Configuration import android.util.Log +import androidx.media3.common.MediaMetadata import com.google.android.gms.cast.* import com.google.android.gms.cast.framework.CastContext import com.google.android.gms.cast.framework.CastState @@ -30,7 +29,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.IOException import java.util.concurrent.atomic.AtomicBoolean -import kotlin.concurrent.Volatile import kotlin.math.max import kotlin.math.min @@ -211,7 +209,6 @@ class CastMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaPl var nextPlayable: Playable? = playable do { nextPlayable = callback.getNextInQueue(nextPlayable) } while (nextPlayable != null && !CastUtils.isCastable(nextPlayable, castContext.sessionManager.currentCastSession)) - if (nextPlayable != null) playMediaObject(nextPlayable, streaming, startWhenPrepared, prepareImmediately, forceReset) return } @@ -222,18 +219,10 @@ class CastMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaPl Logd(TAG, "Method call to playMediaObject was ignored: media file already playing.") return } else { - // set temporarily to pause in order to update list with current position - val isPlaying = remoteMediaClient?.isPlaying ?: false - val position = remoteMediaClient?.approximateStreamPosition?.toInt() ?: 0 - if (isPlaying) callback.onPlaybackPause(curMedia, position) - if (status == PlayerStatus.PLAYING) { - val pos = curMedia?.getPosition() ?: -1 - seekTo(pos) - callback.onPlaybackPause(curMedia, pos) + if (curMedia?.getIdentifier() != prevMedia?.getIdentifier()) { + prevMedia = curMedia + callback.onPostPlayback(prevMedia, false, false, true) } - if (prevMedia != null && curMedia!!.getIdentifier() != prevMedia?.getIdentifier()) - callback.onPostPlayback(prevMedia, false, skipped = false, playingNext = true) - prevMedia = curMedia setPlayerStatus(PlayerStatus.INDETERMINATE, null) } } @@ -246,34 +235,27 @@ class CastMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaPl val metadata = buildMetadata(curMedia!!) try { callback.ensureMediaInfoLoaded(curMedia!!) - callback.onMediaChanged(false) + // TODO: test + callback.onMediaChanged(true) setPlaybackParams(getCurrentPlaybackSpeed(curMedia), isSkipSilence) - CoroutineScope(Dispatchers.IO).launch { - when { - streaming -> { - val streamurl = curMedia!!.getStreamUrl() - if (streamurl != null) { - val media = curMedia - if (media is EpisodeMedia) { - mediaItem = null - mediaSource = null - setDataSource(metadata, media) - } else setDataSource(metadata, streamurl, null, null) - } - } - else -> { - val localMediaurl = curMedia!!.getLocalMediaUrl() - if (!localMediaurl.isNullOrEmpty()) setDataSource(metadata, localMediaurl, null, null) - else throw IOException("Unable to read local file $localMediaurl") + when { + streaming -> { + val streamurl = curMedia!!.getStreamUrl() + if (streamurl != null) { + val media = curMedia + if (media is EpisodeMedia) { + mediaItem = null + mediaSource = null + setDataSource(metadata, media) + } else setDataSource(metadata, streamurl, null, null) } } - mediaInfo = toMediaInfo(playable) - withContext(Dispatchers.Main) { - val uiModeManager = context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager - if (uiModeManager.currentModeType != Configuration.UI_MODE_TYPE_CAR) setPlayerStatus(PlayerStatus.INITIALIZED, curMedia) - if (prepareImmediately) prepare() - } + else -> {} } + mediaInfo = toMediaInfo(playable) + val uiModeManager = context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager + if (uiModeManager.currentModeType != Configuration.UI_MODE_TYPE_CAR) setPlayerStatus(PlayerStatus.INITIALIZED, curMedia) + if (prepareImmediately) prepare() } catch (e: IOException) { e.printStackTrace() setPlayerStatus(PlayerStatus.ERROR, null) @@ -283,11 +265,30 @@ class CastMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaPl setPlayerStatus(PlayerStatus.ERROR, null) EventFlow.postStickyEvent(FlowEvent.PlayerErrorEvent(e.localizedMessage ?: "")) } finally { } + } -// callback.ensureMediaInfoLoaded(curMedia!!) -// callback.onMediaChanged(true) -// setPlayerStatus(PlayerStatus.INITIALIZED, curMedia) -// if (prepareImmediately) prepare() + @Throws(IllegalArgumentException::class, IllegalStateException::class) + override fun setDataSource(metadata: MediaMetadata, media: EpisodeMedia) { + Logd(TAG, "setDataSource1 called") + if (media.episode?.feed?.type == Feed.FeedType.YOUTUBE.name) { + Logd(TAG, "setDataSource1 setting for YouTube source") + try { + val streamInfo = media.episode!!.streamInfo ?: return + val audioStreamsList = getFilteredAudioStreams(streamInfo.audioStreams) + Logd(TAG, "setDataSource1 audioStreamsList ${audioStreamsList.size}") + val audioIndex = if (isNetworkRestricted && prefLowQualityMedia && media.episode?.feed?.preferences?.audioQualitySetting == FeedPreferences.AVQuality.GLOBAL) 0 else { + when (media.episode?.feed?.preferences?.audioQualitySetting) { + FeedPreferences.AVQuality.LOW -> 0 + FeedPreferences.AVQuality.MEDIUM -> audioStreamsList.size / 2 + FeedPreferences.AVQuality.HIGH -> audioStreamsList.size - 1 + else -> audioStreamsList.size - 1 + } + } + val audioStream = audioStreamsList[audioIndex] + Logd(TAG, "setDataSource1 use audio quality: ${audioStream.bitrate} forceVideo: ${media.forceVideo}") + media.audioUrl = audioStream.content + } catch (throwable: Throwable) { Log.e(TAG, "setDataSource1 error: ${throwable.message}") } + } } override fun resume() { @@ -330,13 +331,15 @@ class CastMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaPl } override fun getDuration(): Int { - var retVal = remoteMediaClient?.streamDuration?.toInt() ?: 0 +// if (curMedia != null && remoteMediaClient?.currentItem?.media?.entity != curMedia?.getIdentifier().toString()) return curMedia!!.getDuration() + var retVal = remoteMediaClient?.streamDuration?.toInt() ?: Playable.INVALID_TIME if (retVal == Playable.INVALID_TIME && curMedia != null && curMedia!!.getDuration() > 0) retVal = curMedia!!.getDuration() return retVal } override fun getPosition(): Int { - var retVal = remoteMediaClient?.approximateStreamPosition?.toInt() ?: 0 +// Logd(TAG, "getPosition: $status ${remoteMediaClient?.approximateStreamPosition} ${curMedia?.getPosition()} ${remoteMediaClient?.currentItem?.media?.entity} ${curMedia?.getIdentifier().toString()} ${curMedia?.getEpisodeTitle()}") + var retVal = remoteMediaClient?.approximateStreamPosition?.toInt() ?: Playable.INVALID_TIME if (retVal <= 0 && curMedia != null && curMedia!!.getPosition() >= 0) retVal = curMedia!!.getPosition() return retVal } @@ -347,8 +350,7 @@ class CastMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaPl } override fun getPlaybackSpeed(): Float { - val status = remoteMediaClient?.mediaStatus - return status?.playbackRate?.toFloat() ?: 1.0f + return remoteMediaClient?.mediaStatus?.playbackRate?.toFloat() ?: 1.0f } override fun setVolume(volumeLeft: Float, volumeRight: Float) { @@ -357,11 +359,12 @@ class CastMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaPl } override fun shutdown() { + remoteMediaClient?.stop() remoteMediaClient?.unregisterCallback(remoteMediaClientCallback) } override fun setPlayable(playable: Playable?) { - if (playable !== curMedia) { + if (playable != null && playable !== curMedia) { curMedia = playable mediaInfo = toMediaInfo(playable) } diff --git a/app/src/play/kotlin/ac/mdiq/podcini/playback/cast/CastUtils.kt b/app/src/play/kotlin/ac/mdiq/podcini/playback/cast/CastUtils.kt index 7328df7b..b22f3fed 100644 --- a/app/src/play/kotlin/ac/mdiq/podcini/playback/cast/CastUtils.kt +++ b/app/src/play/kotlin/ac/mdiq/podcini/playback/cast/CastUtils.kt @@ -1,5 +1,6 @@ package ac.mdiq.podcini.playback.cast +import ac.mdiq.podcini.playback.base.VideoMode import ac.mdiq.podcini.storage.model.* import ac.mdiq.podcini.util.Logd import android.content.ContentResolver @@ -9,9 +10,6 @@ import com.google.android.gms.cast.MediaInfo import com.google.android.gms.cast.MediaMetadata import com.google.android.gms.cast.framework.CastSession -/** - * Helper functions for Cast support. - */ object CastUtils { private val TAG: String = CastUtils::class.simpleName ?: "Anonymous" @@ -45,7 +43,11 @@ object CastUtils { if (url.startsWith(ContentResolver.SCHEME_CONTENT)) return false /* Local feed */ return when (media.getMediaType()) { MediaType.AUDIO -> castSession.castDevice!!.hasCapability(CastDevice.CAPABILITY_AUDIO_OUT) - MediaType.VIDEO -> castSession.castDevice!!.hasCapability(CastDevice.CAPABILITY_VIDEO_OUT) + MediaType.VIDEO -> { + if ((media as? EpisodeMedia)?.episode?.feed?.preferences?.videoModePolicy == VideoMode.AUDIO_ONLY) + castSession.castDevice!!.hasCapability(CastDevice.CAPABILITY_AUDIO_OUT) + else castSession.castDevice!!.hasCapability(CastDevice.CAPABILITY_VIDEO_OUT) + } else -> false } } diff --git a/app/src/play/kotlin/ac/mdiq/podcini/playback/cast/MediaInfoCreator.kt b/app/src/play/kotlin/ac/mdiq/podcini/playback/cast/MediaInfoCreator.kt index fe14c3da..a2904d5e 100644 --- a/app/src/play/kotlin/ac/mdiq/podcini/playback/cast/MediaInfoCreator.kt +++ b/app/src/play/kotlin/ac/mdiq/podcini/playback/cast/MediaInfoCreator.kt @@ -91,12 +91,13 @@ object MediaInfoCreator { metadata.putInt(CastUtils.KEY_FORMAT_VERSION, CastUtils.FORMAT_VERSION_VALUE) metadata.putString(CastUtils.KEY_STREAM_URL, media.getStreamUrl()!!) - Logd("MediaInfoCreator", "media.mimeType: ${media.mimeType} ${media.audioUrl}") + Logd("MediaInfoCreator", "media.mimeType: ${media.getIdentifier()} ${feedItem?.title}") // TODO: these are hardcoded for audio only // val builder = MediaInfo.Builder(media.getStreamUrl()!!) // .setContentType(media.mimeType) var url: String = if (media.getMediaType() == MediaType.AUDIO) media.getStreamUrl() ?: "" else media.audioUrl val builder = MediaInfo.Builder(url) + .setEntity(media.getIdentifier().toString()) .setContentType("audio/*") .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) .setMetadata(metadata) diff --git a/changelog.md b/changelog.md index f20eb2de..89431c98 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,9 @@ +# 6.14.2 + +* in feed settings, added audio type setting (Speech, Music, Movie) for improved audio processing from media3 +* improved the behavior of the cast player in the Play app + * casting youtube audio appears working fine + # 6.14.1 * changed the term "virtual queue" to "natural queue" in the literature to refer to the list of episodes in a given feed diff --git a/fastlane/metadata/android/en-US/changelogs/3020301.txt b/fastlane/metadata/android/en-US/changelogs/3020301.txt new file mode 100644 index 00000000..f2809c6e --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/3020301.txt @@ -0,0 +1,5 @@ + Version 6.14.2 + +* in feed settings, added audio type setting (Speech, Music, Movie) for improved audio processing from media3 +* improved the behavior of the cast player in the Play app + * casting youtube audio appears working fine diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 35c12081..3bbe0d7e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,7 +20,6 @@ fyydlin = "v0.5.0" googleMaterialTypeface = "4.0.0.3-kotlin" googleMaterialTypefaceOutlined = "4.0.0.2-kotlin" gradle = "8.6.1" -#gridlayout = "1.0.0" groovyXml = "3.0.19" iconicsCore = "5.5.0-b01" iconicsViews = "5.5.0-b01" @@ -51,7 +50,6 @@ rxjavaVersion = "3.1.8" searchpreference = "v2.5.0" uiToolingPreview = "1.7.5" uiTooling = "1.7.5" -#viewpager2 = "1.1.0" vistaguide = "lv0.24.2.6" wearable = "2.9.0" webkit = "1.12.1" @@ -68,7 +66,6 @@ androidx-coordinatorlayout = { module = "androidx.coordinatorlayout:coordinatorl androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" } androidx-documentfile = { module = "androidx.documentfile:documentfile", version.ref = "documentfile" } androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" } -#androidx-gridlayout = { module = "androidx.gridlayout:gridlayout", version.ref = "gridlayout" } androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } androidx-material3-android = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" } androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3Exoplayer" } @@ -80,7 +77,6 @@ androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version androidx-preference-ktx = { module = "androidx.preference:preference-ktx", version.ref = "preferenceKtx" } androidx-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "uiTooling" } androidx-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "uiToolingPreview" } -#androidx-viewpager2 = { module = "androidx.viewpager2:viewpager2", version.ref = "viewpager2" } androidx-webkit = { module = "androidx.webkit:webkit", version.ref = "webkit" } androidx-window = { module = "androidx.window:window", version.ref = "window" } androidx-work-runtime = { module = "androidx.work:work-runtime", version.ref = "workRuntime" }