Skip to content

Commit b18c2f5

Browse files
committed
Issue mozilla-mobile#1845: Expose list of media in Session.
1 parent f1f8aa8 commit b18c2f5

File tree

16 files changed

+781
-0
lines changed

16 files changed

+781
-0
lines changed

components/browser/engine-gecko-nightly/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngineSession.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import kotlinx.coroutines.Dispatchers
1212
import kotlinx.coroutines.Job
1313
import kotlinx.coroutines.launch
1414
import kotlinx.coroutines.runBlocking
15+
import mozilla.components.browser.engine.gecko.media.GeckoMediaDelegate
1516
import mozilla.components.browser.engine.gecko.permission.GeckoPermissionRequest
1617
import mozilla.components.browser.engine.gecko.prompt.GeckoPromptDelegate
1718
import mozilla.components.browser.errorpages.ErrorType
@@ -582,6 +583,7 @@ class GeckoEngineSession(
582583
geckoSession.permissionDelegate = createPermissionDelegate()
583584
geckoSession.promptDelegate = GeckoPromptDelegate(this)
584585
geckoSession.historyDelegate = createHistoryDelegate()
586+
geckoSession.mediaDelegate = GeckoMediaDelegate(this)
585587
}
586588

587589
companion object {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
package mozilla.components.browser.engine.gecko.media
6+
7+
import mozilla.components.concept.engine.media.Media
8+
import org.mozilla.geckoview.MediaElement
9+
import org.mozilla.geckoview.MediaElement.MEDIA_STATE_ABORT
10+
import org.mozilla.geckoview.MediaElement.MEDIA_STATE_EMPTIED
11+
import org.mozilla.geckoview.MediaElement.MEDIA_STATE_ENDED
12+
import org.mozilla.geckoview.MediaElement.MEDIA_STATE_PAUSE
13+
import org.mozilla.geckoview.MediaElement.MEDIA_STATE_PLAY
14+
import org.mozilla.geckoview.MediaElement.MEDIA_STATE_PLAYING
15+
import org.mozilla.geckoview.MediaElement.MEDIA_STATE_SEEKED
16+
import org.mozilla.geckoview.MediaElement.MEDIA_STATE_SEEKING
17+
import org.mozilla.geckoview.MediaElement.MEDIA_STATE_STALLED
18+
import org.mozilla.geckoview.MediaElement.MEDIA_STATE_SUSPEND
19+
import org.mozilla.geckoview.MediaElement.MEDIA_STATE_WAITING
20+
21+
/**
22+
* [Media] (`concept-engine`) implementation for GeckoView.
23+
*/
24+
internal class GeckoMedia(
25+
internal val mediaElement: MediaElement
26+
) : Media() {
27+
override val controller: Controller = GeckoMediaController(mediaElement)
28+
29+
init {
30+
mediaElement.delegate = MediaDelegate(this)
31+
}
32+
33+
override fun equals(other: Any?): Boolean {
34+
if (other == null || other !is GeckoMedia) {
35+
return false
36+
}
37+
38+
return mediaElement == other.mediaElement
39+
}
40+
41+
override fun hashCode(): Int {
42+
return mediaElement.hashCode()
43+
}
44+
}
45+
46+
private class MediaDelegate(
47+
private val media: Media
48+
) : MediaElement.Delegate {
49+
@Suppress("ComplexMethod")
50+
override fun onPlaybackStateChange(mediaElement: MediaElement, mediaState: Int) {
51+
when (mediaState) {
52+
MEDIA_STATE_PLAY -> media.playbackState = Media.PlaybackState.PLAY
53+
MEDIA_STATE_PLAYING -> media.playbackState = Media.PlaybackState.PLAYING
54+
MEDIA_STATE_PAUSE -> media.playbackState = Media.PlaybackState.PAUSE
55+
MEDIA_STATE_ENDED -> media.playbackState = Media.PlaybackState.ENDED
56+
MEDIA_STATE_SEEKING -> media.playbackState = Media.PlaybackState.SEEKING
57+
MEDIA_STATE_SEEKED -> media.playbackState = Media.PlaybackState.SEEKED
58+
MEDIA_STATE_STALLED -> media.playbackState = Media.PlaybackState.STALLED
59+
MEDIA_STATE_SUSPEND -> media.playbackState = Media.PlaybackState.SUSPENDED
60+
MEDIA_STATE_WAITING -> media.playbackState = Media.PlaybackState.WAITING
61+
MEDIA_STATE_ABORT -> media.playbackState = Media.PlaybackState.ABORT
62+
MEDIA_STATE_EMPTIED -> media.playbackState = Media.PlaybackState.EMPTIED
63+
}
64+
}
65+
66+
override fun onReadyStateChange(mediaElement: MediaElement, readyState: Int) = Unit
67+
override fun onMetadataChange(mediaElement: MediaElement, metaData: MediaElement.Metadata) = Unit
68+
override fun onLoadProgress(mediaElement: MediaElement, progressInfo: MediaElement.LoadProgressInfo) = Unit
69+
override fun onVolumeChange(mediaElement: MediaElement, volume: Double, muted: Boolean) = Unit
70+
override fun onTimeChange(mediaElement: MediaElement, time: Double) = Unit
71+
override fun onPlaybackRateChange(mediaElement: MediaElement, rate: Double) = Unit
72+
override fun onFullscreenChange(mediaElement: MediaElement, fullscreen: Boolean) = Unit
73+
override fun onError(mediaElement: MediaElement, errorCode: Int) = Unit
74+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
package mozilla.components.browser.engine.gecko.media
6+
7+
import mozilla.components.concept.engine.media.Media
8+
import org.mozilla.geckoview.MediaElement
9+
10+
/**
11+
* [Media.Controller] (`concept-engine`) implementation for GeckoView.
12+
*/
13+
internal class GeckoMediaController(
14+
private val mediaElement: MediaElement
15+
) : Media.Controller {
16+
override fun pause() {
17+
mediaElement.pause()
18+
}
19+
20+
override fun play() {
21+
mediaElement.play()
22+
}
23+
24+
override fun seek(time: Double) {
25+
mediaElement.seek(time)
26+
}
27+
28+
override fun setMuted(muted: Boolean) {
29+
mediaElement.setMuted(muted)
30+
}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
package mozilla.components.browser.engine.gecko.media
6+
7+
import mozilla.components.browser.engine.gecko.GeckoEngineSession
8+
import mozilla.components.concept.engine.EngineSession
9+
import mozilla.components.concept.engine.media.Media
10+
import org.mozilla.geckoview.GeckoSession
11+
import org.mozilla.geckoview.MediaElement
12+
13+
/**
14+
* [GeckoSession.MediaDelegate] implementation for wrapping [MediaElement] instances in [GeckoMedia] ([Media]) and
15+
* forwarding them to the [EngineSession.Observer].
16+
*/
17+
internal class GeckoMediaDelegate(
18+
private val engineSession: GeckoEngineSession
19+
) : GeckoSession.MediaDelegate {
20+
private val mediaMap: MutableMap<MediaElement, Media> = mutableMapOf()
21+
22+
override fun onMediaAdd(session: GeckoSession, element: MediaElement) {
23+
engineSession.notifyObservers {
24+
val media = GeckoMedia(element)
25+
26+
mediaMap[element] = media
27+
28+
onMediaAdded(media)
29+
}
30+
}
31+
32+
override fun onMediaRemove(session: GeckoSession, element: MediaElement) {
33+
mediaMap[element]?.let { media ->
34+
engineSession.notifyObservers { onMediaRemoved(media) }
35+
}
36+
}
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
package mozilla.components.browser.engine.gecko.media
6+
7+
import mozilla.components.support.test.mock
8+
import org.junit.Test
9+
import org.mockito.Mockito.verify
10+
import org.mockito.Mockito.verifyNoMoreInteractions
11+
import org.mozilla.geckoview.MediaElement
12+
13+
class GeckoMediaControllerTest {
14+
@Test
15+
fun `Pause is forwarded to media element`() {
16+
val mediaElement: MediaElement = mock()
17+
18+
val controller = GeckoMediaController(mediaElement)
19+
controller.pause()
20+
21+
verify(mediaElement).pause()
22+
}
23+
24+
@Test
25+
fun `Play is forwarded to media element`() {
26+
val mediaElement: MediaElement = mock()
27+
28+
val controller = GeckoMediaController(mediaElement)
29+
controller.play()
30+
31+
verify(mediaElement).play()
32+
}
33+
34+
@Test
35+
fun `Seek is forwarded to media element`() {
36+
val mediaElement: MediaElement = mock()
37+
38+
val controller = GeckoMediaController(mediaElement)
39+
controller.seek(1337.42)
40+
41+
verify(mediaElement).seek(1337.42)
42+
}
43+
44+
@Test
45+
fun `SetMuted is forwarded to media element`() {
46+
val mediaElement: MediaElement = mock()
47+
48+
val controller = GeckoMediaController(mediaElement)
49+
controller.setMuted(true)
50+
51+
verify(mediaElement).setMuted(true)
52+
verifyNoMoreInteractions(mediaElement)
53+
54+
controller.setMuted(false)
55+
verify(mediaElement).setMuted(false)
56+
}
57+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package mozilla.components.browser.engine.gecko.media
2+
3+
import mozilla.components.browser.engine.gecko.GeckoEngineSession
4+
import mozilla.components.concept.engine.EngineSession
5+
import mozilla.components.concept.engine.media.Media
6+
import mozilla.components.support.test.mock
7+
import org.junit.Assert.assertEquals
8+
import org.junit.Assert.assertFalse
9+
import org.junit.Assert.assertNotNull
10+
import org.junit.Test
11+
import org.junit.runner.RunWith
12+
import org.mockito.Mockito
13+
import org.mockito.Mockito.verify
14+
import org.mozilla.geckoview.GeckoRuntime
15+
import org.mozilla.geckoview.MediaElement
16+
import org.robolectric.RobolectricTestRunner
17+
18+
@RunWith(RobolectricTestRunner::class)
19+
class GeckoMediaDelegateTest {
20+
@Test
21+
fun `Added MediaElement is wrapped in GeckoMedia and forwarded to observer`() {
22+
val engineSession = GeckoEngineSession(Mockito.mock(GeckoRuntime::class.java))
23+
24+
var observedMedia: Media? = null
25+
26+
engineSession.register(object : EngineSession.Observer {
27+
override fun onMediaAdded(media: Media) {
28+
observedMedia = media
29+
}
30+
})
31+
32+
val mediaElement: MediaElement = mock()
33+
engineSession.geckoSession.mediaDelegate!!.onMediaAdd(mock(), mediaElement)
34+
35+
assertNotNull(observedMedia!!)
36+
37+
observedMedia!!.controller.play()
38+
verify(mediaElement).play()
39+
}
40+
41+
@Test
42+
fun `WHEN MediaElement is removed THEN previously added GeckoMedia is used to notify observer`() {
43+
val engineSession = GeckoEngineSession(Mockito.mock(GeckoRuntime::class.java))
44+
45+
var addedMedia: Media? = null
46+
var removedMedia: Media? = null
47+
48+
engineSession.register(object : EngineSession.Observer {
49+
override fun onMediaAdded(media: Media) {
50+
addedMedia = media
51+
}
52+
53+
override fun onMediaRemoved(media: Media) {
54+
removedMedia = media
55+
}
56+
})
57+
58+
val mediaElement: MediaElement = mock()
59+
engineSession.geckoSession.mediaDelegate!!.onMediaAdd(mock(), mediaElement)
60+
engineSession.geckoSession.mediaDelegate!!.onMediaRemove(mock(), mediaElement)
61+
62+
assertNotNull(addedMedia!!)
63+
assertNotNull(removedMedia!!)
64+
assertEquals(addedMedia, removedMedia)
65+
}
66+
67+
@Test
68+
fun `WHEN unknown media is removed THEN observer is not notified`() {
69+
val engineSession = GeckoEngineSession(Mockito.mock(GeckoRuntime::class.java))
70+
71+
var onMediaRemovedExecuted = false
72+
73+
engineSession.register(object : EngineSession.Observer {
74+
override fun onMediaRemoved(media: Media) {
75+
onMediaRemovedExecuted = true
76+
}
77+
})
78+
79+
val mediaElement: MediaElement = mock()
80+
engineSession.geckoSession.mediaDelegate!!.onMediaRemove(mock(), mediaElement)
81+
82+
assertFalse(onMediaRemovedExecuted)
83+
}
84+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
package mozilla.components.browser.engine.gecko.media
6+
7+
import mozilla.components.concept.engine.media.Media
8+
import mozilla.components.support.test.argumentCaptor
9+
import mozilla.components.support.test.mock
10+
import org.junit.Assert.assertEquals
11+
import org.junit.Assert.assertTrue
12+
import org.junit.Test
13+
import org.mockito.Mockito.verify
14+
import org.mozilla.geckoview.MediaElement
15+
16+
class GeckoMediaTest {
17+
@Test
18+
fun `Playback state is updated from MediaDelegate`() {
19+
val mediaElement: MediaElement = mock()
20+
21+
val media = GeckoMedia(mediaElement)
22+
23+
val captor = argumentCaptor<MediaElement.Delegate>()
24+
verify(mediaElement).delegate = captor.capture()
25+
26+
val delegate = captor.value
27+
28+
delegate.onPlaybackStateChange(mediaElement, MediaElement.MEDIA_STATE_PLAYING)
29+
assertEquals(Media.PlaybackState.PLAYING, media.playbackState)
30+
31+
delegate.onPlaybackStateChange(mediaElement, MediaElement.MEDIA_STATE_SEEKING)
32+
assertEquals(Media.PlaybackState.SEEKING, media.playbackState)
33+
34+
delegate.onPlaybackStateChange(mediaElement, MediaElement.MEDIA_STATE_WAITING)
35+
assertEquals(Media.PlaybackState.WAITING, media.playbackState)
36+
37+
delegate.onPlaybackStateChange(mediaElement, MediaElement.MEDIA_STATE_PAUSE)
38+
assertEquals(Media.PlaybackState.PAUSE, media.playbackState)
39+
40+
delegate.onPlaybackStateChange(mediaElement, MediaElement.MEDIA_STATE_PLAY)
41+
assertEquals(Media.PlaybackState.PLAY, media.playbackState)
42+
43+
delegate.onPlaybackStateChange(mediaElement, MediaElement.MEDIA_STATE_SEEKED)
44+
assertEquals(Media.PlaybackState.SEEKED, media.playbackState)
45+
46+
delegate.onPlaybackStateChange(mediaElement, MediaElement.MEDIA_STATE_STALLED)
47+
assertEquals(Media.PlaybackState.STALLED, media.playbackState)
48+
49+
delegate.onPlaybackStateChange(mediaElement, MediaElement.MEDIA_STATE_SUSPEND)
50+
assertEquals(Media.PlaybackState.SUSPENDED, media.playbackState)
51+
52+
delegate.onPlaybackStateChange(mediaElement, MediaElement.MEDIA_STATE_ABORT)
53+
assertEquals(Media.PlaybackState.ABORT, media.playbackState)
54+
55+
delegate.onPlaybackStateChange(mediaElement, MediaElement.MEDIA_STATE_EMPTIED)
56+
assertEquals(Media.PlaybackState.EMPTIED, media.playbackState)
57+
58+
delegate.onPlaybackStateChange(mediaElement, MediaElement.MEDIA_STATE_ENDED)
59+
assertEquals(Media.PlaybackState.ENDED, media.playbackState)
60+
}
61+
62+
@Test
63+
fun `GeckoMedia exposes GeckoMediaController`() {
64+
val mediaElement: MediaElement = mock()
65+
66+
val media = GeckoMedia(mediaElement)
67+
68+
assertTrue(media.controller is GeckoMediaController)
69+
}
70+
}

0 commit comments

Comments
 (0)