mirror of
https://github.com/XilinJia/Podcini.git
synced 2025-01-27 20:29:20 +01:00
6.14.2 commit
This commit is contained in:
parent
a213284e40
commit
737577a15a
@ -11,6 +11,7 @@ An open source podcast instrument, attuned to Puccini ![Puccini](./images/Puccin
|
||||
[<img src="./images/external/getItf-droid.png" alt="F-Droid" height="50">](https://f-droid.org/packages/ac.mdiq.podcini.R/)
|
||||
[<img src="./images/external/amazon.png" alt="Amazon" height="40">](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](<https://github.com/AntennaPod/AntennaPod>) as of Feb 5 2024.
|
||||
|
||||
|
@ -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 = ""
|
||||
|
@ -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"
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
@ -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<String, String>()
|
||||
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<VideoStream>?, videoOnlyStreams: List<VideoStream>?, ascendingOrder: Boolean,
|
||||
protected fun getSortedStreamVideosList(videoStreams: List<VideoStream>?, videoOnlyStreams: List<VideoStream>?, ascendingOrder: Boolean,
|
||||
preferVideoOnlyStreams: Boolean): List<VideoStream> {
|
||||
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<AudioStream>?): List<AudioStream> {
|
||||
protected fun getFilteredAudioStreams(audioStreams: List<AudioStream>?): List<AudioStream> {
|
||||
if (audioStreams == null) return listOf()
|
||||
val collectedStreams = mutableSetOf<AudioStream>()
|
||||
for (stream in audioStreams) {
|
||||
|
@ -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())
|
||||
|
||||
|
@ -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
|
||||
|
@ -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<Chapter>) {
|
||||
if (episode != null) {
|
||||
episode!!.chapters.clear()
|
||||
for (c in chapters) {
|
||||
c.episode = episode
|
||||
}
|
||||
for (c in chapters) c.episode = episode
|
||||
episode!!.chapters.addAll(chapters)
|
||||
}
|
||||
}
|
||||
|
@ -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<AudioType>().firstOrNull { it.code == code } ?: SPEECH
|
||||
}
|
||||
fun fromTag(tag: String): AudioType {
|
||||
return enumValues<AudioType>().firstOrNull { it.tag == tag } ?: SPEECH
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class AVQuality(val code: Int, val tag: String) {
|
||||
GLOBAL(0, "Global"),
|
||||
LOW(1, "Low"),
|
||||
|
@ -217,40 +217,3 @@ fun NonlazyGrid(columns: Int, itemCount: Int, modifier: Modifier = Modifier, con
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AutoCompleteTextField(suggestions: List<String>) {
|
||||
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))
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
})
|
||||
@ -632,6 +656,31 @@ class FeedSettingsFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
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.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")
|
||||
val type = FeedPreferences.AudioType.fromTag(selected)
|
||||
feed = upsertBlk(feed!!) { it.preferences?.audioType = type.code }
|
||||
onDismissRequest()
|
||||
}
|
||||
)
|
||||
Text(option.tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SetAudioQuality(selectedOption: String, onDismissRequest: () -> Unit) {
|
||||
var selected by remember {mutableStateOf(selectedOption)}
|
||||
@ -644,26 +693,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?.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.AVQuality.fromTag(selected)
|
||||
feed = upsertBlk(feed!!) { it.preferences?.audioQuality = type.code }
|
||||
onDismissRequest()
|
||||
})
|
||||
Text(option.tag)
|
||||
}
|
||||
}
|
||||
@ -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<String>() }
|
||||
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) {
|
||||
|
@ -538,6 +538,8 @@
|
||||
<string name="pref_feed_skip">Auto skip</string>
|
||||
<string name="pref_feed_skip_sum">Skip introductions and ending credits.</string>
|
||||
<string name="associated">Associated</string>
|
||||
<string name="pref_feed_audio_type">Audio type</string>
|
||||
<string name="pref_feed_audio_type_sum">Either Speech, Music or Movie for improved processing.</string>
|
||||
<string name="pref_feed_audio_quality">Audio quality</string>
|
||||
<string name="pref_feed_audio_quality_sum">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.</string>
|
||||
<string name="pref_feed_video_quality">Video quality</string>
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
5
fastlane/metadata/android/en-US/changelogs/3020301.txt
Normal file
5
fastlane/metadata/android/en-US/changelogs/3020301.txt
Normal file
@ -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
|
@ -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" }
|
||||
|
Loading…
x
Reference in New Issue
Block a user