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

Audio offload refactor #1725

Merged
merged 8 commits into from
Oct 2, 2023
Merged
Show file tree
Hide file tree
Changes from 5 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
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ androidx-concurrent = "1.1.0"
androidx-datastore = "1.0.0"
androidx-health-services = "1.0.0-rc01"
androidx-hilt = "1.1.0-alpha01"
androidx-media3 = "1.2.0-alpha01"
androidx-media3 = "1.2.0-alpha02"
androidx-test-espresso = "3.6.0-alpha01"
androidx-test-ext = "1.2.0-alpha01"
androidx-tracingPerfetto = "1.0.0-rc01"
Expand Down
2 changes: 1 addition & 1 deletion media/backend-media3/api/current.api
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ package com.google.android.horologist.media3.config {
public class WearMedia3Factory {
ctor public WearMedia3Factory(android.content.Context context);
method public final androidx.media3.exoplayer.RenderersFactory audioOnlyRenderersFactory(androidx.media3.exoplayer.audio.AudioSink audioSink, optional androidx.media3.exoplayer.mediacodec.MediaCodecSelector mediaCodecSelector);
method public final androidx.media3.exoplayer.audio.DefaultAudioSink audioSink(boolean attemptOffload, optional int offloadMode, androidx.media3.exoplayer.ExoPlayer.AudioOffloadListener? audioOffloadListener);
method public final androidx.media3.exoplayer.audio.DefaultAudioSink audioSink(androidx.media3.exoplayer.ExoPlayer.AudioOffloadListener? audioOffloadListener);
method public final androidx.media3.exoplayer.mediacodec.MediaCodecSelector mediaCodecSelector();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,24 +28,14 @@ import androidx.media3.exoplayer.mediacodec.MediaCodecSelector
@SuppressLint("UnsafeOptInUsageError")
public open class WearMedia3Factory(private val context: Context) {
public fun audioSink(
attemptOffload: Boolean,
offloadMode: Int = DefaultAudioSink.OFFLOAD_MODE_ENABLED_GAPLESS_NOT_REQUIRED,
audioOffloadListener: AudioOffloadListener?,
): DefaultAudioSink {
return DefaultAudioSink.Builder(context)
.setAudioProcessorChain(DefaultAudioSink.DefaultAudioProcessorChain())
.setExperimentalAudioOffloadListener(audioOffloadListener)
.setEnableFloatOutput(false) // default
.setEnableAudioTrackPlaybackParams(false) // default
.build().apply {
setOffloadMode(
if (attemptOffload) {
offloadMode
} else {
DefaultAudioSink.OFFLOAD_MODE_DISABLED
},
)
}
.build()
}

public fun audioOnlyRenderersFactory(
Expand Down
44 changes: 7 additions & 37 deletions media/media3-audiooffload/api/current.api
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ package com.google.android.horologist.media3.offload {
}

@com.google.android.horologist.annotations.ExperimentalHorologistApi public final class AudioOffloadManager {
ctor public AudioOffloadManager(com.google.android.horologist.media3.logging.ErrorReporter errorReporter, optional kotlinx.coroutines.flow.Flow<? extends com.google.android.horologist.media3.offload.AudioOffloadStrategy> audioOffloadStrategyFlow);
ctor public AudioOffloadManager(com.google.android.horologist.media3.logging.ErrorReporter errorReporter, kotlinx.coroutines.flow.Flow<androidx.media3.common.TrackSelectionParameters.AudioOffloadPreferences> audioOffloadPreferencesFlow);
method @RequiresApi(29) public suspend Object? connect(androidx.media3.exoplayer.ExoPlayer exoPlayer, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public androidx.media3.exoplayer.ExoPlayer.AudioOffloadListener getAudioOffloadListener();
method public kotlinx.coroutines.flow.StateFlow<com.google.android.horologist.media3.offload.AudioOffloadStatus> getOffloadStatus();
Expand All @@ -25,38 +25,35 @@ package com.google.android.horologist.media3.offload {
property public final kotlinx.coroutines.flow.StateFlow<com.google.android.horologist.media3.offload.AudioOffloadStatus> offloadStatus;
}

@com.google.android.horologist.annotations.ExperimentalHorologistApi public final class AudioOffloadStatus {
ctor public AudioOffloadStatus(boolean offloadSchedulingEnabled, boolean sleepingForOffload, optional boolean trackOffload, androidx.media3.common.Format? format, boolean isPlaying, java.util.List<com.google.android.horologist.media3.offload.AudioError> errors, com.google.android.horologist.media3.offload.OffloadTimes offloadTimes, String? strategyStatus, com.google.android.horologist.media3.offload.AudioOffloadStrategy? strategy);
@androidx.media3.common.util.UnstableApi @com.google.android.horologist.annotations.ExperimentalHorologistApi public final class AudioOffloadStatus {
ctor public AudioOffloadStatus(boolean offloadSchedulingEnabled, boolean sleepingForOffload, optional boolean trackOffload, androidx.media3.common.Format? format, boolean isPlaying, java.util.List<com.google.android.horologist.media3.offload.AudioError> errors, com.google.android.horologist.media3.offload.OffloadTimes offloadTimes, androidx.media3.common.TrackSelectionParameters.AudioOffloadPreferences audioOffloadPreferences);
method public boolean component1();
method public boolean component2();
method public boolean component3();
method public androidx.media3.common.Format? component4();
method public boolean component5();
method public java.util.List<com.google.android.horologist.media3.offload.AudioError> component6();
method public com.google.android.horologist.media3.offload.OffloadTimes component7();
method public String? component8();
method public com.google.android.horologist.media3.offload.AudioOffloadStrategy? component9();
method public com.google.android.horologist.media3.offload.AudioOffloadStatus copy(boolean offloadSchedulingEnabled, boolean sleepingForOffload, boolean trackOffload, androidx.media3.common.Format? format, boolean isPlaying, java.util.List<com.google.android.horologist.media3.offload.AudioError> errors, com.google.android.horologist.media3.offload.OffloadTimes offloadTimes, String? strategyStatus, com.google.android.horologist.media3.offload.AudioOffloadStrategy? strategy);
method public androidx.media3.common.TrackSelectionParameters.AudioOffloadPreferences component8();
method public com.google.android.horologist.media3.offload.AudioOffloadStatus copy(boolean offloadSchedulingEnabled, boolean sleepingForOffload, boolean trackOffload, androidx.media3.common.Format? format, boolean isPlaying, java.util.List<com.google.android.horologist.media3.offload.AudioError> errors, com.google.android.horologist.media3.offload.OffloadTimes offloadTimes, androidx.media3.common.TrackSelectionParameters.AudioOffloadPreferences audioOffloadPreferences);
method public String describe();
method public androidx.media3.common.TrackSelectionParameters.AudioOffloadPreferences getAudioOffloadPreferences();
method public java.util.List<com.google.android.horologist.media3.offload.AudioError> getErrors();
method public androidx.media3.common.Format? getFormat();
method public boolean getOffloadSchedulingEnabled();
method public com.google.android.horologist.media3.offload.OffloadTimes getOffloadTimes();
method public boolean getSleepingForOffload();
method public com.google.android.horologist.media3.offload.AudioOffloadStrategy? getStrategy();
method public String? getStrategyStatus();
method public boolean getTrackOffload();
method public boolean isPlaying();
method public String trackOffloadDescription();
method public com.google.android.horologist.media3.offload.OffloadTimes updateToNow();
property public final androidx.media3.common.TrackSelectionParameters.AudioOffloadPreferences audioOffloadPreferences;
property public final java.util.List<com.google.android.horologist.media3.offload.AudioError> errors;
property public final androidx.media3.common.Format? format;
property public final boolean isPlaying;
property public final boolean offloadSchedulingEnabled;
property public final com.google.android.horologist.media3.offload.OffloadTimes offloadTimes;
property public final boolean sleepingForOffload;
property public final com.google.android.horologist.media3.offload.AudioOffloadStrategy? strategy;
property public final String? strategyStatus;
property public final boolean trackOffload;
field public static final com.google.android.horologist.media3.offload.AudioOffloadStatus.Companion Companion;
}
Expand All @@ -66,33 +63,6 @@ package com.google.android.horologist.media3.offload {
property public final com.google.android.horologist.media3.offload.AudioOffloadStatus Disabled;
}

@com.google.android.horologist.annotations.ExperimentalHorologistApi public interface AudioOffloadStrategy {
method public kotlinx.coroutines.flow.Flow<java.lang.String> applyIndefinitely(androidx.media3.exoplayer.ExoPlayer exoPlayer, com.google.android.horologist.media3.logging.ErrorReporter errorReporter);
method public boolean getOffloadEnabled();
property public abstract boolean offloadEnabled;
}

public static final class AudioOffloadStrategy.Always implements com.google.android.horologist.media3.offload.AudioOffloadStrategy {
method public kotlinx.coroutines.flow.Flow<java.lang.String> applyIndefinitely(androidx.media3.exoplayer.ExoPlayer exoPlayer, com.google.android.horologist.media3.logging.ErrorReporter errorReporter);
method public boolean getOffloadEnabled();
property public boolean offloadEnabled;
field public static final com.google.android.horologist.media3.offload.AudioOffloadStrategy.Always INSTANCE;
}

public static final class AudioOffloadStrategy.Never implements com.google.android.horologist.media3.offload.AudioOffloadStrategy {
method public kotlinx.coroutines.flow.Flow<java.lang.String> applyIndefinitely(androidx.media3.exoplayer.ExoPlayer exoPlayer, com.google.android.horologist.media3.logging.ErrorReporter errorReporter);
method public boolean getOffloadEnabled();
property public boolean offloadEnabled;
field public static final com.google.android.horologist.media3.offload.AudioOffloadStrategy.Never INSTANCE;
}

@com.google.android.horologist.annotations.ExperimentalHorologistApi public final class BackgroundAudioOffloadStrategy implements com.google.android.horologist.media3.offload.AudioOffloadStrategy {
method public kotlinx.coroutines.flow.Flow<java.lang.String> applyIndefinitely(androidx.media3.exoplayer.ExoPlayer exoPlayer, com.google.android.horologist.media3.logging.ErrorReporter errorReporter);
method public boolean getOffloadEnabled();
property public boolean offloadEnabled;
field public static final com.google.android.horologist.media3.offload.BackgroundAudioOffloadStrategy INSTANCE;
}

@com.google.android.horologist.annotations.ExperimentalHorologistApi public final class OffloadTimes {
ctor public OffloadTimes(optional long enabled, optional long disabled, optional long notPlaying, optional boolean isPlaying, optional long updated);
method public long component1();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,22 @@ import android.annotation.SuppressLint
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.media3.common.Format
import androidx.media3.common.TrackSelectionParameters.AudioOffloadPreferences
import androidx.media3.common.TrackSelectionParameters.AudioOffloadPreferences.AUDIO_OFFLOAD_MODE_DISABLED
import androidx.media3.common.TrackSelectionParameters.AudioOffloadPreferences.AUDIO_OFFLOAD_MODE_REQUIRED
import androidx.media3.common.TrackSelectionParameters.AudioOffloadPreferences.DEFAULT
import androidx.media3.exoplayer.DecoderReuseEvaluation
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.analytics.AnalyticsListener
import com.google.android.horologist.annotations.ExperimentalHorologistApi
import com.google.android.horologist.media3.logging.ErrorReporter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.withContext

/**
* Coordination point for audio status, such as format, offload status.
Expand All @@ -46,8 +46,7 @@ import kotlinx.coroutines.withContext
@ExperimentalHorologistApi
public class AudioOffloadManager(
private val errorReporter: ErrorReporter,
private val audioOffloadStrategyFlow: Flow<AudioOffloadStrategy> =
flowOf(BackgroundAudioOffloadStrategy),
private val audioOffloadPreferencesFlow: Flow<AudioOffloadPreferences>,
) {
private val _offloadStatus = MutableStateFlow(
AudioOffloadStatus(
Expand All @@ -58,50 +57,38 @@ public class AudioOffloadManager(
isPlaying = false,
errors = listOf(),
offloadTimes = OffloadTimes(),
strategyStatus = null,
strategy = null,
audioOffloadPreferences = DEFAULT,
),
)
public val offloadStatus: StateFlow<AudioOffloadStatus> = _offloadStatus.asStateFlow()

public val audioOffloadListener: ExoPlayer.AudioOffloadListener =
object : ExoPlayer.AudioOffloadListener {
/**
* Logged when the application changes the state of offload scheduling enabled,
* this is typically only active when the app is in the background.
*/
override fun onExperimentalOffloadSchedulingEnabledChanged(offloadSchedulingEnabled: Boolean) {
_offloadStatus.update {
it.copy(offloadSchedulingEnabled = offloadSchedulingEnabled)
}

errorReporter.logMessage("offload scheduling enabled $offloadSchedulingEnabled")
}

/**
* Logged when the app is able to sleep.
*
* This listener should only run for development builds, since this additional work
* negates the effect of offload.
*/
override fun onExperimentalSleepingForOffloadChanged(sleepingForOffload: Boolean) {
override fun onSleepingForOffloadChanged(isSleepingForOffload: Boolean) {
_offloadStatus.update {
// accumulate playback time for previous state
it.copy(
sleepingForOffload = sleepingForOffload,
sleepingForOffload = isSleepingForOffload,
offloadTimes = it.offloadTimes.timesToNow(
it.sleepingForOffload,
it.isPlaying,
),
)
}

errorReporter.logMessage("sleeping for offload $sleepingForOffload")
errorReporter.logMessage("sleeping for offload $isSleepingForOffload")
}

override fun onExperimentalOffloadedPlayback(offloadedPlayback: Boolean) {
override fun onOffloadedPlayback(isOffloadedPlayback: Boolean) {
_offloadStatus.update {
it.copy(trackOffload = offloadedPlayback)
it.copy(trackOffload = isOffloadedPlayback)
}
}
}
Expand Down Expand Up @@ -163,42 +150,26 @@ public class AudioOffloadManager(
* state and activating it based on App foreground state.
*/
@RequiresApi(29)
public suspend fun connect(exoPlayer: ExoPlayer): Unit = withContext(Dispatchers.Main) {
public suspend fun connect(exoPlayer: ExoPlayer) {
val audioOffloadPreferences = audioOffloadPreferencesFlow.first()

_offloadStatus.value = AudioOffloadStatus(
offloadSchedulingEnabled = false,
sleepingForOffload = exoPlayer.experimentalIsSleepingForOffload(),
sleepingForOffload = exoPlayer.isSleepingForOffload,
trackOffload = false,
format = exoPlayer.audioFormat,
isPlaying = exoPlayer.isPlaying,
errors = listOf(),
offloadTimes = OffloadTimes(),
strategyStatus = null,
strategy = null,
audioOffloadPreferences = audioOffloadPreferences,
)

exoPlayer.addAudioOffloadListener(audioOffloadListener)
exoPlayer.addAnalyticsListener(analyticsListener)

try {
audioOffloadStrategyFlow.collectLatest { strategy ->
_offloadStatus.update {
it.copy(
strategy = strategy,
strategyStatus = null,
)
}

strategy.applyIndefinitely(exoPlayer, errorReporter).collect { strategyStatus ->
_offloadStatus.update {
it.copy(strategyStatus = strategyStatus)
}
}
awaitCancellation()
}
} finally {
exoPlayer.removeAudioOffloadListener(audioOffloadListener)
exoPlayer.removeAnalyticsListener(analyticsListener)
}
exoPlayer.trackSelectionParameters = exoPlayer.trackSelectionParameters.buildUpon()
.setAudioOffloadPreferences(audioOffloadPreferences)
.build()
}

@RequiresApi(29)
Expand All @@ -210,14 +181,19 @@ public class AudioOffloadManager(
}

@RequiresApi(Build.VERSION_CODES.Q)
internal fun printDebugLogs() {
internal suspend fun printDebugLogs() {
val audioOffloadPreferences = audioOffloadPreferencesFlow.first()
val status = _offloadStatus.value
val times = status.updateToNow()
val offloadedPlayback = status.trackOffloadDescription()
val strategy = status.strategy
if (strategy != null && strategy.offloadEnabled != status.trackOffload) {
val requiredOffloadStatus = when (audioOffloadPreferences.audioOffloadMode) {
AUDIO_OFFLOAD_MODE_DISABLED -> false
AUDIO_OFFLOAD_MODE_REQUIRED -> true
else -> null
}
if (requiredOffloadStatus != null && requiredOffloadStatus != status.trackOffload) {
errorReporter.logMessage(
"Offload not matching: $strategy track=$offloadedPlayback",
"Offload not matching: $audioOffloadPreferences track=$offloadedPlayback",
category = ErrorReporter.Category.Playback,
level = ErrorReporter.Level.Error,
)
Expand All @@ -228,7 +204,7 @@ public class AudioOffloadManager(
"audioTrackOffload: $offloadedPlayback " +
"format: ${status.format?.shortDescription} " +
"times: ${times.shortDescription} " +
"strategyStatus: ${status.strategyStatus} ",
"audioOffloadPreferences: ${status.audioOffloadPreferences} ",
category = ErrorReporter.Category.Playback,
)
}
Expand Down
Loading