Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[APT-10449] Clients Call From Any Thread #48

Merged
merged 2 commits into from
Sep 20, 2024
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
Expand Up @@ -2,12 +2,15 @@ package com.scribd.armadillo

import android.content.Context
import android.os.Bundle
import android.os.Handler
import android.os.HandlerThread
import android.support.v4.media.MediaBrowserCompat
import android.support.v4.media.session.MediaControllerCompat
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import android.util.Log
import androidx.annotation.VisibleForTesting
import androidx.annotation.VisibleForTesting.PRIVATE
import com.scribd.armadillo.actions.Action
import com.scribd.armadillo.actions.ErrorAction
import com.scribd.armadillo.actions.MediaRequestUpdateAction
Expand Down Expand Up @@ -65,6 +68,7 @@ interface ArmadilloPlayer {
fun removeDownload(audioPlayable: AudioPlayable)
fun removeAllDownloads()
fun clearCache()

/**
* Starts playback with the given [AudioPlayable], allowing for configuration through a given [ArmadilloConfiguration]
*/
Expand Down Expand Up @@ -189,22 +193,37 @@ internal class ArmadilloPlayerChoreographer : ArmadilloPlayer {

@Inject
internal lateinit var context: Context

@Inject
internal lateinit var downloadEngine: DownloadEngine

@Inject
internal lateinit var cacheManager: CacheManager

@Inject
internal lateinit var stateProvider: StateStore.Provider

@Inject
internal lateinit var stateModifier: StateStore.Modifier

@Inject
internal lateinit var actionTransmitter: PlaybackActionTransmitter

@Inject
internal lateinit var mediaContentSharer: ArmadilloMediaBrowse.ContentSharer

private companion object {
const val TAG = "ArmadilloChoreographer"
companion object {
private const val TAG = "ArmadilloChoreographer"
private val observerPollIntervalMillis = 500.milliseconds

@VisibleForTesting(otherwise = PRIVATE)
val handlerThread: HandlerThread by lazy {
HandlerThread("ArmadilloChoreographer")
.also { it.start() }
}

@VisibleForTesting(otherwise = PRIVATE)
val handler: Handler by lazy { Handler(handlerThread.looper) }
kschults marked this conversation as resolved.
Show resolved Hide resolved
}

override val playbackCacheSize: Long
Expand All @@ -216,7 +235,7 @@ internal class ArmadilloPlayerChoreographer : ArmadilloPlayer {
* emits the most recently emitted state and all the subsequent states when an observer subscribes to it.
*/
val armadilloStateSubject: BehaviorSubject<ArmadilloState>
get() = stateProvider.stateSubject
get() = stateProvider.stateSubject
override val armadilloStateObservable: Observable<ArmadilloState>
get() = armadilloStateSubject.observeOn(AndroidSchedulers.mainThread())

Expand All @@ -235,34 +254,46 @@ internal class ArmadilloPlayerChoreographer : ArmadilloPlayer {
@VisibleForTesting
internal var playbackConnection: MediaSessionConnection? = null

private fun runHandler(lambda: () -> Unit) {
handler.post { lambda() }
}

private fun runIfPlaybackReady(lambda: (controls: MediaControllerCompat.TransportControls, playbackState: PlaybackState) -> Unit) {
runHandler {
doIfPlaybackReady { controls, playbackState ->
lambda(controls, playbackState)
}
}
}

override fun initDownloadEngine(): ArmadilloPlayer {
isDownloadEngineInit = true
downloadEngine.init()
updateProgressPollTask()
return this
}

override fun beginDownload(audioPlayable: AudioPlayable) {
override fun beginDownload(audioPlayable: AudioPlayable) = runHandler {
if (!isDownloadEngineInit) {
stateModifier.dispatch(ErrorAction(EngineNotInitialized("download engine cannot start download.")))
return
} else {
downloadEngine.download(audioPlayable)
}
downloadEngine.download(audioPlayable)
}

override fun clearCache() {
override fun clearCache() = runHandler {
cacheManager.clearPlaybackCache()
}

override fun removeAllDownloads() {
override fun removeAllDownloads() = runHandler {
if (!isDownloadEngineInit) {
stateModifier.dispatch(ErrorAction(EngineNotInitialized("Cannot remove all the downloads.")))
return
} else {
downloadEngine.removeAllDownloads()
}
downloadEngine.removeAllDownloads()
}

override fun beginPlayback(audioPlayable: AudioPlayable, config: ArmadilloConfiguration) {
override fun beginPlayback(audioPlayable: AudioPlayable, config: ArmadilloConfiguration) = runHandler {
disposables.clear()
actionTransmitter.begin(observerPollIntervalMillis)
val mediaSessionConnection = MediaSessionConnection(context)
Expand All @@ -279,84 +310,80 @@ internal class ArmadilloPlayerChoreographer : ArmadilloPlayer {
})
}

override fun updateMediaRequest(mediaRequest: AudioPlayable.MediaRequest) {
doIfPlaybackReady { controls, _ ->
override fun updateMediaRequest(mediaRequest: AudioPlayable.MediaRequest) =
runIfPlaybackReady { controls, _ ->
stateModifier.dispatch(MediaRequestUpdateAction(mediaRequest))
controls.sendCustomAction(CustomAction.UpdateMediaRequest(mediaRequest))
}
}

override fun updatePlaybackMetadata(title: String, chapters: List<Chapter>) {
override fun updatePlaybackMetadata(title: String, chapters: List<Chapter>) =
doWhenPlaybackReady { controls ->
stateModifier.dispatch(MetadataUpdateAction(title, chapters))
controls.sendCustomAction(CustomAction.UpdatePlaybackMetadata(title, chapters))
}
}

override fun endPlayback() {
override fun endPlayback() = runHandler {
playbackConnection?.transportControls?.stop()
}

override fun deinit() {
override fun deinit() = runHandler {
Log.v(TAG, "deinit")
disposables.clear()
pollingSubscription = null
isDownloadEngineInit = false
actionTransmitter.destroy()
}

override fun playOrPause() {
doIfPlaybackReady { controls, playbackState ->
when (playbackState) {
PlaybackState.PLAYING -> controls.pause()
PlaybackState.PAUSED -> controls.play()
else -> {
stateModifier.dispatch(ErrorAction(
UnexpectedException(cause = IllegalStateException("Neither playing nor paused"),
actionThatFailedMessage = "Trying to play or pause media."))
)
}
override fun playOrPause() = runIfPlaybackReady { controls, playbackState ->
when (playbackState) {
PlaybackState.PLAYING -> controls.pause()
PlaybackState.PAUSED -> controls.play()
else -> {
stateModifier.dispatch(ErrorAction(
UnexpectedException(cause = IllegalStateException("Neither playing nor paused"),
actionThatFailedMessage = "Trying to play or pause media."))
)
}
}
}

// Note: chapter skip and jump-skip behaviours are swapped. See MediaSessionCallback - we are using a jump-skip for skip-forward, as
// most headphones only have a skip-forward button, and this is the ideal behaviour for spoken audio.
override fun nextChapter() = doIfPlaybackReady { controls, _ -> controls.fastForward() }
override fun nextChapter() = runIfPlaybackReady { controls, _ -> controls.fastForward() }

override fun previousChapter() = doIfPlaybackReady { controls, _ -> controls.rewind() }
override fun previousChapter() = runIfPlaybackReady { controls, _ -> controls.rewind() }

override fun skipForward() = doIfPlaybackReady { controls, _ -> controls.skipToNext() }
override fun skipForward() = runIfPlaybackReady { controls, _ -> controls.skipToNext() }

override fun skipBackward() = doIfPlaybackReady { controls, _ -> controls.skipToPrevious() }
override fun skipBackward() = runIfPlaybackReady { controls, _ -> controls.skipToPrevious() }

override fun seekTo(position: Milliseconds) = doIfPlaybackReady { controls, _ ->
override fun seekTo(position: Milliseconds) = runIfPlaybackReady { controls, _ ->
// Add a shift constant to all seeks originating from the client application
// as opposed to system originated, such as from notification
controls.seekTo(position.longValue + Constants.AUDIO_POSITION_SHIFT_IN_MS)
}

override fun seekWithinChapter(percent: Int) {
override fun seekWithinChapter(percent: Int) = runHandler {
val position = stateProvider.currentState.positionFromChapterPercent(percent)
?: run {
stateModifier.dispatch(ErrorAction(
UnexpectedException(cause = KotlinNullPointerException("Current state's position is null"),
actionThatFailedMessage = "seeking within chapter"))
)
return
return@runHandler
}
seekTo(position)
}

override fun removeDownload(audioPlayable: AudioPlayable) {
override fun removeDownload(audioPlayable: AudioPlayable) = runHandler {
if (!isDownloadEngineInit) {
stateModifier.dispatch(ErrorAction(EngineNotInitialized("Cannot remove a download.")))
return
} else {
downloadEngine.removeDownload(audioPlayable)
}
downloadEngine.removeDownload(audioPlayable)
}

override fun addPlaybackActionListener(listener: PlaybackActionListener) {
override fun addPlaybackActionListener(listener: PlaybackActionListener) = runHandler {
PlaybackActionListenerHolder.actionlisteners.add(listener)
}

Expand All @@ -370,7 +397,7 @@ internal class ArmadilloPlayerChoreographer : ArmadilloPlayer {
mediaContentSharer.browseController = null
}

override fun notifyMediaBrowseContentChanged(rootId: String) = mediaContentSharer.notifyContentChanged(rootId)
override fun notifyMediaBrowseContentChanged(rootId: String) = runHandler { mediaContentSharer.notifyContentChanged(rootId) }

/**
* [ArmadilloPlayerChoreographer] polls for updates of playback & downloading
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package com.scribd.armadillo.di

import android.app.Application
import android.content.Context
import com.google.android.exoplayer2.drm.DefaultDrmSessionManagerProvider
import com.google.android.exoplayer2.drm.DrmSessionManagerProvider
import com.scribd.armadillo.StateStore
import com.scribd.armadillo.broadcast.ArmadilloNoisyReceiver
Expand All @@ -21,8 +20,8 @@ import com.scribd.armadillo.playback.PlaybackStateBuilderImpl
import com.scribd.armadillo.playback.PlaybackStateCompatBuilder
import com.scribd.armadillo.playback.mediasource.DrmMediaSourceHelper
import com.scribd.armadillo.playback.mediasource.DrmMediaSourceHelperImpl
import com.scribd.armadillo.playback.mediasource.HeadersMediaSourceHelper
import com.scribd.armadillo.playback.mediasource.HeadersMediaSourceHelperImpl
import com.scribd.armadillo.playback.mediasource.HeadersMediaSourceFactoryFactory
import com.scribd.armadillo.playback.mediasource.HeadersMediaSourceFactoryFactoryImpl
import com.scribd.armadillo.playback.mediasource.MediaSourceRetriever
import com.scribd.armadillo.playback.mediasource.MediaSourceRetrieverImpl
import dagger.Module
Expand Down Expand Up @@ -67,7 +66,7 @@ internal class PlaybackModule {

@Provides
@Singleton
fun mediaSourceHelper(mediaSourceHelperImpl: HeadersMediaSourceHelperImpl): HeadersMediaSourceHelper = mediaSourceHelperImpl
fun mediaSourceHelper(mediaSourceHelperImpl: HeadersMediaSourceFactoryFactoryImpl): HeadersMediaSourceFactoryFactory = mediaSourceHelperImpl

@Provides
@Singleton
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,14 @@ import com.scribd.armadillo.actions.OpeningLicenseAction
import com.scribd.armadillo.download.DownloadEngine
import com.scribd.armadillo.download.DownloadTracker
import com.scribd.armadillo.download.drm.events.WidevineSessionEventListener
import com.scribd.armadillo.extensions.toUri
import com.scribd.armadillo.models.AudioPlayable
import com.scribd.armadillo.models.DrmType
import javax.inject.Inject

/** For playback, both streaming and downloaded */
internal class DashMediaSourceGenerator @Inject constructor(
context: Context,
private val mediaSourceHelper: HeadersMediaSourceHelper,
private val mediaSourceFactoryFactory: HeadersMediaSourceFactoryFactory,
private val downloadTracker: DownloadTracker,
private val drmMediaSourceHelper: DrmMediaSourceHelper,
private val drmSessionManagerProvider: DrmSessionManagerProvider,
Expand All @@ -34,7 +33,7 @@ internal class DashMediaSourceGenerator @Inject constructor(
if (request.drmInfo != null) {
stateStore.dispatch(OpeningLicenseAction(request.drmInfo.drmType))
}
val dataSourceFactory = mediaSourceHelper.createDataSourceFactory(context, request)
val dataSourceFactory = mediaSourceFactoryFactory.createDataSourceFactory(context, request)

val download = downloadTracker.getDownload(id = mediaId, uri = request.url)
val isDownloaded = download != null && download.state == Download.STATE_COMPLETED
Expand All @@ -46,29 +45,35 @@ internal class DashMediaSourceGenerator @Inject constructor(
)

return if (isDownloaded) {
val drmManager = drmSessionManagerProvider.get(mediaItem)
if(request.drmInfo?.drmType == DrmType.WIDEVINE) {
val drmManager = if (request.drmInfo != null) {
drmSessionManagerProvider.get(mediaItem)
} else null

if (request.drmInfo?.drmType == DrmType.WIDEVINE) {
downloadEngine.redownloadDrmLicense(id = mediaId, request = request)
}
DownloadHelper.createMediaSource(download!!.request, dataSourceFactory, drmManager)
} else {
DashMediaSource.Factory(dataSourceFactory)
.setDrmSessionManagerProvider(drmSessionManagerProvider)
.createMediaSource(mediaItem).also { source ->
//download equivalent is in DashDrmLicenseDownloader
when (request.drmInfo?.drmType) {
DrmType.WIDEVINE -> {
source.addDrmEventListener(
drmHandler,
WidevineSessionEventListener()
)
}

else -> Unit //no DRM
var factory = DashMediaSource.Factory(dataSourceFactory)
if (request.drmInfo != null) {
factory = factory.setDrmSessionManagerProvider(drmSessionManagerProvider)
}
factory.createMediaSource(mediaItem).also { source ->
//download equivalent is in DashDrmLicenseDownloader
when (request.drmInfo?.drmType) {
DrmType.WIDEVINE -> {
source.addDrmEventListener(
drmHandler,
WidevineSessionEventListener()
)
}

else -> Unit //no DRM
}
}
}
}

override fun updateMediaSourceHeaders(request: AudioPlayable.MediaRequest) = mediaSourceHelper.updateMediaSourceHeaders(request)
override fun updateMediaSourceHeaders(request: AudioPlayable.MediaRequest) =
mediaSourceFactoryFactory.updateMediaSourceHeaders(request)
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ internal class DrmMediaSourceHelperImpl @Inject constructor(private val secureSt
.setUri(request.url)
.apply {
// Apply DRM config if content is DRM-protected
val drmConfig = request.drmInfo?.let { drmInfo ->
request.drmInfo?.let { drmInfo ->
MediaItem.DrmConfiguration.Builder(drmInfo.drmType.toExoplayerConstant())
.setLicenseUri(drmInfo.licenseServer)
.setLicenseRequestHeaders(drmInfo.drmHeaders)
Expand All @@ -46,8 +46,9 @@ internal class DrmMediaSourceHelperImpl @Inject constructor(private val secureSt
}
}
.build()
}?.let { drmConfig ->
setDrmConfiguration(drmConfig)
}
setDrmConfiguration(drmConfig)
}
.build()
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import android.content.Context
import com.google.android.exoplayer2.upstream.DataSource
import com.scribd.armadillo.models.AudioPlayable

internal interface HeadersMediaSourceHelper {
internal interface HeadersMediaSourceFactoryFactory {
fun createDataSourceFactory(context: Context, request: AudioPlayable.MediaRequest): DataSource.Factory
fun updateMediaSourceHeaders(request: AudioPlayable.MediaRequest)
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ import javax.inject.Inject
import javax.inject.Singleton

@Singleton
internal class HeadersMediaSourceHelperImpl @Inject constructor(
internal class HeadersMediaSourceFactoryFactoryImpl @Inject constructor(
private val cacheManager: CacheManager,
private val headersStore: HeadersStore
): HeadersMediaSourceHelper {
): HeadersMediaSourceFactoryFactory {
private val previousRequests = mutableMapOf<String, DefaultHttpDataSource.Factory>()

override fun createDataSourceFactory(context: Context, request: AudioPlayable.MediaRequest): DataSource.Factory {
Expand Down
Loading
Loading