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

Using test utilities to produce a fake media source which emulates buffering behaviour #1372

Open
sampengilly opened this issue May 14, 2024 · 7 comments
Assignees
Labels

Comments

@sampengilly
Copy link

I'm trying to write some unit tests and using the existing tests as a guide (like this one: https://github.com/androidx/media/blob/d833d59124d795afc146322fe488b2c0d4b9af6a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/analytics/PlaybackStatsListenerTest.java)

I'm trying to test the behaviour of a custom Player.Listener that should react only once for each media item that starts playing. Using FakeMediaSource I can emulate most of the behaviours I'm interested in covering. However one behaviour I'd like to cover is content which buffers part way through.

If I set a real media item on the player from a remote mp3 url and observe the events, I get a tonne of onPlaybackStateChanged and onIsPlayingChanged events as it swaps between buffering and ready.

I'd like to have a test that covers this behaviour but if possible I'd like to do it using local resources rather than my current method of hitting a real mp3 on a remote server.

Is there a way to emulate this using FakeMediaSource or one of its subclasses? Or would I need to put a local mp3 file in my test resources and load that?

@tonihei
Copy link
Collaborator

tonihei commented May 16, 2024

I think this can be controlled by the FakeMediaPeriod.TrackDataFactory that is passed to the FakeMediaSource constructor. It produces the samples put into the queues for playback. By default it's a single sample following the the end-of-stream signal. See here. If you leave out the end-of-stream signal you create partially buffered sources that don't progress beyond a certain point.

If you need to let them load more data at a later point in the test, you'd need to go one level down and inject your own custom FakeSampleStream (by overriding createMediaPeriod and createSampleStream, see this example). FakeSampleStream has an append method to add more samples to the list as needed.

@sampengilly
Copy link
Author

Hmm, it seems that creating a custom FakeMediaSource which overrides the sample stream to omit the end of stream signal, or to introduce more sample items doesn't seem to have an effect in the test. It doesn't appear to trigger the buffering behaviour that is expected.

In this setup the runUntilPlaybackState(player, Player.STATE_BUFFERING) call times out.

val mediaItem = MediaItem.Builder().setMediaId("TEST_ID").build()
val fakeMediaSource = object : FakeMediaSource(timelineForMediaItem(mediaItem, 10.seconds)) {
    override fun createMediaPeriod(
        id: MediaSource.MediaPeriodId,
        trackGroupArray: TrackGroupArray,
        allocator: Allocator,
        mediaSourceEventDispatcher: MediaSourceEventListener.EventDispatcher,
        drmSessionManager: DrmSessionManager,
        drmEventDispatcher: DrmSessionEventListener.EventDispatcher,
        transferListener: TransferListener?
    ): MediaPeriod = object : FakeMediaPeriod(
        trackGroupArray,
        allocator,
        { _, _ -> ImmutableList.of() },
        mediaSourceEventDispatcher,
        drmSessionManager,
        drmEventDispatcher,
        false
    ) {
        override fun createSampleStream(
            allocator: Allocator,
            mediaSourceEventDispatcher: MediaSourceEventListener.EventDispatcher?,
            drmSessionManager: DrmSessionManager,
            drmEventDispatcher: DrmSessionEventListener.EventDispatcher,
            initialFormat: Format,
            fakeSampleStreamItems: MutableList<FakeSampleStream.FakeSampleStreamItem>
        ): FakeSampleStream = FakeSampleStream(
            allocator,
            mediaSourceEventDispatcher,
            drmSessionManager,
            drmEventDispatcher,
            initialFormat,
            listOf(
                FakeSampleStreamItem.oneByteSample(5.seconds.inWholeMicroseconds),
                FakeSampleStreamItem.oneByteSample(5.seconds.inWholeMicroseconds)
            )
        )
    }
}
player.setMediaSource(fakeMediaSource)
player.prepare()
player.play()

TestPlayerRunHelper.playUntilPosition(player, 0, 1.seconds.inWholeMilliseconds)
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_BUFFERING)

@tonihei
Copy link
Collaborator

tonihei commented May 17, 2024

It's not running because

  1. The source doesn't publish any tracks, so the sample stream logic isn't even called. You can fix this by adding a Format to the FakeMediaSource constructor, e.g. FakeMediaSource(timelineForMediaItem(mediaItem, 10.seconds), ExoPlayerTestRunner.VIDEO_FORMAT)
  2. The first sample at least needs to be a keyframe to start playback, which can be set with FakeSampleStreamItem.oneByteSample(5.seconds.inWholeMicroseconds, C.BUFFER_FLAG_KEY_FRAME)

If I make both these changes, the test runs through as expected.

@sampengilly
Copy link
Author

Thanks, I'll give that a try

@sampengilly
Copy link
Author

Hmmm, I'm still not seeing the expected behaviour with those changes:

private class FakeBufferingMediaSource(
    mediaItem: MediaItem
) : FakeMediaSource(timelineForMediaItem(mediaItem, 10.seconds), ExoPlayerTestRunner.VIDEO_FORMAT) {

    override fun createMediaPeriod(
        id: MediaSource.MediaPeriodId,
        trackGroupArray: TrackGroupArray,
        allocator: Allocator,
        mediaSourceEventDispatcher: MediaSourceEventListener.EventDispatcher,
        drmSessionManager: DrmSessionManager,
        drmEventDispatcher: DrmSessionEventListener.EventDispatcher,
        transferListener: TransferListener?
    ): MediaPeriod = object : FakeMediaPeriod(
        trackGroupArray,
        allocator,
        { _, _ -> ImmutableList.of() },
        mediaSourceEventDispatcher,
        drmSessionManager,
        drmEventDispatcher,
        false
    ) {
        override fun createSampleStream(
            allocator: Allocator,
            mediaSourceEventDispatcher: MediaSourceEventListener.EventDispatcher?,
            drmSessionManager: DrmSessionManager,
            drmEventDispatcher: DrmSessionEventListener.EventDispatcher,
            initialFormat: Format,
            fakeSampleStreamItems: MutableList<FakeSampleStreamItem>
        ): FakeSampleStream = FakeSampleStream(
            allocator,
            mediaSourceEventDispatcher,
            drmSessionManager,
            drmEventDispatcher,
            initialFormat,
            listOf(
                FakeSampleStreamItem.oneByteSample(5.seconds.inWholeMicroseconds, C.BUFFER_FLAG_KEY_FRAME),
                FakeSampleStreamItem.oneByteSample(5.seconds.inWholeMicroseconds),
                FakeSampleStreamItem.END_OF_STREAM_ITEM
            )
        )
    }

}

Logs during test output (I have a listener set on the player which prints the different events)
image

The logs I expect to see with content that buffers (from a real remote mp3 media source)
image

With the changes suggested above, removing the END_OF_STREAM_ITEM from the list causes the test to timeout (waiting for STATE_ENDED when it will never come), so clearly the sample list is having some effect here.

@tonihei
Copy link
Collaborator

tonihei commented May 20, 2024

That sounds like working as intended I think. If you add the END_OF_STREAM_ITEM sample, the player will just play these sample and then go the ended state. If you omit END_OF_STREAM_ITEM., then the player will stay in a buffering state and can't make further progress (and in particular never reaching STATE_ENDED).

As per my original comment, you can change the sample stream later with the append method. So you can have a test setup like

// Create player and set up FakeMediaSource without END_OF_STREAM_ITEM
player.prepare();
player.play();
runUntilPlaybackState(player, Player.STATE_BUFFERING);

// Make assertions about your app here that depend on this buffering.

fakeSampleStream.append(END_OF_STREAM_ITEM); // Allow the stream to end.
runUntilPlaybackState(player, Player.STATE_ENDED);

@sampengilly
Copy link
Author

Even using append I'm a bit lost here.

I've updated my FakeBufferingMediaSource to allow for appending sample items (including calling writeData on the sample stream which seems to be needed for things given to the append function to be written).

internal class FakeBufferingMediaSource(
    timeline: Timeline
) : FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT) {

    private lateinit var sampleStream: FakeSampleStream

    fun appendNextSample(item: FakeSampleStreamItem) {
        sampleStream.append(listOf(item))
        sampleStream.writeData(0)
    }

    override fun createMediaPeriod(
        id: MediaSource.MediaPeriodId,
        trackGroupArray: TrackGroupArray,
        allocator: Allocator,
        mediaSourceEventDispatcher: MediaSourceEventListener.EventDispatcher,
        drmSessionManager: DrmSessionManager,
        drmEventDispatcher: DrmSessionEventListener.EventDispatcher,
        transferListener: TransferListener?
    ): MediaPeriod = object : FakeMediaPeriod(
        trackGroupArray,
        allocator,
        { _, _ -> ImmutableList.of() },
        mediaSourceEventDispatcher,
        drmSessionManager,
        drmEventDispatcher,
        false
    ) {
        override fun createSampleStream(
            allocator: Allocator,
            mediaSourceEventDispatcher: MediaSourceEventListener.EventDispatcher?,
            drmSessionManager: DrmSessionManager,
            drmEventDispatcher: DrmSessionEventListener.EventDispatcher,
            initialFormat: Format,
            fakeSampleStreamItems: MutableList<FakeSampleStreamItem>
        ): FakeSampleStream = FakeSampleStream(
            allocator,
            mediaSourceEventDispatcher,
            drmSessionManager,
            drmEventDispatcher,
            initialFormat,
            listOf()
        ).also { sampleStream = it }
    }

}

In my test though, nothing at all happens until an END_OF_STREAM_ITEM is appended.

fun `play event triggered once for content that buffers midway through`() {
    val mediaItem = MediaItem.Builder().setMediaId("TEST_ID").build()
    val source = FakeBufferingMediaSource(
        timeline = timelineForMediaItem(mediaItem, 10.seconds)
    )
    player.setMediaSource(source)
    player.prepare()
    player.play()
    TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player)

    source.appendNextSample(oneByteSample(2.seconds.inWholeMicroseconds, C.BUFFER_FLAG_KEY_FRAME))

//   TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_BUFFERING)
//   source.appendNextSample(oneByteSample(2.seconds.inWholeMicroseconds, C.BUFFER_FLAG_KEY_FRAME))

//    TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_BUFFERING)
//    source.appendNextSample(END_OF_STREAM_ITEM)

    TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED)

    playEvents.map { it.mediaItem } shouldContainExactly listOf(mediaItem)
}

With the END_OF_STREAM_ITEM line commented out, nothing even begins to play, it times out waiting for a STATE_ENDED event that never comes but beyond that there isn't even a STATE_READY event that occurs.
image

If I uncomment just that line appending the END_OF_STREAM_ITEM suddenly the READY state occurs:
image

The other commented lines appending new samples have no effect

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants