diff --git a/CHANGELOG.md b/CHANGELOG.md index 18531fc5..9bfb2a7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,26 @@ # Changelog ## Pending release -* i18n: Add Romanian translation (including locale_config this time!) by @DevMatei in https://github.com/eddyizm/tempus/pull/357 + +## What's Changed +## [4.9.0](https://github.com/eddyizm/tempo/releases/tag/v4.9.0) (2026-01-24) +* chore: i18n: Add Romanian translation (including locale_config this time!) by @DevMatei in https://github.com/eddyizm/tempus/pull/357 * French localization update by @benoit-smith in https://github.com/eddyizm/tempus/pull/356 * chore(i18n): Update Spanish translation by @jaime-grj in https://github.com/eddyizm/tempus/pull/364 -* chore: updated readme and added known issues for airsonic work around by @eddyizm in https://github.com/eddyizm/tempus/pull/366 +* docs: updated readme and added known issues for airsonic work around by @eddyizm in https://github.com/eddyizm/tempus/pull/366 * fix: toast for made for you click indication by @eddyizm in https://github.com/eddyizm/tempus/pull/365 * fix: sort playlist view by @eddyizm in https://github.com/eddyizm/tempus/pull/368 * feat: sort preference for playlists by @eddyizm in https://github.com/eddyizm/tempus/pull/370 +* fix: use existing future when adding tracks, dialed random album tracks off in instant mix by @eddyizm in https://github.com/eddyizm/tempus/pull/373 +* chore(i18n): Update Polish translation by @skajmer in https://github.com/eddyizm/tempus/pull/374 +* fix: Check for OpenSubsonic extensions also with password authentication by @pgrit in https://github.com/eddyizm/tempus/pull/375 +* feat: Implement duration and seeking for transcodes by @drakeerv in https://github.com/eddyizm/tempus/pull/358 +* feat: Playback speed controls for music by @pgrit in https://github.com/eddyizm/tempus/pull/376 + +## New Contributors +* @pgrit made their first contribution in https://github.com/eddyizm/tempus/pull/375 + +**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.6.4...v4.9.0 ## What's Changed ## [4.6.4](https://github.com/eddyizm/tempo/releases/tag/v4.6.4) (2026-01-13) diff --git a/app/build.gradle b/app/build.gradle index 5452cb03..8babe78c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,8 +10,8 @@ android { minSdkVersion 24 targetSdk 35 - versionCode 13 - versionName '4.6.4' + versionCode 14 + versionName '4.9.0' testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' javaCompileOptions { diff --git a/app/src/main/java/com/cappielloantonio/tempo/service/BaseMediaService.kt b/app/src/main/java/com/cappielloantonio/tempo/service/BaseMediaService.kt index 6f5fc52d..c8cb846f 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/service/BaseMediaService.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/service/BaseMediaService.kt @@ -4,6 +4,7 @@ import android.annotation.SuppressLint import android.app.PendingIntent.FLAG_IMMUTABLE import android.app.PendingIntent.FLAG_UPDATE_CURRENT import android.app.TaskStackBuilder +import android.content.ComponentName import android.content.Context import android.content.Intent import android.net.ConnectivityManager @@ -138,8 +139,13 @@ open class BaseMediaService : MediaLibraryService() { if (item.mediaMetadata.extras != null) MediaManager.scrobble(item, false) - if (player.nextMediaItemIndex == C.INDEX_UNSET) - MediaManager.continuousPlay(player.currentMediaItem) + if (player.nextMediaItemIndex == C.INDEX_UNSET) { + val browserFuture = MediaBrowser.Builder( + this@BaseMediaService, + SessionToken(this@BaseMediaService, ComponentName(this@BaseMediaService, this@BaseMediaService::class.java)) + ).buildAsync() + MediaManager.continuousPlay(player.currentMediaItem, browserFuture) + } } if (player is ExoPlayer) { diff --git a/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java b/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java index ba2cee60..44c5f974 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java +++ b/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java @@ -444,25 +444,20 @@ public class MediaManager { } @OptIn(markerClass = UnstableApi.class) - public static void continuousPlay(MediaItem mediaItem) { + public static void continuousPlay(MediaItem mediaItem, ListenableFuture existingBrowserFuture) { if (mediaItem != null && Preferences.isContinuousPlayEnabled() && Preferences.isInstantMixUsable()) { Preferences.setLastInstantMix(); - LiveData> instantMix = getSongRepository().getContinuousMix(mediaItem.mediaId,25); + LiveData> instantMix = getSongRepository().getContinuousMix(mediaItem.mediaId, 25); instantMix.observeForever(new Observer>() { @Override public void onChanged(List media) { - if (media != null) { - Log.e(TAG, "continuous play"); - ListenableFuture mediaBrowserListenableFuture = new MediaBrowser.Builder( - App.getContext(), - new SessionToken(App.getContext(), new ComponentName(App.getContext(), MediaService.class)) - ).buildAsync(); - - enqueue(mediaBrowserListenableFuture, media, true); + if (media != null && existingBrowserFuture != null) { + Log.d(TAG, "Continuous play: adding " + media.size() + " tracks"); + enqueue(existingBrowserFuture, media, false); } - + instantMix.removeObserver(this); } }); diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/activity/MainActivity.java b/app/src/main/java/com/cappielloantonio/tempo/ui/activity/MainActivity.java index 156805a2..1a75b242 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/activity/MainActivity.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/activity/MainActivity.java @@ -354,7 +354,7 @@ public class MainActivity extends BaseActivity { // TODO Enter all settings to be reset Preferences.setOpenSubsonic(false); - Preferences.setPlaybackSpeed(Constants.MEDIA_PLAYBACK_SPEED_100); + Preferences.setPlaybackSpeed(1.0f); Preferences.setSkipSilenceMode(false); Preferences.setDataSavingMode(false); Preferences.setStarredSyncEnabled(false); @@ -384,7 +384,7 @@ public class MainActivity extends BaseActivity { } private void pingServer() { - if (Preferences.getToken() == null) return; + if (Preferences.getToken() == null && Preferences.getPassword() == null) return; if (Preferences.isInUseServerAddressLocal()) { mainViewModel.ping().observe(this, subsonicResponse -> { @@ -428,7 +428,7 @@ public class MainActivity extends BaseActivity { } private void getOpenSubsonicExtensions() { - if (Preferences.getToken() != null) { + if (Preferences.getToken() != null || Preferences.getPassword() != null) { mainViewModel.getOpenSubsonicExtensions().observe(this, openSubsonicExtensions -> { if (openSubsonicExtensions != null) { Preferences.setOpenSubsonicExtensions(openSubsonicExtensions); diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerControllerFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerControllerFragment.java index e3155b56..f45b5061 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerControllerFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerControllerFragment.java @@ -413,10 +413,10 @@ public class PlayerControllerFragment extends Fragment { bind.getRoot().setShowNextButton(true); bind.getRoot().setShowFastForwardButton(false); bind.getRoot().setRepeatToggleModes(RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL | RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE); - bind.getRoot().findViewById(R.id.player_playback_speed_button).setVisibility(View.GONE); + bind.getRoot().findViewById(R.id.player_playback_speed_button).setVisibility(View.VISIBLE); bind.getRoot().findViewById(R.id.player_skip_silence_toggle_button).setVisibility(View.GONE); bind.getRoot().findViewById(R.id.button_favorite).setVisibility(View.VISIBLE); - resetPlaybackParameters(mediaBrowser); + setPlaybackParameters(mediaBrowser); break; } } @@ -524,31 +524,11 @@ public class PlayerControllerFragment extends Fragment { playbackSpeedButton.setOnClickListener(view -> { float currentSpeed = Preferences.getPlaybackSpeed(); - if (currentSpeed == Constants.MEDIA_PLAYBACK_SPEED_080) { - mediaBrowser.setPlaybackParameters(new PlaybackParameters(Constants.MEDIA_PLAYBACK_SPEED_100)); - playbackSpeedButton.setText(getString(R.string.player_playback_speed, Constants.MEDIA_PLAYBACK_SPEED_100)); - Preferences.setPlaybackSpeed(Constants.MEDIA_PLAYBACK_SPEED_100); - } else if (currentSpeed == Constants.MEDIA_PLAYBACK_SPEED_100) { - mediaBrowser.setPlaybackParameters(new PlaybackParameters(Constants.MEDIA_PLAYBACK_SPEED_125)); - playbackSpeedButton.setText(getString(R.string.player_playback_speed, Constants.MEDIA_PLAYBACK_SPEED_125)); - Preferences.setPlaybackSpeed(Constants.MEDIA_PLAYBACK_SPEED_125); - } else if (currentSpeed == Constants.MEDIA_PLAYBACK_SPEED_125) { - mediaBrowser.setPlaybackParameters(new PlaybackParameters(Constants.MEDIA_PLAYBACK_SPEED_150)); - playbackSpeedButton.setText(getString(R.string.player_playback_speed, Constants.MEDIA_PLAYBACK_SPEED_150)); - Preferences.setPlaybackSpeed(Constants.MEDIA_PLAYBACK_SPEED_150); - } else if (currentSpeed == Constants.MEDIA_PLAYBACK_SPEED_150) { - mediaBrowser.setPlaybackParameters(new PlaybackParameters(Constants.MEDIA_PLAYBACK_SPEED_175)); - playbackSpeedButton.setText(getString(R.string.player_playback_speed, Constants.MEDIA_PLAYBACK_SPEED_175)); - Preferences.setPlaybackSpeed(Constants.MEDIA_PLAYBACK_SPEED_175); - } else if (currentSpeed == Constants.MEDIA_PLAYBACK_SPEED_175) { - mediaBrowser.setPlaybackParameters(new PlaybackParameters(Constants.MEDIA_PLAYBACK_SPEED_200)); - playbackSpeedButton.setText(getString(R.string.player_playback_speed, Constants.MEDIA_PLAYBACK_SPEED_200)); - Preferences.setPlaybackSpeed(Constants.MEDIA_PLAYBACK_SPEED_200); - } else if (currentSpeed == Constants.MEDIA_PLAYBACK_SPEED_200) { - mediaBrowser.setPlaybackParameters(new PlaybackParameters(Constants.MEDIA_PLAYBACK_SPEED_080)); - playbackSpeedButton.setText(getString(R.string.player_playback_speed, Constants.MEDIA_PLAYBACK_SPEED_080)); - Preferences.setPlaybackSpeed(Constants.MEDIA_PLAYBACK_SPEED_080); - } + currentSpeed += 0.25f; + if (currentSpeed > 2.0f) currentSpeed = 0.5f; + mediaBrowser.setPlaybackParameters(new PlaybackParameters(currentSpeed)); + playbackSpeedButton.setText(getString(R.string.player_playback_speed, currentSpeed)); + Preferences.setPlaybackSpeed(currentSpeed); }); skipSilenceToggleButton.setOnClickListener(view -> { @@ -600,7 +580,7 @@ public class PlayerControllerFragment extends Fragment { } private void resetPlaybackParameters(MediaBrowser mediaBrowser) { - mediaBrowser.setPlaybackParameters(new PlaybackParameters(Constants.MEDIA_PLAYBACK_SPEED_100)); + mediaBrowser.setPlaybackParameters(new PlaybackParameters(1.0f)); // TODO Resettare lo skip del silenzio } diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/Constants.kt b/app/src/main/java/com/cappielloantonio/tempo/util/Constants.kt index 2ae3dbb0..7d2224ed 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/util/Constants.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/util/Constants.kt @@ -61,13 +61,6 @@ object Constants { const val MEDIA_TYPE_VIDEO = "video" const val MEDIA_TYPE_RADIO = "radio" - const val MEDIA_PLAYBACK_SPEED_080 = 0.8f - const val MEDIA_PLAYBACK_SPEED_100 = 1.0f - const val MEDIA_PLAYBACK_SPEED_125 = 1.25f - const val MEDIA_PLAYBACK_SPEED_150 = 1.50f - const val MEDIA_PLAYBACK_SPEED_175 = 1.75f - const val MEDIA_PLAYBACK_SPEED_200 = 2.0f - const val MEDIA_RECENTLY_PLAYED = "MEDIA_RECENTLY_PLAYED" const val MEDIA_MOST_PLAYED = "MEDIA_MOST_PLAYED" const val MEDIA_RECENTLY_ADDED = "MEDIA_RECENTLY_ADDED" diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/DynamicMediaSourceFactory.kt b/app/src/main/java/com/cappielloantonio/tempo/util/DynamicMediaSourceFactory.kt index 31dc172a..519afe91 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/util/DynamicMediaSourceFactory.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/util/DynamicMediaSourceFactory.kt @@ -46,8 +46,17 @@ class DynamicMediaSourceFactory( else -> { val extractorsFactory: ExtractorsFactory = DefaultExtractorsFactory() - ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory) - .createMediaSource(mediaItem) + val progressiveFactory = ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory) + + val uri = mediaItem.localConfiguration?.uri + val isTranscoding = uri?.getQueryParameter("maxBitRate") != null || + (uri?.getQueryParameter("format") != null && uri?.getQueryParameter("format") != "raw") + + if (isTranscoding && OpenSubsonicExtensionsUtil.isTranscodeOffsetExtensionAvailable()) { + TranscodingMediaSource(mediaItem, dataSourceFactory, progressiveFactory) + } else { + progressiveFactory.createMediaSource(mediaItem) + } } } } diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/MusicUtil.java b/app/src/main/java/com/cappielloantonio/tempo/util/MusicUtil.java index cd2c7a36..c1656899 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/util/MusicUtil.java +++ b/app/src/main/java/com/cappielloantonio/tempo/util/MusicUtil.java @@ -31,7 +31,7 @@ public class MusicUtil { private static final Pattern BITRATE_PATTERN = Pattern.compile("&maxBitRate=\\d+"); private static final Pattern FORMAT_PATTERN = Pattern.compile("&format=\\w+"); - public static Uri getStreamUri(String id) { + public static Uri getStreamUri(String id, int timeOffset) { Map params = App.getSubsonicClientInstance(false).getParams(); StringBuilder uri = new StringBuilder(); @@ -58,6 +58,8 @@ public class MusicUtil { uri.append("&format=").append(getTranscodingFormatPreference()); if (Preferences.askForEstimateContentLength()) uri.append("&estimateContentLength=true"); + if (timeOffset > 0) + uri.append("&timeOffset=").append(timeOffset); uri.append("&id=").append(id); @@ -66,6 +68,10 @@ public class MusicUtil { return Uri.parse(uri.toString()); } + public static Uri getStreamUri(String id) { + return getStreamUri(id, 0); + } + public static Uri updateStreamUri(Uri uri) { String s = uri.toString(); Matcher m1 = BITRATE_PATTERN.matcher(s); diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/TranscodingMediaSource.kt b/app/src/main/java/com/cappielloantonio/tempo/util/TranscodingMediaSource.kt new file mode 100644 index 00000000..14270ff9 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/util/TranscodingMediaSource.kt @@ -0,0 +1,322 @@ +package com.cappielloantonio.tempo.util + +import androidx.annotation.OptIn +import androidx.media3.common.C +import androidx.media3.common.MediaItem +import androidx.media3.common.Timeline +import androidx.media3.common.util.UnstableApi +import androidx.media3.common.util.Util +import androidx.media3.datasource.DataSource +import androidx.media3.datasource.TransferListener +import androidx.media3.decoder.DecoderInputBuffer +import androidx.media3.exoplayer.FormatHolder +import androidx.media3.exoplayer.LoadingInfo +import androidx.media3.exoplayer.SeekParameters +import androidx.media3.exoplayer.source.CompositeMediaSource +import androidx.media3.exoplayer.source.ForwardingTimeline +import androidx.media3.exoplayer.source.MediaPeriod +import androidx.media3.exoplayer.source.MediaSource +import androidx.media3.exoplayer.source.ProgressiveMediaSource +import androidx.media3.exoplayer.source.SampleStream +import androidx.media3.exoplayer.trackselection.ExoTrackSelection +import androidx.media3.exoplayer.upstream.Allocator + +@OptIn(UnstableApi::class) +class TranscodingMediaSource( + private val mediaItem: MediaItem, + private val dataSourceFactory: DataSource.Factory, + private val progressiveMediaSourceFactory: ProgressiveMediaSource.Factory +) : CompositeMediaSource() { + + private var durationUs: Long = C.TIME_UNSET + private var currentSource: MediaSource? = null + + init { + val extras = mediaItem.mediaMetadata.extras + if (extras != null && extras.containsKey("duration")) { + val seconds = extras.getInt("duration") + if (seconds > 0) { + durationUs = Util.msToUs(seconds * 1000L) + } + } + } + + override fun getMediaItem() = mediaItem + + override fun prepareSourceInternal(mediaTransferListener: TransferListener?) { + super.prepareSourceInternal(mediaTransferListener) + val initialSource = progressiveMediaSourceFactory.createMediaSource(mediaItem) + currentSource = initialSource + prepareChildSource(null, initialSource) + } + + override fun onChildSourceInfoRefreshed( + childSourceId: Void?, + mediaSource: MediaSource, + newTimeline: Timeline + ) { + val timeline = + if (durationUs != C.TIME_UNSET) { + DurationOverridingTimeline(newTimeline, durationUs) + } else { + newTimeline + } + refreshSourceInfo(timeline) + } + + override fun createPeriod( + id: MediaSource.MediaPeriodId, + allocator: Allocator, + startPositionUs: Long + ): MediaPeriod { + val source = currentSource ?: throw IllegalStateException("Source not ready") + val childPeriod = source.createPeriod(id, allocator, startPositionUs) + return TranscodingMediaPeriod(childPeriod, source, id, allocator) + } + + override fun releasePeriod(mediaPeriod: MediaPeriod) { + val transcodingPeriod = mediaPeriod as TranscodingMediaPeriod + transcodingPeriod.release() + + if (transcodingPeriod.currentOffsetUs > 0) { + releaseChildSource(null) + val initialSource = progressiveMediaSourceFactory.createMediaSource(mediaItem) + currentSource = initialSource + prepareChildSource(null, initialSource) + } + } + + override fun getMediaPeriodIdForChildMediaPeriodId( + childSourceId: Void?, + mediaPeriodId: MediaSource.MediaPeriodId + ) = mediaPeriodId + + private inner class TranscodingMediaPeriod( + private var currentPeriod: MediaPeriod, + private var source: MediaSource, + private val id: MediaSource.MediaPeriodId, + private val allocator: Allocator + ) : MediaPeriod, MediaPeriod.Callback { + + private var localCallback: MediaPeriod.Callback? = null + internal var currentOffsetUs: Long = 0 + private var isReloading = false + + private var lastSelections: Array? = null + private var lastMayRetainStreamFlags: BooleanArray? = null + private var activeWrappers: Array = emptyArray() + + fun release() { + source.releasePeriod(currentPeriod) + } + + override fun prepare(callback: MediaPeriod.Callback, positionUs: Long) { + localCallback = callback + currentPeriod.prepare(this, positionUs) + } + + override fun maybeThrowPrepareError() { + if (!isReloading) currentPeriod.maybeThrowPrepareError() + } + + override fun getTrackGroups() = currentPeriod.trackGroups + + override fun getStreamKeys(trackSelections: MutableList) = + currentPeriod.getStreamKeys(trackSelections) + + override fun selectTracks( + selections: Array, + mayRetainStreamFlags: BooleanArray, + streams: Array, + streamResetFlags: BooleanArray, + positionUs: Long + ): Long { + lastSelections = selections + lastMayRetainStreamFlags = mayRetainStreamFlags + + val childStreams = arrayOfNulls(streams.size) + streams.forEachIndexed { i, stream -> + childStreams[i] = (stream as? OffsetSampleStream)?.childStream + } + + val startPos = + currentPeriod.selectTracks( + selections, + mayRetainStreamFlags, + childStreams, + streamResetFlags, + positionUs - currentOffsetUs + ) + + val newWrappers = arrayOfNulls(streams.size) + for (i in streams.indices) { + val child = childStreams[i] + if (child == null) { + streams[i] = null + } else { + val existingWrapper = streams[i] as? OffsetSampleStream + if (existingWrapper != null && existingWrapper.childStream === child) { + newWrappers[i] = existingWrapper + } else { + val wrapper = OffsetSampleStream(child) + newWrappers[i] = wrapper + streams[i] = wrapper + } + } + } + activeWrappers = newWrappers + + return startPos + currentOffsetUs + } + + override fun discardBuffer(positionUs: Long, toKeyframe: Boolean) { + if (!isReloading) { + currentPeriod.discardBuffer(positionUs - currentOffsetUs, toKeyframe) + } + } + + override fun readDiscontinuity(): Long { + if (isReloading) return C.TIME_UNSET + val discontinuity = currentPeriod.readDiscontinuity() + return if (discontinuity == C.TIME_UNSET) C.TIME_UNSET + else discontinuity + currentOffsetUs + } + + override fun seekToUs(positionUs: Long): Long { + if (positionUs == 0L && currentOffsetUs == 0L) { + return currentPeriod.seekToUs(positionUs) + } + + reloadSource(positionUs) + return positionUs + } + + override fun getAdjustedSeekPositionUs(positionUs: Long, seekParameters: SeekParameters) = + positionUs + + override fun getBufferedPositionUs(): Long { + if (isReloading) return currentOffsetUs + val buffered = currentPeriod.bufferedPositionUs + if (buffered == C.TIME_END_OF_SOURCE) return C.TIME_END_OF_SOURCE + return if (buffered == C.TIME_UNSET) C.TIME_UNSET else buffered + currentOffsetUs + } + + override fun getNextLoadPositionUs(): Long { + if (isReloading) return C.TIME_UNSET + val next = currentPeriod.nextLoadPositionUs + if (next == C.TIME_END_OF_SOURCE) return C.TIME_END_OF_SOURCE + return if (next == C.TIME_UNSET) C.TIME_UNSET else next + currentOffsetUs + } + + override fun reevaluateBuffer(positionUs: Long) { + if (!isReloading) currentPeriod.reevaluateBuffer(positionUs - currentOffsetUs) + } + + override fun continueLoading(isLoading: LoadingInfo): Boolean { + if (isReloading) return false + val builder = isLoading.buildUpon() + builder.setPlaybackPositionUs(isLoading.playbackPositionUs - currentOffsetUs) + return currentPeriod.continueLoading(builder.build()) + } + + override fun isLoading() = isReloading || currentPeriod.isLoading + + override fun onPrepared(mediaPeriod: MediaPeriod) { + if (isReloading && mediaPeriod == currentPeriod) { + isReloading = false + restoreTracks() + localCallback?.onContinueLoadingRequested(this) + } else { + localCallback?.onPrepared(this) + } + } + + override fun onContinueLoadingRequested(source: MediaPeriod) { + if (!isReloading) localCallback?.onContinueLoadingRequested(this) + } + + private fun reloadSource(positionUs: Long) { + isReloading = true + currentOffsetUs = positionUs + + activeWrappers.forEach { it?.childStream = null } + + source.releasePeriod(currentPeriod) + releaseChildSource(null) + + val seconds = Util.usToMs(positionUs) / 1000 + val newUri = MusicUtil.getStreamUri(mediaItem.mediaId, seconds.toInt()) + val newMediaItem = mediaItem.buildUpon().setUri(newUri).build() + + val newSource = progressiveMediaSourceFactory.createMediaSource(newMediaItem) + + source = newSource + currentSource = newSource + prepareChildSource(null, newSource) + + val newPeriod = newSource.createPeriod(id, allocator, 0) + currentPeriod = newPeriod + newPeriod.prepare(this, 0) + } + + private fun restoreTracks() { + val selections = lastSelections ?: return + val flags = lastMayRetainStreamFlags ?: return + + val childStreams = arrayOfNulls(activeWrappers.size) + val streamResetFlags = BooleanArray(activeWrappers.size) + + currentPeriod.selectTracks(selections, flags, childStreams, streamResetFlags, 0) + + for (i in activeWrappers.indices) { + activeWrappers[i]?.childStream = childStreams[i] + } + } + + private inner class OffsetSampleStream(var childStream: SampleStream?) : SampleStream { + override fun isReady() = childStream?.isReady ?: false + override fun maybeThrowError() { + childStream?.maybeThrowError() + } + + override fun readData( + formatHolder: FormatHolder, + buffer: DecoderInputBuffer, + readFlags: Int + ): Int { + val stream = childStream ?: return C.RESULT_NOTHING_READ + val result = stream.readData(formatHolder, buffer, readFlags) + if (result == C.RESULT_BUFFER_READ && !buffer.isEndOfStream) { + buffer.timeUs += currentOffsetUs + } + return result + } + + override fun skipData(positionUs: Long) = + childStream?.skipData(positionUs - currentOffsetUs) ?: 0 + } + } + + private class DurationOverridingTimeline(timeline: Timeline, private val durationUs: Long) : + ForwardingTimeline(timeline) { + + override fun getWindow( + windowIndex: Int, + window: Window, + defaultPositionProjectionUs: Long + ): Window { + super.getWindow(windowIndex, window, defaultPositionProjectionUs) + window.durationUs = durationUs + window.isSeekable = true + window.isDynamic = false + window.liveConfiguration = null + return window + } + + override fun getPeriod(periodIndex: Int, period: Period, setIds: Boolean): Period { + super.getPeriod(periodIndex, period, setIds) + period.durationUs = durationUs + return period + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/HomeViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/HomeViewModel.java index 42ff0b25..3cd90d0d 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/HomeViewModel.java +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/HomeViewModel.java @@ -101,7 +101,7 @@ public class HomeViewModel extends AndroidViewModel { } public LiveData> getRandomShuffleSample() { - return songRepository.getRandomSample(1000, null, null); + return songRepository.getRandomSample(100, null, null); } public LiveData> getChronologySample(LifecycleOwner owner) { diff --git a/app/src/main/res/layout/inner_fragment_player_controller_layout.xml b/app/src/main/res/layout/inner_fragment_player_controller_layout.xml index 29747587..8fab7c84 100644 --- a/app/src/main/res/layout/inner_fragment_player_controller_layout.xml +++ b/app/src/main/res/layout/inner_fragment_player_controller_layout.xml @@ -16,6 +16,23 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> +