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
Expand Up @@ -32,7 +32,6 @@ import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.Lifecycle
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import io.putdotio.android.design.PutioTheme
Expand All @@ -43,16 +42,15 @@ import io.putdotio.android.playback.MOBILE_PLAYER_SIDE_SHEET_TAG
import io.putdotio.android.playback.PlaybackAudioTrack
import io.putdotio.android.playback.PlaybackSubtitleTrack
import io.putdotio.android.playback.languageName
import io.putdotio.android.playback.DefaultMobilePlayerFactory
import io.putdotio.android.playback.MobilePlayerFactory
import io.putdotio.android.playback.MobilePlayerScreen
import io.putdotio.android.playback.PlaybackContent
import io.putdotio.android.playback.PlaybackMediaType
import io.putdotio.android.playback.PlaybackState
import io.putdotio.android.playback.PlaybackTarget
import io.putdotio.android.playback.SubtitleTrackIdentity
import io.putdotio.android.playback.audioAttributes
import io.putdotio.android.playback.playbackAudioTracks
import io.putdotio.android.playback.playbackRenderersFactory
import io.putdotio.android.playback.playbackState
import io.putdotio.android.playback.playbackSubtitleTracks
import io.putdotio.sdk.files.PlaybackSource
Expand Down Expand Up @@ -359,19 +357,14 @@ private class FullscreenProofPlayerFactory : MobilePlayerFactory {
var renderedFrame = false
private set
fun current(): Player = checkNotNull(player)
// The production player streams through the download cache's HTTP source, which cannot read
// the caller's local file; this one keeps its renderers and audio attributes.
override fun create(context: Context, mediaType: PlaybackMediaType): Player =
ExoPlayer.Builder(context, playbackRenderersFactory(context))
.setAudioAttributes(mediaType.audioAttributes(), true)
.build()
.also {
player = it
renderedFrame = false
it.addListener(object : Player.Listener {
override fun onRenderedFirstFrame() { renderedFrame = true }
})
}
DefaultMobilePlayerFactory.create(context, mediaType).also {
player = it
renderedFrame = false
it.addListener(object : Player.Listener {
override fun onRenderedFirstFrame() { renderedFrame = true }
})
}
// This private-video proof must not stop an unrelated real audio session.
override fun stopAudio(context: Context) = Unit
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,19 @@ import androidx.lifecycle.Lifecycle
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import io.putdotio.android.design.PutioTheme
import io.putdotio.android.files.FilesItemId
import io.putdotio.android.playback.DefaultMobilePlayerFactory
import io.putdotio.android.playback.MobilePlayerFactory
import io.putdotio.android.playback.MobilePlayerScreen
import io.putdotio.android.playback.PlaybackContent
import io.putdotio.android.playback.PlaybackMediaType
import io.putdotio.android.playback.PlaybackPositionObserver
import io.putdotio.android.playback.PlaybackTarget
import io.putdotio.android.playback.SubtitleStartupPolicy
import io.putdotio.android.playback.audioAttributes
import io.putdotio.android.playback.playbackRenderersFactory
import io.putdotio.android.playback.playbackState
import io.putdotio.android.playback.withReportingLease
import io.putdotio.sdk.files.PlaybackSource
Expand Down Expand Up @@ -357,10 +355,7 @@ class MobilePictureInPictureProofTest {

private class MenuButtons(val action: Rect, val expand: Rect, val close: Rect)

/**
* Production renderers and audio attributes over a local file, which the production player's
* download-cache HTTP source cannot read; positions go through the real observer to a recorder.
*/
/** The production player over a local file; positions go through the real observer to a recorder. */
private class PictureInPictureProofPlayerFactory(private val lease: String) : MobilePlayerFactory {
var player: Player? = null
private set
Expand All @@ -373,21 +368,18 @@ private class PictureInPictureProofPlayerFactory(private val lease: String) : Mo
fun current(): Player = checkNotNull(player)

override fun create(context: Context, mediaType: PlaybackMediaType): Player =
ExoPlayer.Builder(context, playbackRenderersFactory(context))
.setAudioAttributes(mediaType.audioAttributes(), true)
.build()
.also {
created += 1
player = it
renderedFrame = false
it.addListener(
object : Player.Listener {
override fun onRenderedFirstFrame() {
renderedFrame = true
}
},
)
}
DefaultMobilePlayerFactory.create(context, mediaType).also {
created += 1
player = it
renderedFrame = false
it.addListener(
object : Player.Listener {
override fun onRenderedFirstFrame() {
renderedFrame = true
}
},
)
}

override fun reportableItem(item: MediaItem, useStartFrom: Boolean): MediaItem = item.withReportingLease(lease)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onAllNodesWithTag
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.click
Expand Down Expand Up @@ -202,9 +203,10 @@ class MobilePlaybackOptionsProofTest {
openSpeedOptions()
val choice = compose.onNodeWithText("1.25×")
val instrumentation = InstrumentationRegistry.getInstrumentation()
for (ignored in 0 until 8) {
if (choice.fetchSemanticsNode().config.getOrElse(SemanticsProperties.Focused) { false }) break
instrumentation.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_DOWN)
// Focus opens on the speed track, which takes the arrow keys; Tab moves on to the quick speeds.
for (ignored in 0 until 12) {
if (focused("1.25×")) break
instrumentation.sendKeyDownUpSync(KeyEvent.KEYCODE_TAB)
compose.waitForIdle()
}
choice.assertIsFocused()
Expand All @@ -214,6 +216,10 @@ class MobilePlaybackOptionsProofTest {
}
}

private fun focused(label: String) = compose.onAllNodesWithText(label).fetchSemanticsNodes().any {
it.config.getOrElse(SemanticsProperties.Focused) { false }
}

/** The audio choices: the video's Audio side sheet, or Now Playing's sheet under More. */
private fun showOptions() {
showControls()
Expand Down Expand Up @@ -268,8 +274,11 @@ class MobilePlaybackOptionsProofTest {
} else {
hasContentDescription("Playback speed ", substring = true)
}
if (compose.onAllNodes(trigger).fetchSemanticsNodes().isEmpty()) {
// Controls that are fading out still exist but aren't displayed; a tap brings them back.
repeat(3) {
if (runCatching { compose.onNode(trigger).assertIsDisplayed() }.isSuccess) return
compose.onNodeWithTag(MOBILE_PLAYER_GESTURE_TAG).performTouchInput { click() }
compose.waitForIdle()
}
compose.onNode(trigger).assertIsDisplayed()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.core.content.ContextCompat
import androidx.media3.common.MediaItem
import androidx.media3.common.Player as Media3Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DefaultDataSource
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.session.MediaController
Expand Down Expand Up @@ -109,9 +110,15 @@ internal object DefaultMobilePlayerFactory : MobilePlayerFactory {
): Media3Player {
return ExoPlayer.Builder(context, playbackRenderersFactory(context))
// Completed downloads play from the cache; everything else streams through the same source.
// Local schemes such as file:// bypass it; network URLs still go through the cache.
.setMediaSourceFactory(
DefaultMediaSourceFactory(
MobileDownloadCache.get(context).let { it.playbackFactory(it.activeUserId ?: NO_DOWNLOAD_USER) },
DefaultDataSource.Factory(
context,
MobileDownloadCache.get(context).let {
it.playbackFactory(it.activeUserId ?: NO_DOWNLOAD_USER)
},
),
),
)
.setAudioAttributes(mediaType.audioAttributes(), true)
Expand Down