Skip to content

Commit

Permalink
Interface for DRM Visibility
Browse files Browse the repository at this point in the history
- Adds DrmState to ArmadilloState, giving full visibility into DRM status to the client, including the expiration date for content.
- Splits SocketTimeout from HttpResponseCodeException, into ConnectivityException to better differentiate connectivity difficulties from
  developer error.
- Ensures that ArmadilloState is updated from the main thread consistently.
  • Loading branch information
kabliz committed Aug 21, 2024
1 parent e9df9bb commit 93eaed8
Show file tree
Hide file tree
Showing 23 changed files with 504 additions and 43 deletions.
16 changes: 16 additions & 0 deletions Armadillo/src/main/java/com/scribd/armadillo/ArmadilloDebugView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import android.widget.TextView
import com.scribd.armadillo.models.ArmadilloState
import com.scribd.armadillo.models.AudioPlayable
import com.scribd.armadillo.models.DownloadState
import com.scribd.armadillo.models.DrmState
import java.util.concurrent.TimeUnit

/**
Expand Down Expand Up @@ -43,6 +44,7 @@ class ArmadilloDebugView : FrameLayout {
private val playbackSpeedTv: TextView by lazy { findViewById<TextView>(R.id.playbackSpeedTv) }
private val downloadStateTv: TextView by lazy { findViewById<TextView>(R.id.downloadStateTv) }
private val downloadPercentTv: TextView by lazy { findViewById<TextView>(R.id.downloadPercentTv) }
private val drmStatusTv: TextView by lazy { findViewById(R.id.drmStatusTv) }

fun update(state: ArmadilloState, audioPlayable: AudioPlayable) {
val activity = context as Activity
Expand All @@ -51,6 +53,20 @@ class ArmadilloDebugView : FrameLayout {
appStateUpdateCountTv.text = appStateCountText
actionsDispatchedTv.text = state.debugState.getActionHistoryDisplayString()

drmStatusTv.text = when (state.drmPlaybackState) {
is DrmState.NoDRM -> context.getString(R.string.arm_drm_none)
is DrmState.LicenseOpening -> context.getString(R.string.arm_drm_opening)
is DrmState.LicenseReleased -> context.getString(R.string.arm_drm_released)
is DrmState.LicenseExpired -> context.getString(R.string.arm_drm_expired)
is DrmState.LicenseError -> context.getString(R.string.arm_drm_error)
is DrmState.LicenseAcquired,
is DrmState.LicenseUsable -> {
"${context.getString(R.string.arm_drm_using)} ${state.drmPlaybackState.drmType.toString()} : ${context.getString(R.string
.arm_drm_expiring)} " +
"${state.drmPlaybackState.expireMillis}"
}
}

val playbackInfo = state.playbackInfo
if (playbackInfo != null) {
titleTv.text = activity.getString(R.string.arm_title, state.playbackInfo.audioPlayable.title)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ interface ArmadilloPlayer {
fun beginPlayback(audioPlayable: AudioPlayable, config: ArmadilloConfiguration = ArmadilloConfiguration())

/**
* Provide new media request data to the currently playing content.
* Provide new media request data to the currently playing content, such as request headers or DRM updates.
* It is an error to call this with a different URL from the currently playing media.
* It is also an error to call this when no content is currently loaded.
* If you want to start playback from a new URL, use [beginPlayback].
Expand Down
48 changes: 47 additions & 1 deletion Armadillo/src/main/java/com/scribd/armadillo/Reducer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,17 @@ import com.scribd.armadillo.actions.ContentEndedAction
import com.scribd.armadillo.actions.CustomMediaSessionAction
import com.scribd.armadillo.actions.ErrorAction
import com.scribd.armadillo.actions.FastForwardAction
import com.scribd.armadillo.actions.LicenseAcquiredAction
import com.scribd.armadillo.actions.LicenseDrmErrorAction
import com.scribd.armadillo.actions.LicenseExpirationDetermined
import com.scribd.armadillo.actions.LicenseExpiredAction
import com.scribd.armadillo.actions.LicenseKeyIsUsableAction
import com.scribd.armadillo.actions.LicenseReleasedAction
import com.scribd.armadillo.actions.LoadingAction
import com.scribd.armadillo.actions.MediaRequestUpdateAction
import com.scribd.armadillo.actions.MetadataUpdateAction
import com.scribd.armadillo.actions.NewAudioPlayableAction
import com.scribd.armadillo.actions.OpeningLicenseAction
import com.scribd.armadillo.actions.PlaybackEngineReady
import com.scribd.armadillo.actions.PlaybackProgressAction
import com.scribd.armadillo.actions.PlaybackSpeedAction
Expand All @@ -33,6 +40,7 @@ import com.scribd.armadillo.extensions.filterOutCompletedItems
import com.scribd.armadillo.extensions.removeItemsByUrl
import com.scribd.armadillo.extensions.replaceDownloadProgressItemsByUrl
import com.scribd.armadillo.models.ArmadilloState
import com.scribd.armadillo.models.DrmState
import com.scribd.armadillo.models.MediaControlState
import com.scribd.armadillo.models.PlaybackInfo
import com.scribd.armadillo.models.PlaybackProgress
Expand All @@ -46,6 +54,7 @@ import com.scribd.armadillo.time.Millisecond
*/
internal object Reducer {
const val TAG = "Reducer"

/**
* Due to floating point math, adding up the duration of the each chapter to determine the audioPlayable duration will result in some
* level of inaccuracy. Generally, we seek to have the duration be slightly shorter then the actual playlist length. This value is
Expand Down Expand Up @@ -140,7 +149,8 @@ internal object Reducer {
playbackSpeed = Constants.DEFAULT_PLAYBACK_SPEED,
controlState = MediaControlState(isStartingNewAudioPlayable = true),
skipDistance = oldState.playbackInfo?.skipDistance ?: Constants.AUDIO_SKIP_DURATION,
isLoading = true))
isLoading = true),
drmPlaybackState = DrmState.NoDRM)
.apply { debugState = newDebug }
}
is MediaRequestUpdateAction -> {
Expand Down Expand Up @@ -261,6 +271,42 @@ internal object Reducer {
))
.apply { debugState = newDebug }
}
is OpeningLicenseAction -> {
val oldDrm = oldState.drmPlaybackState
return oldState
.copy(drmPlaybackState = DrmState.LicenseOpening(action.type ?: oldDrm.drmType))
}
is LicenseAcquiredAction -> {
val oldDrm = oldState.drmPlaybackState
return oldState
.copy(drmPlaybackState = DrmState.LicenseAcquired(action.type, oldDrm.expireMillis))
}
is LicenseExpiredAction -> {
val oldDrm = oldState.drmPlaybackState
return oldState
.copy(drmPlaybackState = DrmState.LicenseExpired(oldDrm.drmType, oldDrm.expireMillis))
}
is LicenseExpirationDetermined -> {
val oldDrm = oldState.drmPlaybackState
return oldState
.copy(drmPlaybackState = DrmState.LicenseUsable(oldDrm.drmType, action.expirationMilliseconds))
}
is LicenseReleasedAction -> {
val oldDrm = oldState.drmPlaybackState
return oldState
.copy(drmPlaybackState = DrmState.LicenseReleased(oldDrm.drmType,oldDrm.expireMillis, oldDrm.isSessionValid))
}
is LicenseKeyIsUsableAction -> {
val oldDrm = oldState.drmPlaybackState
return oldState
.copy(drmPlaybackState = DrmState.LicenseUsable(oldDrm.drmType, oldDrm.expireMillis))
}
is LicenseDrmErrorAction -> {
val oldDrm = oldState.drmPlaybackState
return oldState
.copy(drmPlaybackState = DrmState.LicenseError(oldDrm.drmType, oldDrm.expireMillis))
}

else -> throw UnrecognizedAction(action)
}
}
Expand Down
17 changes: 11 additions & 6 deletions Armadillo/src/main/java/com/scribd/armadillo/StateStore.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.scribd.armadillo

import android.content.Context
import android.os.Handler
import com.scribd.armadillo.actions.Action
import com.scribd.armadillo.actions.ClearErrorAction
import com.scribd.armadillo.error.MissingDataException
Expand All @@ -25,7 +27,7 @@ internal interface StateStore {
}
}

internal class ArmadilloStateStore(private val reducer: Reducer) :
internal class ArmadilloStateStore(private val reducer: Reducer, private val appContext: Context) :
StateStore.Modifier, StateStore.Provider, StateStore.Initializer {

private companion object {
Expand All @@ -37,11 +39,14 @@ internal class ArmadilloStateStore(private val reducer: Reducer) :
override fun init(state: ArmadilloState) = armadilloStateObservable.onNext(state)

override fun dispatch(action: Action) {
val newAppState = reducer.reduce(currentState, action)
armadilloStateObservable.onNext(newAppState)

if (currentState.error != null) {
dispatch(ClearErrorAction)
//run on consistent thread
Handler(appContext.mainLooper).post {
val newAppState = reducer.reduce(currentState, action)
armadilloStateObservable.onNext(newAppState)

if (currentState.error != null) {
dispatch(ClearErrorAction)
}
}
}

Expand Down
31 changes: 31 additions & 0 deletions Armadillo/src/main/java/com/scribd/armadillo/actions/Action.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.scribd.armadillo.models.ArmadilloState
import com.scribd.armadillo.models.AudioPlayable
import com.scribd.armadillo.models.Chapter
import com.scribd.armadillo.models.DownloadProgressInfo
import com.scribd.armadillo.models.DrmType
import com.scribd.armadillo.models.PlaybackState

// Exoplayer State Updates
Expand Down Expand Up @@ -103,6 +104,36 @@ internal object ContentEndedAction : Action {
override val name = "ContentEndedAction"
}

internal data class OpeningLicenseAction(val type: DrmType?) : Action {
override val name: String = "OpeningLicenseAction"
}

internal data class LicenseAcquiredAction(val type: DrmType) : Action {
override val name = "LicenseAcquiredAction"
}

internal data class LicenseExpirationDetermined(val expirationMilliseconds: Milliseconds) : Action {
override val name: String
get() = "LicenseExpirationDetermined"
}

/** session can be recovering or resuming */
internal object LicenseKeyIsUsableAction : Action {
override val name: String = "LicenseKeyUsableAction"
}

internal object LicenseExpiredAction : Action {
override val name: String = "LicenseExpiredAction"
}

internal object LicenseReleasedAction : Action {
override val name = "LicenseReleasedAction"
}

internal object LicenseDrmErrorAction : Action {
override val name: String = "LicenseDrmErrorAction"
}

// Errors

internal data class ErrorAction(val error: ArmadilloException) : Action {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.scribd.armadillo.analytics

import android.app.ForegroundServiceStartNotAllowedException
import android.os.Build
import androidx.annotation.VisibleForTesting
import com.scribd.armadillo.Constants
import com.scribd.armadillo.Milliseconds
Expand All @@ -11,6 +10,7 @@ import com.scribd.armadillo.error.ActionListenerException
import com.scribd.armadillo.hasSnowCone
import com.scribd.armadillo.models.ArmadilloState
import com.scribd.armadillo.models.AudioPlayable
import com.scribd.armadillo.models.DrmState
import com.scribd.armadillo.models.InternalState
import com.scribd.armadillo.models.PlaybackState
import com.scribd.armadillo.time.milliseconds
Expand All @@ -34,7 +34,12 @@ internal class PlaybackActionTransmitterImpl(private val stateProvider: StateSto
private var disposables = CompositeDisposable()

private var seekStartState: ArmadilloState? = null
private var currentState: ArmadilloState = ArmadilloState(null, emptyList(), InternalState(), null)
private var currentState: ArmadilloState = ArmadilloState(
playbackInfo = null,
downloadInfo = emptyList(),
drmPlaybackState = DrmState.NoDRM,
internalState = InternalState(),
error = null)
private var lastState: ArmadilloState = currentState
private var pollingIntervalMillis: Milliseconds = 500.milliseconds

Expand Down Expand Up @@ -78,7 +83,12 @@ internal class PlaybackActionTransmitterImpl(private val stateProvider: StateSto

override fun destroy() {
disposables.clear()
currentState = ArmadilloState(null, emptyList(), InternalState(), null)
currentState = ArmadilloState(
playbackInfo = null,
downloadInfo = emptyList(),
drmPlaybackState = DrmState.NoDRM,
internalState = InternalState(),
error = null)
lastState = currentState
seekStartState = null
}
Expand Down Expand Up @@ -116,7 +126,12 @@ internal class PlaybackActionTransmitterImpl(private val stateProvider: StateSto
if (current.playbackInfo?.controlState?.isStopping == true && lastState.playbackInfo?.controlState?.isStopping == false) {
actionListeners.dispatch { listener, state -> listener.onStop(state) }
playbackStateListener?.onPlaybackEnd()
stateToRetainForLast = ArmadilloState(null, emptyList(), InternalState(), null)
stateToRetainForLast = ArmadilloState(
playbackInfo = null,
downloadInfo = emptyList(),
drmPlaybackState = DrmState.NoDRM,
internalState = InternalState(),
error = null)
seekStartState = null
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ internal class AppModule(private val context: Context) {
@Singleton
@PrivateModule
@Provides
fun stateStore(reducer: Reducer): ArmadilloStateStore = ArmadilloStateStore(reducer)
fun stateStore(reducer: Reducer): ArmadilloStateStore = ArmadilloStateStore(reducer, context)

@Singleton
@Provides
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import com.scribd.armadillo.ArmadilloPlayerChoreographer
import com.scribd.armadillo.ArmadilloPlayerFactory
import com.scribd.armadillo.analytics.PlaybackActionTransmitterImpl
import com.scribd.armadillo.download.DefaultExoplayerDownloadService
import com.scribd.armadillo.download.HeaderAwareDownloaderFactory
import com.scribd.armadillo.download.drm.events.WidevineSessionEventListener
import com.scribd.armadillo.playback.ExoplayerPlaybackEngine
import com.scribd.armadillo.playback.MediaSessionCallback
import com.scribd.armadillo.playback.PlaybackService
Expand All @@ -28,4 +28,5 @@ internal interface MainComponent {
fun inject(playerEventListener: PlayerEventListener)
fun inject(playbackActionTransmitterImpl: PlaybackActionTransmitterImpl)
fun inject(mediaSourceRetrieverImpl: MediaSourceRetrieverImpl)
fun inject(widevineSessionEventListener: WidevineSessionEventListener)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ 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
import com.scribd.armadillo.broadcast.ArmadilloNoisySpeakerReceiver
import com.scribd.armadillo.broadcast.ArmadilloNotificationDeleteReceiver
import com.scribd.armadillo.broadcast.NotificationDeleteReceiver
import com.scribd.armadillo.download.drm.ArmadilloDrmSessionManagerProvider
import com.scribd.armadillo.mediaitems.ArmadilloMediaBrowse
import com.scribd.armadillo.mediaitems.MediaContentSharer
import com.scribd.armadillo.playback.ArmadilloAudioAttributes
Expand Down Expand Up @@ -73,5 +75,5 @@ internal class PlaybackModule {

@Provides
@Singleton
fun drmSessionManagerProvider(): DrmSessionManagerProvider = DefaultDrmSessionManagerProvider()
fun drmSessionManagerProvider(stateStore: StateStore.Modifier): DrmSessionManagerProvider = ArmadilloDrmSessionManagerProvider(stateStore)
}
Loading

0 comments on commit 93eaed8

Please sign in to comment.