diff options
Diffstat (limited to 'mobile/android/chrome/geckoview/GeckoViewMediaChild.js')
-rw-r--r-- | mobile/android/chrome/geckoview/GeckoViewMediaChild.js | 439 |
1 files changed, 439 insertions, 0 deletions
diff --git a/mobile/android/chrome/geckoview/GeckoViewMediaChild.js b/mobile/android/chrome/geckoview/GeckoViewMediaChild.js new file mode 100644 index 0000000000..e81008ebc5 --- /dev/null +++ b/mobile/android/chrome/geckoview/GeckoViewMediaChild.js @@ -0,0 +1,439 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; +const { GeckoViewChildModule } = ChromeUtils.import( + "resource://gre/modules/GeckoViewChildModule.jsm" +); + +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); + +XPCOMUtils.defineLazyModuleGetters(this, { + MediaUtils: "resource://gre/modules/MediaUtils.jsm", +}); + +class GeckoViewMediaChild extends GeckoViewChildModule { + onInit() { + this._videoIndex = 0; + XPCOMUtils.defineLazyGetter(this, "_videos", () => new Map()); + this._mediaEvents = [ + "abort", + "canplay", + "canplaythrough", + "durationchange", + "emptied", + "ended", + "error", + "loadeddata", + "loadedmetadata", + "pause", + "play", + "playing", + "progress", + "ratechange", + "resize", + "seeked", + "seeking", + "stalled", + "suspend", + "timeupdate", + "volumechange", + "waiting", + ]; + + this._mediaEventCallback = event => { + this.handleMediaEvent(event); + }; + this._fullscreenMedia = null; + this._stateSymbol = Symbol(); + } + + onEnable() { + debug`onEnable`; + + addEventListener("UAWidgetSetupOrChange", this, false); + addEventListener("MozDOMFullscreen:Entered", this, false); + addEventListener("MozDOMFullscreen:Exited", this, false); + addEventListener("pagehide", this, false); + + this.messageManager.addMessageListener("GeckoView:MediaObserve", this); + this.messageManager.addMessageListener("GeckoView:MediaUnobserve", this); + this.messageManager.addMessageListener("GeckoView:MediaPlay", this); + this.messageManager.addMessageListener("GeckoView:MediaPause", this); + this.messageManager.addMessageListener("GeckoView:MediaSeek", this); + this.messageManager.addMessageListener("GeckoView:MediaSetVolume", this); + this.messageManager.addMessageListener("GeckoView:MediaSetMuted", this); + this.messageManager.addMessageListener( + "GeckoView:MediaSetPlaybackRate", + this + ); + } + + onDisable() { + debug`onDisable`; + + removeEventListener("UAWidgetSetupOrChange", this); + removeEventListener("MozDOMFullscreen:Entered", this); + removeEventListener("MozDOMFullscreen:Exited", this); + removeEventListener("pagehide", this); + + this.messageManager.removeMessageListener("GeckoView:MediaObserve", this); + this.messageManager.removeMessageListener("GeckoView:MediaUnobserve", this); + this.messageManager.removeMessageListener("GeckoView:MediaPlay", this); + this.messageManager.removeMessageListener("GeckoView:MediaPause", this); + this.messageManager.removeMessageListener("GeckoView:MediaSeek", this); + this.messageManager.removeMessageListener("GeckoView:MediaSetVolume", this); + this.messageManager.removeMessageListener("GeckoView:MediaSetMuted", this); + this.messageManager.removeMessageListener( + "GeckoView:MediaSetPlaybackRate", + this + ); + } + + receiveMessage(aMsg) { + debug`receiveMessage: ${aMsg.name}`; + + const data = aMsg.data; + const element = this.findElement(data.id); + if (!element) { + warn`Didn't find HTMLMediaElement with id: ${data.id}`; + return; + } + + switch (aMsg.name) { + case "GeckoView:MediaObserve": + this.observeMedia(element); + break; + case "GeckoView:MediaUnobserve": + this.unobserveMedia(element); + break; + case "GeckoView:MediaPlay": + element.play(); + break; + case "GeckoView:MediaPause": + element.pause(); + break; + case "GeckoView:MediaSeek": + element.currentTime = data.time; + break; + case "GeckoView:MediaSetVolume": + element.volume = data.volume; + break; + case "GeckoView:MediaSetMuted": + element.muted = !!data.muted; + break; + case "GeckoView:MediaSetPlaybackRate": + element.playbackRate = data.playbackRate; + break; + } + } + + handleEvent(aEvent) { + debug`handleEvent: ${aEvent.type}`; + + switch (aEvent.type) { + case "UAWidgetSetupOrChange": + this.handleNewMedia(aEvent.composedTarget); + break; + case "MozDOMFullscreen:Entered": + const element = content && content.document.fullscreenElement; + // document.fullscreenElement can be a div container instead of the + // HTMLMediaElement in some pages (e.g Vimeo). + const mediaElement = MediaUtils.findVideoElement(element); + if (mediaElement) { + this.handleFullscreenChange(mediaElement); + } + break; + case "MozDOMFullscreen:Exited": + this.handleFullscreenChange(null); + break; + case "pagehide": + if (aEvent.target === content.document) { + this.handleWindowUnload(); + } + break; + } + } + + handleWindowUnload() { + for (const weakElement of this._videos.values()) { + const element = weakElement.get(); + if (element) { + this.unobserveMedia(element); + } + } + if (this._videos.size > 0) { + this.notifyMediaRemoveAll(); + } + this._videos.clear(); + this._fullscreenMedia = null; + } + + handleNewMedia(aElement) { + if (this.getState(aElement) || !MediaUtils.isMediaElement(aElement)) { + return; + } + const state = { + id: ++this._videoIndex, + notified: false, + observing: false, + }; + aElement[this._stateSymbol] = state; + this._videos.set(state.id, Cu.getWeakReference(aElement)); + + this.notifyNewMedia(aElement); + } + + notifyNewMedia(aElement) { + this.getState(aElement).notified = true; + const message = MediaUtils.getMetadata(aElement) ?? {}; + message.type = "GeckoView:MediaAdd"; + message.id = this.getState(aElement).id; + + this.eventDispatcher.sendRequest(message); + } + + observeMedia(aElement) { + if (this.isObserving(aElement)) { + return; + } + this.getState(aElement).observing = true; + + for (const name of this._mediaEvents) { + aElement.addEventListener(name, this._mediaEventCallback, true); + } + + // Notify current state + this.notifyTimeChange(aElement); + this.notifyVolumeChange(aElement); + this.notifyRateChange(aElement); + if (aElement.readyState >= 1) { + this.notifyMetadataChange(aElement); + } + this.notifyReadyStateChange(aElement); + if (!aElement.paused) { + this.notifyPlaybackStateChange(aElement, "play"); + } + if (aElement === this._fullscreenMedia) { + this.notifyFullscreenChange(aElement, true); + } + if (this.hasError(aElement)) { + this.notifyMediaError(aElement); + } + } + + unobserveMedia(aElement) { + if (!this.isObserving(aElement)) { + return; + } + this.getState(aElement).observing = false; + for (const name of this._mediaEvents) { + aElement.removeEventListener(name, this._mediaEventCallback); + } + } + + isObserving(aElement) { + return ( + aElement && this.getState(aElement) && this.getState(aElement).observing + ); + } + + findElement(aId) { + for (const weakElement of this._videos.values()) { + const element = weakElement.get(); + if (element && this.getState(element).id === aId) { + return element; + } + } + return null; + } + + getState(aElement) { + return aElement[this._stateSymbol]; + } + + hasError(aElement) { + // We either have an explicit error, or networkState is set to NETWORK_NO_SOURCE + // after selecting a source. + return ( + aElement.error != null || + (aElement.networkState === aElement.NETWORK_NO_SOURCE && + this.hasSources(aElement)) + ); + } + + hasSources(aElement) { + if (aElement.hasAttribute("src") && aElement.getAttribute("src") !== "") { + return true; + } + for ( + var child = aElement.firstChild; + child !== null; + child = child.nextElementSibling + ) { + if (child instanceof content.window.HTMLSourceElement) { + return true; + } + } + return false; + } + + handleMediaEvent(aEvent) { + const element = aEvent.target; + if (!this.isObserving(element)) { + return; + } + switch (aEvent.type) { + case "timeupdate": + this.notifyTimeChange(element); + break; + case "volumechange": + this.notifyVolumeChange(element); + break; + case "loadedmetadata": + this.notifyMetadataChange(element); + this.notifyReadyStateChange(element); + break; + case "ratechange": + this.notifyRateChange(element); + break; + case "error": + this.notifyMediaError(element); + break; + case "progress": + this.notifyLoadProgress(element, aEvent); + break; + case "durationchange": // Fallthrough + case "resize": + this.notifyMetadataChange(element); + break; + case "canplay": // Fallthrough + case "canplaythrough": // Fallthrough + case "loadeddata": + this.notifyReadyStateChange(element); + break; + default: + this.notifyPlaybackStateChange(element, aEvent.type); + break; + } + } + + handleFullscreenChange(aElement) { + if (aElement === this._fullscreenMedia) { + return; + } + + if (this.isObserving(this._fullscreenMedia)) { + this.notifyFullscreenChange(this._fullscreenMedia, false); + } + this._fullscreenMedia = aElement; + + if (this.isObserving(aElement)) { + this.notifyFullscreenChange(aElement, true); + } + } + + notifyPlaybackStateChange(aElement, aName) { + this.eventDispatcher.sendRequest({ + type: "GeckoView:MediaPlaybackStateChanged", + id: this.getState(aElement).id, + playbackState: aName, + }); + } + + notifyReadyStateChange(aElement) { + this.eventDispatcher.sendRequest({ + type: "GeckoView:MediaReadyStateChanged", + id: this.getState(aElement).id, + readyState: aElement.readyState, + }); + } + + notifyMetadataChange(aElement) { + const message = MediaUtils.getMetadata(aElement) ?? {}; + message.type = "GeckoView:MediaMetadataChanged"; + message.id = this.getState(aElement).id; + + this.eventDispatcher.sendRequest(message); + } + + notifyLoadProgress(aElement, aEvent) { + const message = { + type: "GeckoView:MediaProgress", + id: this.getState(aElement).id, + loadedBytes: aEvent.lengthComputable ? aEvent.loaded : -1, + totalBytes: aEvent.lengthComputable ? aEvent.total : -1, + }; + if (aElement.buffered && aElement.buffered.length > 0) { + message.timeRangeStarts = []; + message.timeRangeEnds = []; + for (let i = 0; i < aElement.buffered.length; ++i) { + message.timeRangeStarts.push(aElement.buffered.start(i)); + message.timeRangeEnds.push(aElement.buffered.end(i)); + } + } + this.eventDispatcher.sendRequest(message); + } + + notifyTimeChange(aElement) { + this.eventDispatcher.sendRequest({ + type: "GeckoView:MediaTimeChanged", + id: this.getState(aElement).id, + time: aElement.currentTime, + }); + } + + notifyRateChange(aElement) { + this.eventDispatcher.sendRequest({ + type: "GeckoView:MediaRateChanged", + id: this.getState(aElement).id, + rate: aElement.playbackRate, + }); + } + + notifyVolumeChange(aElement) { + this.eventDispatcher.sendRequest({ + type: "GeckoView:MediaVolumeChanged", + id: this.getState(aElement).id, + volume: aElement.volume, + muted: !!aElement.muted, + }); + } + + notifyFullscreenChange(aElement, aIsFullscreen) { + this.eventDispatcher.sendRequest({ + type: "GeckoView:MediaFullscreenChanged", + id: this.getState(aElement).id, + fullscreen: aIsFullscreen, + }); + } + + notifyMediaError(aElement) { + const code = aElement.error ? aElement.error.code : 0; + this.eventDispatcher.sendRequest({ + type: "GeckoView:MediaError", + id: this.getState(aElement).id, + code, + }); + } + + notifyMediaRemove(aElement) { + this.eventDispatcher.sendRequest({ + type: "GeckoView:MediaRemove", + id: this.getState(aElement).id, + }); + } + + notifyMediaRemoveAll() { + this.eventDispatcher.sendRequest({ + type: "GeckoView:MediaRemoveAll", + }); + } +} + +const { debug, warn } = GeckoViewMediaChild.initLogging("GeckoViewMedia"); +const module = GeckoViewMediaChild.create(this); |