diff --git a/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/playback/file/DurationFallbackPlayingFile.kt b/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/playback/file/DurationFallbackPlayingFile.kt new file mode 100644 index 0000000000..66de7693da --- /dev/null +++ b/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/playback/file/DurationFallbackPlayingFile.kt @@ -0,0 +1,13 @@ +package com.lasthopesoftware.bluewater.client.playback.file + +import com.lasthopesoftware.bluewater.client.playback.file.progress.ReadFileDuration +import com.namehillsoftware.handoff.promises.Promise +import org.joda.time.Duration + +class DurationFallbackPlayingFile( + private val playingFile: PlayingFile, + private val fallbackDurationReader: ReadFileDuration +) : PlayingFile by playingFile { + override val duration: Promise + get() = Promise.whenAny(playingFile.duration, fallbackDurationReader.duration) +} diff --git a/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/playback/file/exoplayer/ExoPlayerPlaybackHandler.kt b/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/playback/file/exoplayer/ExoPlayerPlaybackHandler.kt index c1ec51b44a..2a3887d7fc 100644 --- a/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/playback/file/exoplayer/ExoPlayerPlaybackHandler.kt +++ b/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/playback/file/exoplayer/ExoPlayerPlaybackHandler.kt @@ -11,9 +11,11 @@ import com.lasthopesoftware.bluewater.client.playback.file.PlayableFile import com.lasthopesoftware.bluewater.client.playback.file.PlayedFile import com.lasthopesoftware.bluewater.client.playback.file.PlayingFile import com.lasthopesoftware.bluewater.client.playback.file.exoplayer.error.ExoPlayerException +import com.lasthopesoftware.bluewater.client.playback.file.progress.ReadFileDuration import com.lasthopesoftware.bluewater.client.playback.file.progress.ReadFileProgress import com.lasthopesoftware.bluewater.shared.lazyLogger import com.lasthopesoftware.policies.retries.RetryOnRejectionLazyPromise +import com.lasthopesoftware.promises.ResolvedPromiseBox import com.lasthopesoftware.promises.extensions.ProgressedPromise import com.lasthopesoftware.promises.extensions.toPromise import com.namehillsoftware.handoff.cancellation.CancellationResponse @@ -32,8 +34,7 @@ class ExoPlayerPlaybackHandler(private val exoPlayer: PromisingExoPlayer) : PlayingFile, PlayedFile, Player.Listener, - CancellationResponse -{ + CancellationResponse { companion object { private val minutesAndSecondsFormatter by lazy { @@ -53,9 +54,10 @@ class ExoPlayerPlaybackHandler(private val exoPlayer: PromisingExoPlayer) : override fun respond(currentPosition: Long): Duration = Duration.millis(currentPosition) } - private val fileProgressReader = AtomicReference(PausedExoPlayerFileProgressReader(exoPlayer)) + private val fileProgressReader = + AtomicReference(PausedExoPlayerFileProgressReader(exoPlayer)) - private val promisedDuration = RetryOnRejectionLazyPromise { exoPlayer.getDuration().then(LongDurationTransformer) } + private val promisedDuration by lazy { PlayingExoPlayerFileDurationReader(exoPlayer) } init { awaitCancellation(this) @@ -81,7 +83,7 @@ class ExoPlayerPlaybackHandler(private val exoPlayer: PromisingExoPlayer) : get() = fileProgressReader.get().progress override val duration: Promise - get() = promisedDuration.value + get() = promisedDuration.duration override fun promisePlayback(): Promise { isPlaying = true @@ -106,7 +108,9 @@ class ExoPlayerPlaybackHandler(private val exoPlayer: PromisingExoPlayer) : val formatter = minutesAndSecondsFormatter logger.warn( "The player was playing, but it transitioned to idle! " + - "Playback progress: " + p.toPeriod().toString(formatter) + " / " + d.toPeriod().toString(formatter) + ". ") + "Playback progress: " + p.toPeriod().toString(formatter) + " / " + d.toPeriod() + .toString(formatter) + ". " + ) } } .eventually { exoPlayer.getPlayWhenReady() } @@ -121,7 +125,8 @@ class ExoPlayerPlaybackHandler(private val exoPlayer: PromisingExoPlayer) : } } - @OptIn(UnstableApi::class) override fun onPlayerError(error: PlaybackException) { + @OptIn(UnstableApi::class) + override fun onPlayerError(error: PlaybackException) { removeListener() when (val cause = error.cause) { is EOFException -> { @@ -129,11 +134,13 @@ class ExoPlayerPlaybackHandler(private val exoPlayer: PromisingExoPlayer) : resolve(this) return } + is NoSuchElementException -> { logger.warn("The player was unexpectedly unable to dequeue messages, completing playback", error) resolve(this) return } + is ProtocolException -> { when (cause.message) { "unexpected end of stream" -> { @@ -143,6 +150,7 @@ class ExoPlayerPlaybackHandler(private val exoPlayer: PromisingExoPlayer) : } } } + is ParserException -> { if (cause.message.startsWith("Searched too many bytes.")) { logger.warn("The stream was corrupted, completing playback", error) @@ -150,6 +158,7 @@ class ExoPlayerPlaybackHandler(private val exoPlayer: PromisingExoPlayer) : return } } + is HttpDataSource.InvalidResponseCodeException -> { if (cause.responseCode == 416) { logger.warn("Received an error code of " + cause.responseCode + ", completing playback", cause) @@ -183,8 +192,7 @@ class ExoPlayerPlaybackHandler(private val exoPlayer: PromisingExoPlayer) : private interface CloseableReadFileProgress : ReadFileProgress, AutoCloseable private class PausedExoPlayerFileProgressReader(private val exoPlayer: PromisingExoPlayer) : - CloseableReadFileProgress - { + CloseableReadFileProgress { private val promisedFileProgress = RetryOnRejectionLazyPromise { exoPlayer.getCurrentPosition().then(LongDurationTransformer) } @@ -198,28 +206,64 @@ class ExoPlayerPlaybackHandler(private val exoPlayer: PromisingExoPlayer) : } private class PlayingExoPlayerFileProgressReader(private val exoPlayer: PromisingExoPlayer) : - CloseableReadFileProgress - { - companion object { - private val zeroAndLong by lazy { 0L.toPromise() } - } - + CloseableReadFileProgress { @Volatile private var isClosed = false - private val currentDurationPromise = AtomicReference(zeroAndLong) + private val currentProgressPromise = AtomicReference(0L.toPromise()) override val progress: Promise get() = - if (isClosed) currentDurationPromise.get().then(LongDurationTransformer) - else currentDurationPromise.updateAndGet { prev -> + if (isClosed) currentProgressPromise.get().then(LongDurationTransformer) + else currentProgressPromise.updateAndGet { prev -> prev.cancel() exoPlayer.getCurrentPosition() }.then(LongDurationTransformer) override fun close() { isClosed = true - currentDurationPromise.get().cancel() + currentProgressPromise.get().cancel() + } + } + + private class PlayingExoPlayerFileDurationReader(private val exoPlayer: PromisingExoPlayer) : + ReadFileDuration, AutoCloseable { + @Volatile + private var isClosed = false + + private val currentDurationPromise = AtomicReference(ResolvedPromiseBox(PositiveDurationPromise(exoPlayer))) + + override val duration: Promise + get() = + if (isClosed) currentDurationPromise.get().originalPromise + else currentDurationPromise.get().resolvedPromise ?: currentDurationPromise.updateAndGet { prev -> + prev.run { + if (resolvedPromise != null) this + else { + originalPromise.cancel() + ResolvedPromiseBox(PositiveDurationPromise(exoPlayer)) + } + } + }.originalPromise + + override fun close() { + isClosed = true + currentDurationPromise.get().originalPromise.cancel() + } + } + + private class PositiveDurationPromise(exoPlayer: PromisingExoPlayer) : Proxy(), + ImmediateResponse { + init { + val promisedDuration = exoPlayer.getDuration().then(LongDurationTransformer) + doCancel(promisedDuration) + promisedDuration.then(this) + proxyRejection(promisedDuration) + } + + override fun respond(position: Duration) { + if (position.isLongerThan(Duration.ZERO)) + resolve(position) } } } diff --git a/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/playback/file/exoplayer/preparation/ExoPlayerPlaybackPreparer.kt b/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/playback/file/exoplayer/preparation/ExoPlayerPlaybackPreparer.kt index 9ed9937a83..f7b9e7d3f1 100644 --- a/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/playback/file/exoplayer/preparation/ExoPlayerPlaybackPreparer.kt +++ b/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/playback/file/exoplayer/preparation/ExoPlayerPlaybackPreparer.kt @@ -96,10 +96,9 @@ class ExoPlayerPlaybackPreparer( .eventually { mediaSource -> bufferingExoPlayerProvider .promiseBufferingExoPlayer(mediaSource, it) - .then { ep -> Pair(mediaSource, ep) } + .then { ep -> mediaSource to ep } } - .eventually { pair -> - val (mediaSource, newBufferingExoPlayer) = pair + .eventually { (mediaSource, newBufferingExoPlayer) -> bufferingExoPlayer = newBufferingExoPlayer val prepareAtMillis = prepareAt.millis diff --git a/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/playback/file/initialization/IPlaybackInitialization.kt b/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/playback/file/initialization/IPlaybackInitialization.kt deleted file mode 100644 index 055035b6ac..0000000000 --- a/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/playback/file/initialization/IPlaybackInitialization.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.lasthopesoftware.bluewater.client.playback.file.initialization - -import android.net.Uri -import java.io.IOException - -interface IPlaybackInitialization { - @Throws(IOException::class) - fun initializeMediaPlayer(fileUri: Uri?): TMediaPlayer -} diff --git a/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/playback/file/properties/FilePropertiesDurationPlayableFile.kt b/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/playback/file/properties/FilePropertiesDurationPlayableFile.kt new file mode 100644 index 0000000000..2657c78fbb --- /dev/null +++ b/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/playback/file/properties/FilePropertiesDurationPlayableFile.kt @@ -0,0 +1,32 @@ +package com.lasthopesoftware.bluewater.client.playback.file.properties + +import com.lasthopesoftware.bluewater.client.browsing.files.ServiceFile +import com.lasthopesoftware.bluewater.client.browsing.files.properties.ProvideLibraryFileProperties +import com.lasthopesoftware.bluewater.client.browsing.library.repository.LibraryId +import com.lasthopesoftware.bluewater.client.playback.file.DurationFallbackPlayingFile +import com.lasthopesoftware.bluewater.client.playback.file.PlayableFile +import com.lasthopesoftware.bluewater.client.playback.file.PlayingFile +import com.lasthopesoftware.promises.extensions.cancelBackThen +import com.namehillsoftware.handoff.promises.Promise + +class FilePropertiesDurationPlayableFile( + private val playableFile: PlayableFile, + private val libraryFileProperties: ProvideLibraryFileProperties, + private val libraryId: LibraryId, + private val serviceFile: ServiceFile, +) : PlayableFile by playableFile { + override fun promisePlayback(): Promise = + playableFile + .promisePlayback() + .cancelBackThen { file, _ -> + DurationFallbackPlayingFile( + file, + FilePropertiesDurationReader( + libraryFileProperties, + libraryId, + serviceFile, + ) + ) + } +} + diff --git a/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/playback/file/properties/FilePropertiesDurationReader.kt b/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/playback/file/properties/FilePropertiesDurationReader.kt new file mode 100644 index 0000000000..8398b378c0 --- /dev/null +++ b/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/playback/file/properties/FilePropertiesDurationReader.kt @@ -0,0 +1,24 @@ +package com.lasthopesoftware.bluewater.client.playback.file.properties + +import com.lasthopesoftware.bluewater.client.browsing.files.ServiceFile +import com.lasthopesoftware.bluewater.client.browsing.files.properties.FilePropertyHelpers.duration +import com.lasthopesoftware.bluewater.client.browsing.files.properties.ProvideLibraryFileProperties +import com.lasthopesoftware.bluewater.client.browsing.library.repository.LibraryId +import com.lasthopesoftware.bluewater.client.playback.file.progress.ReadFileDuration +import com.lasthopesoftware.policies.retries.RetryOnRejectionLazyPromise +import com.lasthopesoftware.promises.extensions.cancelBackThen +import org.joda.time.Duration + +class FilePropertiesDurationReader( + private val libraryFileProperties: ProvideLibraryFileProperties, + private val libraryId: LibraryId, + private val serviceFile: ServiceFile, +) : ReadFileDuration { + override val duration by RetryOnRejectionLazyPromise { + libraryFileProperties + .promiseFileProperties(libraryId, serviceFile) + .cancelBackThen { fp, _ -> + fp.duration ?: Duration.ZERO + } + } +} diff --git a/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/playback/file/properties/FilePropertiesPlaybackPreparer.kt b/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/playback/file/properties/FilePropertiesPlaybackPreparer.kt new file mode 100644 index 0000000000..112b4da554 --- /dev/null +++ b/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/playback/file/properties/FilePropertiesPlaybackPreparer.kt @@ -0,0 +1,44 @@ +package com.lasthopesoftware.bluewater.client.playback.file.properties + +import com.lasthopesoftware.bluewater.client.browsing.files.ServiceFile +import com.lasthopesoftware.bluewater.client.browsing.files.properties.ProvideLibraryFileProperties +import com.lasthopesoftware.bluewater.client.browsing.library.repository.LibraryId +import com.lasthopesoftware.bluewater.client.playback.engine.preparation.ProvidePlayableFilePreparationSources +import com.lasthopesoftware.bluewater.client.playback.file.preparation.PlayableFilePreparationSource +import com.lasthopesoftware.bluewater.client.playback.file.preparation.PreparedPlayableFile +import com.lasthopesoftware.promises.extensions.cancelBackThen +import com.namehillsoftware.handoff.promises.Promise +import org.joda.time.Duration + +class FilePropertiesPreparationProvider( + private val preparationSourceProvider: ProvidePlayableFilePreparationSources, + private val libraryFileProperties: ProvideLibraryFileProperties +) : ProvidePlayableFilePreparationSources by preparationSourceProvider { + override fun providePlayableFilePreparationSource(): PlayableFilePreparationSource = FilePropertiesPlaybackPreparer( + preparationSourceProvider.providePlayableFilePreparationSource()) + + private inner class FilePropertiesPlaybackPreparer( + private val innerPreparationSource: PlayableFilePreparationSource, + ) : PlayableFilePreparationSource { + override fun promisePreparedPlaybackFile( + libraryId: LibraryId, + serviceFile: ServiceFile, + preparedAt: Duration): Promise = + innerPreparationSource + .promisePreparedPlaybackFile(libraryId, serviceFile, preparedAt) + .cancelBackThen { file, _ -> + file?.let { f -> + PreparedPlayableFile( + FilePropertiesDurationPlayableFile( + f.playbackHandler, + libraryFileProperties, + libraryId, + serviceFile + ), + f.playableFileVolumeManager, + f.bufferingPlaybackFile, + ) + } + } + } +} diff --git a/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/playback/service/PlaybackService.kt b/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/playback/service/PlaybackService.kt index 6be85b4faa..76b94435c2 100644 --- a/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/playback/service/PlaybackService.kt +++ b/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/playback/service/PlaybackService.kt @@ -68,6 +68,7 @@ import com.lasthopesoftware.bluewater.client.playback.file.exoplayer.preparation import com.lasthopesoftware.bluewater.client.playback.file.exoplayer.preparation.mediasource.MediaSourceProvider import com.lasthopesoftware.bluewater.client.playback.file.exoplayer.preparation.mediasource.RemoteDataSourceFactoryProvider import com.lasthopesoftware.bluewater.client.playback.file.preparation.queues.QueueProviders +import com.lasthopesoftware.bluewater.client.playback.file.properties.FilePropertiesPreparationProvider import com.lasthopesoftware.bluewater.client.playback.file.volume.MaxFileVolumeProvider import com.lasthopesoftware.bluewater.client.playback.file.volume.preparation.MaxFileVolumePreparationProvider import com.lasthopesoftware.bluewater.client.playback.nowplaying.broadcasters.notification.NotificationsConfiguration @@ -491,23 +492,26 @@ import java.util.concurrent.TimeoutException .then { h -> Handler(h.looper) } .then { playbackHandler -> MaxFileVolumePreparationProvider( - FallbackPreparedPlayableFileSourceProvider( - ExoPlayerPlayableFilePreparationSourceProvider( - this, - playbackHandler, - mainLoopHandlerExecutor, - mediaSourceProvider, - bestMatchUriProvider - ), - ExoPlayerPlayableFilePreparationSourceProvider( - this, - playbackHandler, - mainLoopHandlerExecutor, - mediaSourceProvider, - remoteFileUriProvider + FilePropertiesPreparationProvider( + FallbackPreparedPlayableFileSourceProvider( + ExoPlayerPlayableFilePreparationSourceProvider( + this, + playbackHandler, + mainLoopHandlerExecutor, + mediaSourceProvider, + bestMatchUriProvider + ), + ExoPlayerPlayableFilePreparationSourceProvider( + this, + playbackHandler, + mainLoopHandlerExecutor, + mediaSourceProvider, + remoteFileUriProvider + ), ), + libraryFilePropertiesProvider, ), - maxFileVolumeProvider + maxFileVolumeProvider, ) } }) diff --git a/projectBlueWater/src/main/java/com/lasthopesoftware/promises/extensions/ValuePromiseExtension.kt b/projectBlueWater/src/main/java/com/lasthopesoftware/promises/extensions/ValuePromiseExtension.kt index b88702dce4..e579d32298 100644 --- a/projectBlueWater/src/main/java/com/lasthopesoftware/promises/extensions/ValuePromiseExtension.kt +++ b/projectBlueWater/src/main/java/com/lasthopesoftware/promises/extensions/ValuePromiseExtension.kt @@ -93,6 +93,7 @@ fun T.toPromise(): Promise = when (this) { null -> Promise.empty() is Unit -> toPromise() as Promise is Int -> toPromise() as Promise + is Long -> toPromise() as Promise is Boolean -> toPromise() as Promise is String -> if (isEmpty()) EmptyStringPromise as Promise else Promise(this) else -> Promise(this) @@ -107,11 +108,21 @@ fun Int.toPromise(): Promise = when (this) { else -> Promise(this) } +fun Long.toPromise(): Promise = when (this) { + 0L -> ZeroAndLongPromise + 1L -> OneAndLongPromise + -1L -> NegativeOneAndLongPromise + else -> Promise(this) +} + fun Boolean.toPromise(): Promise = if (this) TruePromise else FalsePromise private object ZeroPromise : Promise(0) +private object ZeroAndLongPromise : Promise(0L) private object OnePromise : Promise(1) +private object OneAndLongPromise : Promise(1L) private object NegativeOnePromise : Promise(-1) +private object NegativeOneAndLongPromise : Promise(-1L) private object TruePromise : Promise(true) private object FalsePromise: Promise(false) private object EmptyStringPromise: Promise("") diff --git a/projectBlueWater/src/test/java/com/lasthopesoftware/bluewater/client/playback/file/DurationFallback/GivenAPlayingFile/AndItsDurationResolvesFirst/When getting the duration.kt b/projectBlueWater/src/test/java/com/lasthopesoftware/bluewater/client/playback/file/DurationFallback/GivenAPlayingFile/AndItsDurationResolvesFirst/When getting the duration.kt new file mode 100644 index 0000000000..40862908f2 --- /dev/null +++ b/projectBlueWater/src/test/java/com/lasthopesoftware/bluewater/client/playback/file/DurationFallback/GivenAPlayingFile/AndItsDurationResolvesFirst/When getting the duration.kt @@ -0,0 +1,36 @@ +package com.lasthopesoftware.bluewater.client.playback.file.DurationFallback.GivenAPlayingFile.AndItsDurationResolvesFirst + +import com.lasthopesoftware.bluewater.client.playback.file.DurationFallbackPlayingFile +import com.lasthopesoftware.bluewater.shared.promises.extensions.toExpiringFuture +import com.lasthopesoftware.promises.extensions.toPromise +import io.mockk.every +import io.mockk.mockk +import org.assertj.core.api.Assertions.assertThat +import org.joda.time.Duration +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test + +class `When getting the duration` { + private val mut by lazy { + DurationFallbackPlayingFile( + mockk { + every { duration } returns Duration.standardSeconds(372).toPromise() + }, + mockk { + every { duration } returns Duration.standardSeconds(500).toPromise() + } + ) + } + + private var duration: Duration? = null + + @BeforeAll + fun act() { + duration = mut.duration.toExpiringFuture().get() + } + + @Test + fun `then the duration is read from the playing file`() { + assertThat(duration).isEqualTo(Duration.standardSeconds(372)) + } +} diff --git a/projectBlueWater/src/test/java/com/lasthopesoftware/bluewater/client/playback/file/DurationFallback/GivenAPlayingFile/AndTheFilePropertyDurationResolvesFirst/When getting the duration.kt b/projectBlueWater/src/test/java/com/lasthopesoftware/bluewater/client/playback/file/DurationFallback/GivenAPlayingFile/AndTheFilePropertyDurationResolvesFirst/When getting the duration.kt new file mode 100644 index 0000000000..351712522b --- /dev/null +++ b/projectBlueWater/src/test/java/com/lasthopesoftware/bluewater/client/playback/file/DurationFallback/GivenAPlayingFile/AndTheFilePropertyDurationResolvesFirst/When getting the duration.kt @@ -0,0 +1,37 @@ +package com.lasthopesoftware.bluewater.client.playback.file.DurationFallback.GivenAPlayingFile.AndTheFilePropertyDurationResolvesFirst + +import com.lasthopesoftware.bluewater.client.playback.file.DurationFallbackPlayingFile +import com.lasthopesoftware.bluewater.shared.promises.extensions.toExpiringFuture +import com.lasthopesoftware.promises.extensions.toPromise +import com.namehillsoftware.handoff.promises.Promise +import io.mockk.every +import io.mockk.mockk +import org.assertj.core.api.Assertions.assertThat +import org.joda.time.Duration +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test + +class `When getting the duration` { + private val mut by lazy { + DurationFallbackPlayingFile( + mockk { + every { duration } returns Promise {} + }, + mockk { + every { duration } returns Duration.standardSeconds(975).toPromise() + } + ) + } + + private var duration: Duration? = null + + @BeforeAll + fun act() { + duration = mut.duration.toExpiringFuture().get() + } + + @Test + fun `then the duration is read from the fallback source`() { + assertThat(duration).isEqualTo(Duration.standardSeconds(975)) + } +}