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))
+ }
+ })
+ }
+}