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
[](https://f-droid.org/packages/ac.mdiq.podcini.R/)
[](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" }