mirror of
https://github.com/antebudimir/tempus.git
synced 2026-04-15 16:27:26 +00:00
Merge branch 'development'
This commit is contained in:
commit
269066e036
15 changed files with 413 additions and 78 deletions
17
CHANGELOG.md
17
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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -444,25 +444,20 @@ public class MediaManager {
|
|||
}
|
||||
|
||||
@OptIn(markerClass = UnstableApi.class)
|
||||
public static void continuousPlay(MediaItem mediaItem) {
|
||||
public static void continuousPlay(MediaItem mediaItem, ListenableFuture<MediaBrowser> existingBrowserFuture) {
|
||||
if (mediaItem != null && Preferences.isContinuousPlayEnabled() && Preferences.isInstantMixUsable()) {
|
||||
Preferences.setLastInstantMix();
|
||||
|
||||
LiveData<List<Child>> instantMix = getSongRepository().getContinuousMix(mediaItem.mediaId,25);
|
||||
LiveData<List<Child>> instantMix = getSongRepository().getContinuousMix(mediaItem.mediaId, 25);
|
||||
|
||||
instantMix.observeForever(new Observer<List<Child>>() {
|
||||
@Override
|
||||
public void onChanged(List<Child> media) {
|
||||
if (media != null) {
|
||||
Log.e(TAG, "continuous play");
|
||||
ListenableFuture<MediaBrowser> 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);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String, String> 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);
|
||||
|
|
|
|||
|
|
@ -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<Void>() {
|
||||
|
||||
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<out ExoTrackSelection?>? = null
|
||||
private var lastMayRetainStreamFlags: BooleanArray? = null
|
||||
private var activeWrappers: Array<OffsetSampleStream?> = 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<ExoTrackSelection>) =
|
||||
currentPeriod.getStreamKeys(trackSelections)
|
||||
|
||||
override fun selectTracks(
|
||||
selections: Array<out ExoTrackSelection?>,
|
||||
mayRetainStreamFlags: BooleanArray,
|
||||
streams: Array<SampleStream?>,
|
||||
streamResetFlags: BooleanArray,
|
||||
positionUs: Long
|
||||
): Long {
|
||||
lastSelections = selections
|
||||
lastMayRetainStreamFlags = mayRetainStreamFlags
|
||||
|
||||
val childStreams = arrayOfNulls<SampleStream>(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<OffsetSampleStream>(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<SampleStream>(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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -101,7 +101,7 @@ public class HomeViewModel extends AndroidViewModel {
|
|||
}
|
||||
|
||||
public LiveData<List<Child>> getRandomShuffleSample() {
|
||||
return songRepository.getRandomSample(1000, null, null);
|
||||
return songRepository.getRandomSample(100, null, null);
|
||||
}
|
||||
|
||||
public LiveData<List<Chronology>> getChronologySample(LifecycleOwner owner) {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,23 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<Button
|
||||
android:id="@+id/player_playback_speed_button"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginLeft="0dp"
|
||||
android:layout_marginTop="0dp"
|
||||
android:insetLeft="0dp"
|
||||
android:insetTop="0dp"
|
||||
android:insetRight="0dp"
|
||||
android:insetBottom="0dp"
|
||||
app:cornerRadius="30dp"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:tint="?attr/colorOnPrimaryContainer" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/player_media_extension"
|
||||
style="@style/Widget.Material3.Chip.Suggestion"
|
||||
|
|
@ -253,23 +270,6 @@
|
|||
app:layout_constraintStart_toEndOf="@+id/placeholder_view_middle_right"
|
||||
app:layout_constraintTop_toTopOf="@+id/placeholder_view_middle_right" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/player_playback_speed_button"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:layout_marginStart="24dp"
|
||||
android:insetLeft="0dp"
|
||||
android:insetTop="0dp"
|
||||
android:insetRight="0dp"
|
||||
android:insetBottom="0dp"
|
||||
app:cornerRadius="30dp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/placeholder_view_middle_left"
|
||||
app:layout_constraintEnd_toStartOf="@+id/placeholder_view_middle_left"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/placeholder_view_middle_left"
|
||||
app:tint="?attr/colorOnPrimaryContainer" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/exo_shuffle"
|
||||
android:layout_width="32dp"
|
||||
|
|
|
|||
|
|
@ -239,7 +239,16 @@
|
|||
<item>4</item>
|
||||
<item>8</item>
|
||||
</string-array>
|
||||
|
||||
|
||||
<string-array name="playlist_sort_option_titles">
|
||||
<item>Po nazwie</item>
|
||||
<item>Losowo</item>
|
||||
</string-array>
|
||||
<string-array name="playlist_sort_option_values">
|
||||
<item>ORDER_BY_NAME</item>
|
||||
<item>ORDER_BY_RANDOM</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="skip_min_star_rating_titles">
|
||||
<item>Minimum 0 gwiazdek</item>
|
||||
<item>Minimum 1 gwiazdka</item>
|
||||
|
|
@ -254,4 +263,4 @@
|
|||
<item>3</item>
|
||||
<item>4</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@
|
|||
<string name="artist_list_page_downloaded">Pobrani wykonawcy</string>
|
||||
<string name="artist_list_page_starred">Wykonawcy oznaczeni gwiazdką</string>
|
||||
<string name="artist_list_page_title">Wykonawcy</string>
|
||||
<string name="artist_no_artist_info_toast">Brak dodatkowych informacji o wykonawcy</string>
|
||||
<string name="artist_page_radio_button">Radio</string>
|
||||
<string name="artist_page_shuffle_button">Odtwarzanie losowe</string>
|
||||
<string name="artist_page_switch_layout_button">Zmień układ</string>
|
||||
|
|
@ -350,6 +351,7 @@
|
|||
<string name="settings_music_directory_summary">Jeżeli włączone, widoczna będzie sekcja z folderami z muzyką. Weź pod uwagę że żeby funkcja nawigacji po folderach działała poprawnie, serwer musi wspierać tę funkcję.</string>
|
||||
<string name="settings_podcast">Pokazuj podcasty</string>
|
||||
<string name="settings_podcast_summary">Jeżeli włączone, widoczna będzie sekcja z podcastami. Zrestartuj aplikację aby, zmiany przyniosły pełny efekt.</string>
|
||||
<string name="settings_playlist_sort">Sortowanie playlist</string>
|
||||
<string name="settings_audio_quality">Pokaż jakość audio</string>
|
||||
<string name="settings_audio_quality_summary">Bitrate i format audio będzie pokazywany dla każdego utworu.</string>
|
||||
<string name="settings_song_rating">Pokaż ocenę piosenek w gwiazdkach</string>
|
||||
|
|
|
|||
0
fastlane/metadata/android/en-US/changelogs/14.txt
Normal file
0
fastlane/metadata/android/en-US/changelogs/14.txt
Normal file
Loading…
Add table
Add a link
Reference in a new issue