Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<Duration>
get() = Promise.whenAny(playingFile.duration, fallbackDurationReader.duration)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -32,8 +34,7 @@ class ExoPlayerPlaybackHandler(private val exoPlayer: PromisingExoPlayer) :
PlayingFile,
PlayedFile,
Player.Listener,
CancellationResponse
{
CancellationResponse {

companion object {
private val minutesAndSecondsFormatter by lazy {
Expand All @@ -53,9 +54,10 @@ class ExoPlayerPlaybackHandler(private val exoPlayer: PromisingExoPlayer) :
override fun respond(currentPosition: Long): Duration = Duration.millis(currentPosition)
}

private val fileProgressReader = AtomicReference<CloseableReadFileProgress>(PausedExoPlayerFileProgressReader(exoPlayer))
private val fileProgressReader =
AtomicReference<CloseableReadFileProgress>(PausedExoPlayerFileProgressReader(exoPlayer))

private val promisedDuration = RetryOnRejectionLazyPromise { exoPlayer.getDuration().then(LongDurationTransformer) }
private val promisedDuration by lazy { PlayingExoPlayerFileDurationReader(exoPlayer) }

init {
awaitCancellation(this)
Expand All @@ -81,7 +83,7 @@ class ExoPlayerPlaybackHandler(private val exoPlayer: PromisingExoPlayer) :
get() = fileProgressReader.get().progress

override val duration: Promise<Duration>
get() = promisedDuration.value
get() = promisedDuration.duration

override fun promisePlayback(): Promise<PlayingFile> {
isPlaying = true
Expand All @@ -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() }
Expand All @@ -121,19 +125,22 @@ 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 -> {
logger.warn("The file ended unexpectedly. Completing playback", error)
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" -> {
Expand All @@ -143,13 +150,15 @@ 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)
resolve(this)
return
}
}

is HttpDataSource.InvalidResponseCodeException -> {
if (cause.responseCode == 416) {
logger.warn("Received an error code of " + cause.responseCode + ", completing playback", cause)
Expand Down Expand Up @@ -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)
}
Expand All @@ -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<Duration>
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<Duration>
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<Duration>(),
ImmediateResponse<Duration, Unit> {
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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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<PlayingFile> =
playableFile
.promisePlayback()
.cancelBackThen { file, _ ->
DurationFallbackPlayingFile(
file,
FilePropertiesDurationReader(
libraryFileProperties,
libraryId,
serviceFile,
)
)
}
}

Original file line number Diff line number Diff line change
@@ -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
}
}
}
Original file line number Diff line number Diff line change
@@ -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<PreparedPlayableFile?> =
innerPreparationSource
.promisePreparedPlaybackFile(libraryId, serviceFile, preparedAt)
.cancelBackThen { file, _ ->
file?.let { f ->
PreparedPlayableFile(
FilePropertiesDurationPlayableFile(
f.playbackHandler,
libraryFileProperties,
libraryId,
serviceFile
),
f.playableFileVolumeManager,
f.bufferingPlaybackFile,
)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
)
}
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ fun <T> T.toPromise(): Promise<T> = when (this) {
null -> Promise.empty()
is Unit -> toPromise() as Promise<T>
is Int -> toPromise() as Promise<T>
is Long -> toPromise() as Promise<T>
is Boolean -> toPromise() as Promise<T>
is String -> if (isEmpty()) EmptyStringPromise as Promise<T> else Promise(this)
else -> Promise(this)
Expand All @@ -107,11 +108,21 @@ fun Int.toPromise(): Promise<Int> = when (this) {
else -> Promise(this)
}

fun Long.toPromise(): Promise<Long> = when (this) {
0L -> ZeroAndLongPromise
1L -> OneAndLongPromise
-1L -> NegativeOneAndLongPromise
else -> Promise(this)
}

fun Boolean.toPromise(): Promise<Boolean> = if (this) TruePromise else FalsePromise

private object ZeroPromise : Promise<Int>(0)
private object ZeroAndLongPromise : Promise<Long>(0L)
private object OnePromise : Promise<Int>(1)
private object OneAndLongPromise : Promise<Long>(1L)
private object NegativeOnePromise : Promise<Int>(-1)
private object NegativeOneAndLongPromise : Promise<Long>(-1L)
private object TruePromise : Promise<Boolean>(true)
private object FalsePromise: Promise<Boolean>(false)
private object EmptyStringPromise: Promise<String>("")
Expand Down
Loading
Loading