mirror of
https://github.com/antebudimir/tempus.git
synced 2026-04-15 16:27:26 +00:00
feat: Implement duration and seeking for transcodes (#358)
Co-authored-by: eddyizm <eddyizm@gmail.com>
This commit is contained in:
parent
e5b7756f96
commit
bde34d3df0
3 changed files with 340 additions and 3 deletions
|
|
@ -46,8 +46,17 @@ class DynamicMediaSourceFactory(
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
val extractorsFactory: ExtractorsFactory = DefaultExtractorsFactory()
|
val extractorsFactory: ExtractorsFactory = DefaultExtractorsFactory()
|
||||||
ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory)
|
val progressiveFactory = ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory)
|
||||||
.createMediaSource(mediaItem)
|
|
||||||
|
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 BITRATE_PATTERN = Pattern.compile("&maxBitRate=\\d+");
|
||||||
private static final Pattern FORMAT_PATTERN = Pattern.compile("&format=\\w+");
|
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();
|
Map<String, String> params = App.getSubsonicClientInstance(false).getParams();
|
||||||
|
|
||||||
StringBuilder uri = new StringBuilder();
|
StringBuilder uri = new StringBuilder();
|
||||||
|
|
@ -58,6 +58,8 @@ public class MusicUtil {
|
||||||
uri.append("&format=").append(getTranscodingFormatPreference());
|
uri.append("&format=").append(getTranscodingFormatPreference());
|
||||||
if (Preferences.askForEstimateContentLength())
|
if (Preferences.askForEstimateContentLength())
|
||||||
uri.append("&estimateContentLength=true");
|
uri.append("&estimateContentLength=true");
|
||||||
|
if (timeOffset > 0)
|
||||||
|
uri.append("&timeOffset=").append(timeOffset);
|
||||||
|
|
||||||
uri.append("&id=").append(id);
|
uri.append("&id=").append(id);
|
||||||
|
|
||||||
|
|
@ -66,6 +68,10 @@ public class MusicUtil {
|
||||||
return Uri.parse(uri.toString());
|
return Uri.parse(uri.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Uri getStreamUri(String id) {
|
||||||
|
return getStreamUri(id, 0);
|
||||||
|
}
|
||||||
|
|
||||||
public static Uri updateStreamUri(Uri uri) {
|
public static Uri updateStreamUri(Uri uri) {
|
||||||
String s = uri.toString();
|
String s = uri.toString();
|
||||||
Matcher m1 = BITRATE_PATTERN.matcher(s);
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue