diff --git a/mobile/android/geckoview/src/androidTest/assets/www/badVideoPath.html b/mobile/android/geckoview/src/androidTest/assets/www/badVideoPath.html new file mode 100644 index 000000000000..d8f21a1263c2 --- /dev/null +++ b/mobile/android/geckoview/src/androidTest/assets/www/badVideoPath.html @@ -0,0 +1,8 @@ + + Bad Video Path + + + + diff --git a/mobile/android/geckoview/src/androidTest/assets/www/mp4.html b/mobile/android/geckoview/src/androidTest/assets/www/mp4.html new file mode 100644 index 000000000000..d775f6728962 --- /dev/null +++ b/mobile/android/geckoview/src/androidTest/assets/www/mp4.html @@ -0,0 +1,8 @@ + + MP4 Video + + + + diff --git a/mobile/android/geckoview/src/androidTest/assets/www/ogg.html b/mobile/android/geckoview/src/androidTest/assets/www/ogg.html new file mode 100644 index 000000000000..cb840639c65c --- /dev/null +++ b/mobile/android/geckoview/src/androidTest/assets/www/ogg.html @@ -0,0 +1,8 @@ + + OGG Video + + + + diff --git a/mobile/android/geckoview/src/androidTest/assets/www/videos/gizmo.webm b/mobile/android/geckoview/src/androidTest/assets/www/videos/gizmo.webm new file mode 100644 index 000000000000..518531a93f5c Binary files /dev/null and b/mobile/android/geckoview/src/androidTest/assets/www/videos/gizmo.webm differ diff --git a/mobile/android/geckoview/src/androidTest/assets/www/videos/short.mp4 b/mobile/android/geckoview/src/androidTest/assets/www/videos/short.mp4 new file mode 100644 index 000000000000..a674b7eb6808 Binary files /dev/null and b/mobile/android/geckoview/src/androidTest/assets/www/videos/short.mp4 differ diff --git a/mobile/android/geckoview/src/androidTest/assets/www/videos/video.ogg b/mobile/android/geckoview/src/androidTest/assets/www/videos/video.ogg new file mode 100644 index 000000000000..ac7ece3519ac Binary files /dev/null and b/mobile/android/geckoview/src/androidTest/assets/www/videos/video.ogg differ diff --git a/mobile/android/geckoview/src/androidTest/assets/www/webm.html b/mobile/android/geckoview/src/androidTest/assets/www/webm.html new file mode 100644 index 000000000000..d57aba250169 --- /dev/null +++ b/mobile/android/geckoview/src/androidTest/assets/www/webm.html @@ -0,0 +1,8 @@ + + WebM Video + + + + diff --git a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/BaseSessionTest.kt b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/BaseSessionTest.kt index 4e30c1cec123..3f3a9a84b7cb 100644 --- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/BaseSessionTest.kt +++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/BaseSessionTest.kt @@ -41,6 +41,10 @@ open class BaseSessionTest(noErrorCollector: Boolean = false) { const val SAVE_STATE_PATH = "/assets/www/saveState.html" const val TITLE_CHANGE_HTML_PATH = "/assets/www/titleChange.html" const val TRACKERS_PATH = "/assets/www/trackers.html" + const val VIDEO_OGG_PATH = "/assets/www/ogg.html" + const val VIDEO_MP4_PATH = "/assets/www/mp4.html" + const val VIDEO_WEBM_PATH = "/assets/www/webm.html" + const val VIDEO_BAD_PATH = "/assets/www/badVideoPath.html" const val UNKNOWN_HOST_URI = "http://www.test.invalid/" const val FULLSCREEN_PATH = "/assets/www/fullscreen.html" } diff --git a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/MediaElementTest.kt b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/MediaElementTest.kt new file mode 100644 index 000000000000..7a2ce72afaa1 --- /dev/null +++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/MediaElementTest.kt @@ -0,0 +1,413 @@ +/* -*- 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 org.mozilla.geckoview.GeckoSession +import org.mozilla.geckoview.MediaElement +import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.AssertCalled +import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.TimeoutMillis +import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.WithDevToolsAPI +import org.mozilla.geckoview.test.util.Callbacks + +import android.support.test.filters.MediumTest +import android.support.test.runner.AndroidJUnit4 +import org.hamcrest.Matchers.* +import org.junit.Assume.assumeThat +import org.junit.Ignore +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@TimeoutMillis(45000) +@MediumTest +class MediaElementTest : BaseSessionTest() { + + interface MediaElementDelegate : MediaElement.Delegate { + override fun onPlaybackStateChange(mediaElement: MediaElement, mediaState: Int) {} + override fun onReadyStateChange(mediaElement: MediaElement, readyState: Int) {} + override fun onMetadataChange(mediaElement: MediaElement, metaData: MediaElement.Metadata) {} + override fun onLoadProgress(mediaElement: MediaElement, progressInfo: MediaElement.LoadProgressInfo) {} + override fun onVolumeChange(mediaElement: MediaElement, volume: Double, muted: Boolean) {} + override fun onTimeChange(mediaElement: MediaElement, time: Double) {} + override fun onPlaybackRateChange(mediaElement: MediaElement, rate: Double) {} + override fun onFullscreenChange(mediaElement: MediaElement, fullscreen: Boolean) {} + override fun onError(mediaElement: MediaElement, errorCode: Int) {} + } + + private fun setupPrefsAndDelegates(path: String) { + sessionRule.setPrefsUntilTestEnd(mapOf( + "media.autoplay.enabled.user-gestures-needed" to false, + "media.autoplay.default" to 0, + "media.autoplay.ask-permission" to false, + "full-screen-api.allow-trusted-requests-only" to false)) + + sessionRule.session.loadTestPath(path) + sessionRule.waitUntilCalled(object : Callbacks.MediaDelegate { + @AssertCalled + override fun onMediaAdd(session: GeckoSession, element: MediaElement) { + sessionRule.addExternalDelegateUntilTestEnd( + MediaElementDelegate::class, + element::setDelegate, + { element.delegate = null }, + object : MediaElementDelegate {}) + } + }) + } + + private fun waitUntilVideoReady(path: String, waitState: Int = MediaElement.MEDIA_READY_STATE_HAVE_ENOUGH_DATA): MediaElement { + setupPrefsAndDelegates(path) + var ready = false + var result: MediaElement? = null + while (!ready) { + sessionRule.waitUntilCalled(object : MediaElementDelegate { + @AssertCalled + override fun onReadyStateChange(mediaElement: MediaElement, readyState: Int) { + if (readyState == waitState) { + ready = true + result = mediaElement + } + } + }) + } + if (result == null) { + throw IllegalStateException("No MediaElement Found") + } + return result!! + } + + private fun waitForPlaybackStateChange(waitState: Int, lambda: (element: MediaElement, state: Int) -> Unit = { _: MediaElement, _: Int -> }) { + var waiting = true + while (waiting) { + sessionRule.waitUntilCalled(object : MediaElementDelegate { + @AssertCalled + override fun onPlaybackStateChange(mediaElement: MediaElement, mediaState: Int) { + if (mediaState == waitState) { + waiting = false + lambda(mediaElement, mediaState) + } + } + }) + } + } + + private fun waitForMetadata(path: String): MediaElement.Metadata? { + setupPrefsAndDelegates(path) + var meta: MediaElement.Metadata? = null + while (meta == null) { + sessionRule.waitUntilCalled(object : MediaElementDelegate { + @AssertCalled + override fun onMetadataChange(mediaElement: MediaElement, metaData: MediaElement.Metadata) { + meta = metaData + } + }) + } + return meta + } + + private fun playMedia(path: String) { + val mediaElement = waitUntilVideoReady(path) + mediaElement.play() + waitForPlaybackStateChange(MediaElement.MEDIA_STATE_PLAY) + waitForPlaybackStateChange(MediaElement.MEDIA_STATE_PLAYING) + } + + private fun playMediaFromScript(path: String) { + waitUntilVideoReady(path) + sessionRule.evaluateJS(mainSession, "$('video').play()") + waitForPlaybackStateChange(MediaElement.MEDIA_STATE_PLAY) + waitForPlaybackStateChange(MediaElement.MEDIA_STATE_PLAYING) + } + + private fun pauseMedia(path: String) { + val mediaElement = waitUntilVideoReady(path) + mediaElement.play() + waitForPlaybackStateChange(MediaElement.MEDIA_STATE_PLAYING) { element: MediaElement, _: Int -> + element.pause() + } + waitForPlaybackStateChange(MediaElement.MEDIA_STATE_PAUSE) + } + + private fun timeMedia(path: String, limit: Double) { + val mediaElement = waitUntilVideoReady(path) + mediaElement.play() + var waiting = true + while (waiting) { + sessionRule.waitUntilCalled(object : MediaElementDelegate { + @AssertCalled + override fun onTimeChange(mediaElement: MediaElement, time: Double) { + if (time > limit) { + waiting = false + } + } + }) + } + } + + private fun seekMedia(path: String, seek: Double) { + val media = waitUntilVideoReady(path) + media.seek(seek) + var waiting = true + // Sometimes we get a MediaElement.MEDIA_STATE_SUSPEND state change. So just wait until + // the test receives the SEEKING state change or time out. + while (waiting) { + sessionRule.waitUntilCalled(object : MediaElementDelegate { + @AssertCalled + override fun onPlaybackStateChange(mediaElement: MediaElement, mediaState: Int) { + if (mediaState == MediaElement.MEDIA_STATE_SEEKING) { + waiting = false + } + } + }) + } + waiting = true + while (waiting) { + sessionRule.waitUntilCalled(object : MediaElementDelegate { + @AssertCalled + override fun onTimeChange(mediaElement: MediaElement, time: Double) { + if (time >= seek) { + waiting = false + } + } + }) + } + sessionRule.waitUntilCalled(object : MediaElementDelegate { + @AssertCalled + override fun onPlaybackStateChange(mediaElement: MediaElement, mediaState: Int) { + assertThat("Done seeking", mediaState, equalTo(MediaElement.MEDIA_STATE_SEEKED)) + } + }) + } + + private fun fullscreenMedia(path: String) { + waitUntilVideoReady(path) + sessionRule.evaluateJS(mainSession, "$('video').requestFullscreen()") + var waiting = true + while (waiting) { + sessionRule.waitUntilCalled(object : MediaElementDelegate { + @AssertCalled + override fun onFullscreenChange(mediaElement: MediaElement, fullscreen: Boolean) { + if (fullscreen) { + waiting = false + } + } + }) + } + } + + @WithDevToolsAPI + @Test + fun oggPlayMedia() { + playMedia(VIDEO_OGG_PATH) + } + + @WithDevToolsAPI + @Test + fun oggPlayMediaFromScript() { + playMediaFromScript(VIDEO_OGG_PATH) + } + + @WithDevToolsAPI + @Test + fun oggPauseMedia() { + pauseMedia(VIDEO_OGG_PATH) + } + + @WithDevToolsAPI + @Test + fun oggTimeMedia() { + timeMedia(VIDEO_OGG_PATH, 2.0) + } + + @WithDevToolsAPI + @Test + fun oggMetadataMedia() { + val meta = waitForMetadata(VIDEO_OGG_PATH) + assertThat("Current source is set", meta?.currentSource, equalTo("resource://android/assets/www/videos/video.ogg")) + assertThat("Width is set", meta?.width, equalTo(320L)) + assertThat("Height is set", meta?.height, equalTo(240L)) + assertThat("Video is seekable", meta?.isSeekable, equalTo(true)) + // Disabled duration test for Bug 1510393 + // assertThat("Duration is set", meta?.duration, closeTo(4.0, 0.1)) + assertThat("Contains one video track", meta?.videoTrackCount, equalTo(1)) + assertThat("Contains one audio track", meta?.audioTrackCount, equalTo(0)) + } + + @WithDevToolsAPI + @Test + fun oggSeekMedia() { + seekMedia(VIDEO_OGG_PATH, 2.0) + } + + @WithDevToolsAPI + @Test + fun oggFullscreenMedia() { + fullscreenMedia(VIDEO_OGG_PATH) + } + + @WithDevToolsAPI + @Test + fun webmPlayMedia() { + playMedia(VIDEO_WEBM_PATH) + } + + @WithDevToolsAPI + @Test + fun webmPlayMediaFromScript() { + playMediaFromScript(VIDEO_WEBM_PATH) + } + + @WithDevToolsAPI + @Test + fun webmPauseMedia() { + pauseMedia(VIDEO_WEBM_PATH) + } + + @WithDevToolsAPI + @Test + fun webmTimeMedia() { + timeMedia(VIDEO_WEBM_PATH, 0.2) + } + + @WithDevToolsAPI + @Test + fun webmMetadataMedia() { + val meta = waitForMetadata(VIDEO_WEBM_PATH) + assertThat("Current source is set", meta?.currentSource, equalTo("resource://android/assets/www/videos/gizmo.webm")) + assertThat("Width is set", meta?.width, equalTo(560L)) + assertThat("Height is set", meta?.height, equalTo(320L)) + assertThat("Video is seekable", meta?.isSeekable, equalTo(true)) + assertThat("Duration is set", meta?.duration, closeTo(5.6, 0.1)) + assertThat("Contains one video track", meta?.videoTrackCount, equalTo(1)) + assertThat("Contains one audio track", meta?.audioTrackCount, equalTo(1)) + } + + @WithDevToolsAPI + @Test + fun webmSeekMedia() { + seekMedia(VIDEO_WEBM_PATH, 0.2) + } + + @WithDevToolsAPI + @Test + fun webmFullscreenMedia() { + fullscreenMedia(VIDEO_WEBM_PATH) + } + + private fun waitForVolumeChange(volumeLevel: Double, isMuted: Boolean) { + sessionRule.waitUntilCalled(object : MediaElementDelegate { + @AssertCalled + override fun onVolumeChange(mediaElement: MediaElement, volume: Double, muted: Boolean) { + assertThat("Volume was set", volume, closeTo(volumeLevel, 0.0001)) + assertThat("Not muted", muted, equalTo(isMuted)) + } + }) + } + + @WithDevToolsAPI + @Test + fun webmVolumeMedia() { + val media = waitUntilVideoReady(VIDEO_WEBM_PATH) + val volumeLevel = 0.5 + val volumeLevel2 = 0.75 + media.setVolume(volumeLevel) + waitForVolumeChange(volumeLevel, false) + media.setMuted(true) + waitForVolumeChange(volumeLevel, true) + media.setVolume(volumeLevel2) + waitForVolumeChange(volumeLevel2, true) + media.setMuted(false) + waitForVolumeChange(volumeLevel2, false) + } + + // NOTE: All MP4 tests are disabled on automation by Bug 1503952 + @WithDevToolsAPI + @Test + fun mp4PlayMedia() { + assumeThat(sessionRule.env.isAutomation, equalTo(false)) + playMedia(VIDEO_MP4_PATH) + } + + @WithDevToolsAPI + @Test + fun mp4PlayMediaFromScript() { + assumeThat(sessionRule.env.isAutomation, equalTo(false)) + playMediaFromScript(VIDEO_MP4_PATH) + } + + @WithDevToolsAPI + @Test + fun mp4PauseMedia() { + assumeThat(sessionRule.env.isAutomation, equalTo(false)) + pauseMedia(VIDEO_MP4_PATH) + } + + @WithDevToolsAPI + @Test + fun mp4TimeMedia() { + assumeThat(sessionRule.env.isAutomation, equalTo(false)) + timeMedia(VIDEO_MP4_PATH, 0.2) + } + + @WithDevToolsAPI + @Test + fun mp4MetadataMedia() { + assumeThat(sessionRule.env.isAutomation, equalTo(false)) + val meta = waitForMetadata(VIDEO_MP4_PATH) + assertThat("Current source is set", meta?.currentSource, equalTo("resource://android/assets/www/videos/short.mp4")) + assertThat("Width is set", meta?.width, equalTo(320L)) + assertThat("Height is set", meta?.height, equalTo(240L)) + assertThat("Video is seekable", meta?.isSeekable, equalTo(true)) + assertThat("Duration is set", meta?.duration, closeTo(0.5, 0.1)) + assertThat("Contains one video track", meta?.videoTrackCount, equalTo(1)) + assertThat("Contains one audio track", meta?.audioTrackCount, equalTo(1)) + } + + @WithDevToolsAPI + @Test + fun mp4SeekMedia() { + assumeThat(sessionRule.env.isAutomation, equalTo(false)) + seekMedia(VIDEO_MP4_PATH, 0.2) + } + + @WithDevToolsAPI + @Test + fun mp4FullscreenMedia() { + assumeThat(sessionRule.env.isAutomation, equalTo(false)) + fullscreenMedia(VIDEO_MP4_PATH) + } + + @WithDevToolsAPI + @Test + fun mp4VolumeMedia() { + assumeThat(sessionRule.env.isAutomation, equalTo(false)) + val media = waitUntilVideoReady(VIDEO_MP4_PATH) + val volumeLevel = 0.5 + val volumeLevel2 = 0.75 + media.setVolume(volumeLevel) + waitForVolumeChange(volumeLevel, false) + media.setMuted(true) + waitForVolumeChange(volumeLevel, true) + media.setVolume(volumeLevel2) + waitForVolumeChange(volumeLevel2, true) + media.setMuted(false) + waitForVolumeChange(volumeLevel2, false) + } + + @Ignore + @WithDevToolsAPI + @Test + fun badMediaPath() { + // Disabled on automation by Bug 1503957 + assumeThat(sessionRule.env.isAutomation, equalTo(false)) + setupPrefsAndDelegates(VIDEO_BAD_PATH) + sessionRule.waitForPageStop() + sessionRule.waitUntilCalled(object : MediaElementDelegate { + @AssertCalled + override fun onError(mediaElement: MediaElement, errorCode: Int) { + assertThat("Got media error", errorCode, equalTo(MediaElement.MEDIA_ERROR_NETWORK_NO_SOURCE)) + } + }) + } +}