/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- * Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ package org.mozilla.geckoview.test import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.MediumTest import org.hamcrest.Matchers.* // ktlint-disable no-wildcard-imports import org.junit.After import org.junit.Assume.assumeThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mozilla.geckoview.GeckoResult import org.mozilla.geckoview.GeckoSession import org.mozilla.geckoview.MediaSession import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.AssertCalled class Metadata( title: String?, artist: String?, album: String?, ) : MediaSession.Metadata(title, artist, album, null) @RunWith(AndroidJUnit4::class) @MediumTest class MediaSessionTest : BaseSessionTest() { companion object { // See MEDIA_SESSION_DOM1_PATH file for details. const val DOM_TEST_TITLE1 = "hoot" const val DOM_TEST_TITLE2 = "hoot2" const val DOM_TEST_TITLE3 = "hoot3" const val DOM_TEST_ARTIST1 = "owl" const val DOM_TEST_ARTIST2 = "stillowl" const val DOM_TEST_ARTIST3 = "immaowl" const val DOM_TEST_ALBUM1 = "hoots" const val DOM_TEST_ALBUM2 = "dahoots" const val DOM_TEST_ALBUM3 = "mahoots" const val DEFAULT_TEST_TITLE1 = "MediaSessionDefaultTest1" const val TEST_DURATION1 = 3.34 const val WEBM_TEST_DURATION = 5.59 const val WEBM_TEST_WIDTH = 560L const val WEBM_TEST_HEIGHT = 320L val DOM_META = arrayOf( Metadata( DOM_TEST_TITLE1, DOM_TEST_ARTIST1, DOM_TEST_ALBUM1, ), Metadata( DOM_TEST_TITLE2, DOM_TEST_ARTIST2, DOM_TEST_ALBUM2, ), Metadata( DOM_TEST_TITLE3, DOM_TEST_ARTIST3, DOM_TEST_ALBUM3, ), ) } @Before fun setup() { sessionRule.setPrefsUntilTestEnd( mapOf( "media.mediacontrol.stopcontrol.aftermediaends" to false, ), ) } @After fun teardown() { } @Test fun domMetadataPlayback() { // TODO: needs bug 1700243 assumeThat(sessionRule.env.isIsolatedProcess, equalTo(false)) val onActivatedCalled = arrayOf(GeckoResult()) val onMetadataCalled = arrayOf( GeckoResult(), GeckoResult(), GeckoResult(), GeckoResult(), GeckoResult(), ) val onPlayCalled = arrayOf( GeckoResult(), GeckoResult(), GeckoResult(), GeckoResult(), GeckoResult(), GeckoResult(), ) val onPauseCalled = arrayOf( GeckoResult(), GeckoResult(), GeckoResult(), GeckoResult(), GeckoResult(), GeckoResult(), ) // Test: // 1. Load DOM Media Session page which contains 3 audio tracks. // 2. Track 1 is played on page load. // a. Ensure onActivated is called. // b. Ensure onMetadata (1) is called. // c. Ensure onPlay (1) is called. val completedStep2 = GeckoResult.allOf( onActivatedCalled[0], onMetadataCalled[0], onPlayCalled[0], ) // 3. Pause playback of track 1. // a. Ensure onPause (1) is called. val completedStep3 = GeckoResult.allOf( onPauseCalled[0], ) // 4. Resume playback (1). // a. Ensure onMetadata (1) is called. // b. Ensure onPlay (1) is called. val completedStep4 = GeckoResult.allOf( onPlayCalled[1], onMetadataCalled[1], ) // 5. Wait for track 1 end. // a. Ensure onPause (1) is called. val completedStep5 = GeckoResult.allOf( onPauseCalled[1], ) // 6. Play next track (2). // a. Ensure onMetadata (2) is called. // b. Ensure onPlay (2) is called. val completedStep6 = GeckoResult.allOf( onMetadataCalled[2], onPlayCalled[2], ) // 7. Play next track (3). // a. Ensure onPause (2) is called. // b. Ensure onMetadata (3) is called. // c. Ensure onPlay (3) is called. val completedStep7 = GeckoResult.allOf( onPauseCalled[2], onMetadataCalled[3], onPlayCalled[3], ) // 8. Play previous track (2). // a. Ensure onPause (3) is called. // b. Ensure onMetadata (2) is called. // c. Ensure onPlay (2) is called. val completedStep8a = GeckoResult.allOf( onPauseCalled[3], ) // Without the split, this seems to race and we don't get the pause event. val completedStep8b = GeckoResult.allOf( onMetadataCalled[4], onPlayCalled[4], ) // 9. Wait for track 2 end. // a. Ensure onPause (2) is called. val completedStep9 = GeckoResult.allOf( onPauseCalled[4], ) val path = MEDIA_SESSION_DOM1_PATH val session1 = sessionRule.createOpenSession() var mediaSession1: MediaSession? = null // 1. session1.loadTestPath(path) session1.delegateUntilTestEnd(object : MediaSession.Delegate { @AssertCalled(count = 1, order = [1]) override fun onActivated( session: GeckoSession, mediaSession: MediaSession, ) { onActivatedCalled[0].complete(null) mediaSession1 = mediaSession } @AssertCalled(false) override fun onDeactivated( session: GeckoSession, mediaSession: MediaSession, ) { } @AssertCalled override fun onFeatures( session: GeckoSession, mediaSession: MediaSession, features: Long, ) { val play = (features and MediaSession.Feature.PLAY) != 0L val pause = (features and MediaSession.Feature.PAUSE) != 0L val stop = (features and MediaSession.Feature.PAUSE) != 0L val next = (features and MediaSession.Feature.PAUSE) != 0L val prev = (features and MediaSession.Feature.PAUSE) != 0L assertThat( "Playback constrols should be supported", play && pause && stop && next && prev, equalTo(true), ) } @AssertCalled(count = 5, order = [2]) override fun onMetadata( session: GeckoSession, mediaSession: MediaSession, meta: MediaSession.Metadata, ) { assertThat( "Title should match", meta.title, equalTo( forEachCall( DOM_META[0].title, DOM_META[0].title, DOM_META[1].title, DOM_META[2].title, DOM_META[1].title, ), ), ) assertThat( "Artist should match", meta.artist, equalTo( forEachCall( DOM_META[0].artist, DOM_META[0].artist, DOM_META[1].artist, DOM_META[2].artist, DOM_META[1].artist, ), ), ) assertThat( "Album should match", meta.album, equalTo( forEachCall( DOM_META[0].album, DOM_META[0].album, DOM_META[1].album, DOM_META[2].album, DOM_META[1].album, ), ), ) assertThat( "Artwork image should be non-null", meta.artwork!!.getBitmap(200), notNullValue(), ) onMetadataCalled[sessionRule.currentCall.counter - 1] .complete(null) } @AssertCalled override fun onPositionState( session: GeckoSession, mediaSession: MediaSession, state: MediaSession.PositionState, ) { assertThat( "Duration should match", state.duration, closeTo(TEST_DURATION1, 0.01), ) assertThat( "Playback rate should match", state.playbackRate, closeTo(1.0, 0.01), ) assertThat( "Position should be >= 0", state.position, greaterThanOrEqualTo(0.0), ) } @AssertCalled(count = 5, order = [2]) override fun onPlay( session: GeckoSession, mediaSession: MediaSession, ) { onPlayCalled[sessionRule.currentCall.counter - 1] .complete(null) } @AssertCalled(count = 5) override fun onPause( session: GeckoSession, mediaSession: MediaSession, ) { onPauseCalled[sessionRule.currentCall.counter - 1] .complete(null) } }) sessionRule.waitForResult(completedStep2) mediaSession1!!.pause() sessionRule.waitForResult(completedStep3) mediaSession1!!.play() sessionRule.waitForResult(completedStep4) sessionRule.waitForResult(completedStep5) mediaSession1!!.pause() mediaSession1!!.nextTrack() mediaSession1!!.play() sessionRule.waitForResult(completedStep6) mediaSession1!!.pause() mediaSession1!!.nextTrack() mediaSession1!!.play() sessionRule.waitForResult(completedStep7) mediaSession1!!.pause() sessionRule.waitForResult(completedStep8a) mediaSession1!!.previousTrack() mediaSession1!!.play() sessionRule.waitForResult(completedStep8b) sessionRule.waitForResult(completedStep9) } @Test fun defaultMetadataPlayback() { // TODO: needs bug 1700243 assumeThat(sessionRule.env.isIsolatedProcess, equalTo(false)) val onActivatedCalled = arrayOf(GeckoResult()) val onPlayCalled = arrayOf( GeckoResult(), GeckoResult(), GeckoResult(), GeckoResult(), GeckoResult(), GeckoResult(), ) val onPauseCalled = arrayOf( GeckoResult(), GeckoResult(), GeckoResult(), GeckoResult(), GeckoResult(), GeckoResult(), ) // Test: // 1. Load Media Session page which contains 1 audio track. // 2. Track 1 is played on page load. // a. Ensure onActivated is called. // b. Ensure onPlay (1) is called. val completedStep2 = GeckoResult.allOf( onActivatedCalled[0], onPlayCalled[0], ) // 3. Pause playback of track 1. // a. Ensure onPause (1) is called. val completedStep3 = GeckoResult.allOf( onPauseCalled[0], ) // 4. Resume playback (1). // b. Ensure onPlay (1) is called. val completedStep4 = GeckoResult.allOf( onPlayCalled[1], ) // 5. Wait for track 1 end. // a. Ensure onPause (1) is called. val completedStep5 = GeckoResult.allOf( onPauseCalled[1], ) val path = MEDIA_SESSION_DEFAULT1_PATH val session1 = sessionRule.createOpenSession() var mediaSession1: MediaSession? = null // 1. session1.loadTestPath(path) session1.delegateUntilTestEnd(object : MediaSession.Delegate { @AssertCalled(count = 1, order = [1]) override fun onActivated( session: GeckoSession, mediaSession: MediaSession, ) { onActivatedCalled[0].complete(null) mediaSession1 = mediaSession } @AssertCalled(count = 2, order = [2]) override fun onPlay( session: GeckoSession, mediaSession: MediaSession, ) { onPlayCalled[sessionRule.currentCall.counter - 1] .complete(null) } @AssertCalled(count = 2) override fun onPause( session: GeckoSession, mediaSession: MediaSession, ) { onPauseCalled[sessionRule.currentCall.counter - 1] .complete(null) } }) sessionRule.waitForResult(completedStep2) mediaSession1!!.pause() sessionRule.waitForResult(completedStep3) mediaSession1!!.play() sessionRule.waitForResult(completedStep4) sessionRule.waitForResult(completedStep5) } @Test fun domMultiSessions() { // TODO: needs bug 1700243 assumeThat(sessionRule.env.isIsolatedProcess, equalTo(false)) val onActivatedCalled = arrayOf( arrayOf(GeckoResult()), arrayOf(GeckoResult()), ) val onMetadataCalled = arrayOf( arrayOf( GeckoResult(), GeckoResult(), GeckoResult(), GeckoResult(), GeckoResult(), ), arrayOf( GeckoResult(), GeckoResult(), GeckoResult(), GeckoResult(), GeckoResult(), ), ) val onPlayCalled = arrayOf( arrayOf( GeckoResult(), GeckoResult(), GeckoResult(), GeckoResult(), GeckoResult(), ), arrayOf( GeckoResult(), GeckoResult(), GeckoResult(), GeckoResult(), GeckoResult(), ), ) val onPauseCalled = arrayOf( arrayOf( GeckoResult(), GeckoResult(), GeckoResult(), GeckoResult(), GeckoResult(), ), arrayOf( GeckoResult(), GeckoResult(), GeckoResult(), GeckoResult(), GeckoResult(), ), ) // Test: // 1. Session1: Load DOM Media Session page with 3 audio tracks. // 2. Session1: Track 1 is played on page load. // a. Session1: Ensure onActivated is called. // b. Session1: Ensure onMetadata (1) is called. // c. Session1: Ensure onPlay (1) is called. // d. Session1: Verify isActive. val completedStep2 = GeckoResult.allOf( onActivatedCalled[0][0], onMetadataCalled[0][0], onPlayCalled[0][0], ) // 3. Session1: Pause playback of track 1. // a. Session1: Ensure onPause (1) is called. val completedStep3 = GeckoResult.allOf( onPauseCalled[0][0], ) // 4. Session2: Load DOM Media Session page with 3 audio tracks. // 5. Session2: Track 1 is played on page load. // a. Session2: Ensure onActivated is called. // b. Session2: Ensure onMetadata (1) is called. // c. Session2: Ensure onPlay (1) is called. // d. Session2: Verify isActive. val completedStep5 = GeckoResult.allOf( onActivatedCalled[1][0], onMetadataCalled[1][0], onPlayCalled[1][0], ) // 6. Session2: Pause playback of track 1. // a. Session2: Ensure onPause (1) is called. val completedStep6 = GeckoResult.allOf( onPauseCalled[1][0], ) // 7. Session1: Play next track (2). // a. Session1: Ensure onMetadata (2) is called. // b. Session1: Ensure onPlay (2) is called. val completedStep7 = GeckoResult.allOf( onMetadataCalled[0][1], onPlayCalled[0][1], ) // 8. Session1: wait for track 1 end. // a. Ensure onPause (1) is called. val completedStep8 = GeckoResult.allOf( onPauseCalled[0][1], ) val path = MEDIA_SESSION_DOM1_PATH val session1 = sessionRule.createOpenSession() val session2 = sessionRule.createOpenSession() var mediaSession1: MediaSession? = null var mediaSession2: MediaSession? = null session1.delegateUntilTestEnd(object : MediaSession.Delegate { @AssertCalled(count = 1) override fun onActivated( session: GeckoSession, mediaSession: MediaSession, ) { onActivatedCalled[0][sessionRule.currentCall.counter - 1] .complete(null) mediaSession1 = mediaSession assertThat( "Should be active", mediaSession1?.isActive, equalTo(true), ) } @AssertCalled override fun onPositionState( session: GeckoSession, mediaSession: MediaSession, state: MediaSession.PositionState, ) { assertThat( "Duration should match", state.duration, closeTo(TEST_DURATION1, 0.01), ) assertThat( "Playback rate should match", state.playbackRate, closeTo(1.0, 0.01), ) assertThat( "Position should be >= 0", state.position, greaterThanOrEqualTo(0.0), ) } @AssertCalled override fun onFeatures( session: GeckoSession, mediaSession: MediaSession, features: Long, ) { val play = (features and MediaSession.Feature.PLAY) != 0L val pause = (features and MediaSession.Feature.PAUSE) != 0L val stop = (features and MediaSession.Feature.PAUSE) != 0L val next = (features and MediaSession.Feature.PAUSE) != 0L val prev = (features and MediaSession.Feature.PAUSE) != 0L assertThat( "Playback constrols should be supported", play && pause && stop && next && prev, equalTo(true), ) } @AssertCalled override fun onMetadata( session: GeckoSession, mediaSession: MediaSession, meta: MediaSession.Metadata, ) { val count = sessionRule.currentCall.counter if (count < 3) { // Ignore redundant calls. onMetadataCalled[0][count - 1].complete(null) } assertThat( "Title should match", meta.title, equalTo( forEachCall( DOM_META[0].title, DOM_META[1].title, ), ), ) assertThat( "Artist should match", meta.artist, equalTo( forEachCall( DOM_META[0].artist, DOM_META[1].artist, ), ), ) assertThat( "Album should match", meta.album, equalTo( forEachCall( DOM_META[0].album, DOM_META[1].album, ), ), ) assertThat( "Artwork image should be non-null", meta.artwork!!.getBitmap(200), notNullValue(), ) } @AssertCalled(count = 2) override fun onPlay( session: GeckoSession, mediaSession: MediaSession, ) { onPlayCalled[0][sessionRule.currentCall.counter - 1] .complete(null) } @AssertCalled(count = 2) override fun onPause( session: GeckoSession, mediaSession: MediaSession, ) { onPauseCalled[0][sessionRule.currentCall.counter - 1] .complete(null) } }) session2.delegateUntilTestEnd(object : MediaSession.Delegate { @AssertCalled(count = 1) override fun onActivated( session: GeckoSession, mediaSession: MediaSession, ) { onActivatedCalled[1][sessionRule.currentCall.counter - 1] .complete(null) mediaSession2 = mediaSession assertThat( "Should be active", mediaSession1!!.isActive, equalTo(true), ) assertThat( "Should be active", mediaSession2!!.isActive, equalTo(true), ) } @AssertCalled override fun onMetadata( session: GeckoSession, mediaSession: MediaSession, meta: MediaSession.Metadata, ) { val count = sessionRule.currentCall.counter if (count < 2) { // Ignore redundant calls. onMetadataCalled[1][0].complete(null) } assertThat( "Title should match", meta.title, equalTo( forEachCall( DOM_META[0].title, ), ), ) assertThat( "Artist should match", meta.artist, equalTo( forEachCall( DOM_META[0].artist, ), ), ) assertThat( "Album should match", meta.album, equalTo( forEachCall( DOM_META[0].album, ), ), ) } @AssertCalled(count = 1) override fun onPlay( session: GeckoSession, mediaSession: MediaSession, ) { onPlayCalled[1][sessionRule.currentCall.counter - 1] .complete(null) } @AssertCalled(count = 1) override fun onPause( session: GeckoSession, mediaSession: MediaSession, ) { onPauseCalled[1][sessionRule.currentCall.counter - 1] .complete(null) } }) session1.loadTestPath(path) sessionRule.waitForResult(completedStep2) mediaSession1!!.pause() sessionRule.waitForResult(completedStep3) session2.loadTestPath(path) sessionRule.waitForResult(completedStep5) mediaSession2!!.pause() sessionRule.waitForResult(completedStep6) mediaSession1!!.pause() mediaSession1!!.nextTrack() mediaSession1!!.play() sessionRule.waitForResult(completedStep7) sessionRule.waitForResult(completedStep8) } @Test fun fullscreenVideoElementMetadata() { // TODO: bug 1810736 assumeThat(sessionRule.env.isIsolatedProcess, equalTo(false)) sessionRule.setPrefsUntilTestEnd( mapOf( "media.autoplay.default" to 0, "full-screen-api.allow-trusted-requests-only" to false, ), ) val onActivatedCalled = GeckoResult() val onPlayCalled = GeckoResult() val onPauseCalled = GeckoResult() val onFullscreenCalled = arrayOf( GeckoResult(), GeckoResult(), ) // Test: // 1. Load video test page which contains 1 video element. // a. Ensure page has loaded. // 2. Play video element. // a. Ensure onActivated is called. // b. Ensure onPlay is called. val completedStep2 = GeckoResult.allOf( onActivatedCalled, onPlayCalled, ) // 3. Enter fullscreen of the video. // a. Ensure onFullscreen is called. val completedStep3 = GeckoResult.allOf( onFullscreenCalled[0], ) // 4. Exit fullscreen of the video. // a. Ensure onFullscreen is called. val completedStep4 = GeckoResult.allOf( onFullscreenCalled[1], ) // 5. Pause the video. // a. Ensure onPause is called. val completedStep5 = GeckoResult.allOf( onPauseCalled, ) var mediaSession1: MediaSession? = null val path = VIDEO_WEBM_PATH val session1 = sessionRule.createOpenSession() session1.delegateUntilTestEnd(object : MediaSession.Delegate { @AssertCalled(count = 1, order = [1]) override fun onActivated( session: GeckoSession, mediaSession: MediaSession, ) { mediaSession1 = mediaSession onActivatedCalled.complete(null) assertThat( "Should be active", mediaSession.isActive, equalTo(true), ) } @AssertCalled(count = 1, order = [2]) override fun onPlay( session: GeckoSession, mediaSession: MediaSession, ) { onPlayCalled.complete(null) } @AssertCalled(count = 1) override fun onPause( session: GeckoSession, mediaSession: MediaSession, ) { onPauseCalled.complete(null) } @AssertCalled(count = 2) override fun onFullscreen( session: GeckoSession, mediaSession: MediaSession, enabled: Boolean, meta: MediaSession.ElementMetadata?, ) { if (sessionRule.currentCall.counter == 1) { assertThat( "Fullscreen should be enabled", enabled, equalTo(true), ) assertThat( "Element metadata should exist", meta, notNullValue(), ) assertThat( "Duration should match", meta!!.duration, closeTo(WEBM_TEST_DURATION, 0.01), ) assertThat( "Width should match", meta.width, equalTo(WEBM_TEST_WIDTH), ) assertThat( "Height should match", meta.height, equalTo(WEBM_TEST_HEIGHT), ) assertThat( "Audio track count should match", meta.audioTrackCount, equalTo(1), ) assertThat( "Video track count should match", meta.videoTrackCount, equalTo(1), ) } else { assertThat( "Fullscreen should be disabled", enabled, equalTo(false), ) } onFullscreenCalled[sessionRule.currentCall.counter - 1] .complete(null) } }) // 1. session1.loadTestPath(path) sessionRule.waitForPageStop() // 2. session1.evaluateJS("document.querySelector('video').play()") sessionRule.waitForResult(completedStep2) // 3. session1.evaluateJS( "document.querySelector('video').requestFullscreen()", ) sessionRule.waitForResult(completedStep3) // 4. session1.evaluateJS("document.exitFullscreen()") sessionRule.waitForResult(completedStep4) // 5. mediaSession1!!.pause() sessionRule.waitForResult(completedStep5) } @Test fun fullscreenVideoWithActivated() { // TODO: bug 1810736 assumeThat(sessionRule.env.isIsolatedProcess, equalTo(false)) sessionRule.setPrefsUntilTestEnd( mapOf( "media.autoplay.default" to 0, "full-screen-api.allow-trusted-requests-only" to false, ), ) val path = VIDEO_WEBM_PATH val session = sessionRule.createOpenSession() val resultFullscreen = GeckoResult() session.loadTestPath(path) sessionRule.waitForPageStop() session.delegateDuringNextWait(object : MediaSession.Delegate { override fun onFullscreen( session: GeckoSession, mediaSession: MediaSession, enabled: Boolean, meta: MediaSession.ElementMetadata?, ) { assertThat( "Fullscreen should be enabled", enabled, equalTo(true), ) assertThat( "Element metadata should exist", meta, notNullValue(), ) resultFullscreen.complete(null) } }) session.evaluateJS("document.querySelector('video').requestFullscreen()") sessionRule.waitForResult(resultFullscreen) } @Test fun switchingProcess() { // TODO: bug 1810736 assumeThat(sessionRule.env.isIsolatedProcess, equalTo(false)) sessionRule.setPrefsUntilTestEnd( mapOf( "media.autoplay.default" to 0, ), ) mainSession.loadUri("about:blank") sessionRule.waitForPageStop() mainSession.loadTestPath(VIDEO_WEBM_PATH) sessionRule.waitForPageStop() val onPlayCalled = GeckoResult() mainSession.delegateUntilTestEnd(object : MediaSession.Delegate { @AssertCalled(count = 1) override fun onPlay( session: GeckoSession, mediaSession: MediaSession, ) { onPlayCalled.complete(null) } }) mainSession.evaluateJS("document.querySelector('video').play()") sessionRule.waitForResult(onPlayCalled) } }