/* 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"; // This is a UA widget. It runs in per-origin UA widget scope, // to be loaded by UAWidgetsChild.jsm. /* * This is the class of entry. It will construct the actual implementation * according to the value of the "controls" property. */ this.VideoControlsWidget = class { constructor(shadowRoot) { this.shadowRoot = shadowRoot; this.element = shadowRoot.host; this.document = this.element.ownerDocument; this.window = this.document.defaultView; this.isMobile = this.window.navigator.appVersion.includes("Android"); } /* * Callback called by UAWidgets right after constructor. */ onsetup() { this.switchImpl(); } /* * Callback called by UAWidgets when the "controls" property changes. */ onchange() { this.switchImpl(); } /* * Actually switch the implementation. * - With "controls" set, the VideoControlsImplWidget controls should load. * - Without it, on mobile, the NoControlsImplWidget should load, so * the user could see the click-to-play button when the video/audio is blocked. */ switchImpl() { let newImpl; if (this.element.controls) { newImpl = VideoControlsImplWidget; } else if (this.isMobile) { newImpl = NoControlsImplWidget; } // Skip if we are asked to load the same implementation. // This can happen if the property is set again w/o value change. if (this.impl && this.impl.constructor == newImpl) { return; } if (this.impl) { this.impl.destructor(); this.shadowRoot.firstChild.remove(); } if (newImpl) { this.impl = new newImpl(this.shadowRoot); this.impl.onsetup(); } else { this.impl = undefined; } } destructor() { if (!this.impl) { return; } this.impl.destructor(); this.shadowRoot.firstChild.remove(); delete this.impl; } }; this.VideoControlsImplWidget = class { constructor(shadowRoot) { this.shadowRoot = shadowRoot; this.element = shadowRoot.host; this.document = this.element.ownerDocument; this.window = this.document.defaultView; } onsetup() { this.generateContent(); this.Utils = { debug: false, video: null, videocontrols: null, controlBar: null, playButton: null, muteButton: null, volumeControl: null, durationLabel: null, positionLabel: null, scrubber: null, progressBar: null, bufferBar: null, statusOverlay: null, controlsSpacer: null, clickToPlay: null, controlsOverlay: null, fullscreenButton: null, layoutControls: null, textTracksCount: 0, videoEvents: ["play", "pause", "ended", "volumechange", "loadeddata", "loadstart", "timeupdate", "progress", "playing", "waiting", "canplay", "canplaythrough", "seeking", "seeked", "emptied", "loadedmetadata", "error", "suspend", "stalled", "mozvideoonlyseekbegin", "mozvideoonlyseekcompleted"], showHours: false, firstFrameShown: false, timeUpdateCount: 0, maxCurrentTimeSeen: 0, isPausedByDragging: false, _isAudioOnly: false, get isAudioOnly() { return this._isAudioOnly; }, set isAudioOnly(val) { this._isAudioOnly = val; this.setFullscreenButtonState(); if (!this.isTopLevelSyntheticDocument) { return; } if (this._isAudioOnly) { this.video.style.height = this.controlBarMinHeight + "px"; this.video.style.width = "66%"; } else { this.video.style.removeProperty("height"); this.video.style.removeProperty("width"); } }, suppressError: false, setupStatusFader(immediate) { // Since the play button will be showing, we don't want to // show the throbber behind it. The throbber here will // only show if needed after the play button has been pressed. if (!this.clickToPlay.hidden) { this.startFadeOut(this.statusOverlay, true); return; } var show = false; if (this.video.seeking || (this.video.error && !this.suppressError) || this.video.networkState == this.video.NETWORK_NO_SOURCE || (this.video.networkState == this.video.NETWORK_LOADING && (this.video.paused || this.video.ended ? this.video.readyState < this.video.HAVE_CURRENT_DATA : this.video.readyState < this.video.HAVE_FUTURE_DATA)) || (this.timeUpdateCount <= 1 && !this.video.ended && this.video.readyState < this.video.HAVE_FUTURE_DATA && this.video.networkState == this.video.NETWORK_LOADING)) { show = true; } // Explicitly hide the status fader if this // is audio only until bug 619421 is fixed. if (this.isAudioOnly) { show = false; } if (this._showThrobberTimer) { show = true; } this.log("Status overlay: seeking=" + this.video.seeking + " error=" + this.video.error + " readyState=" + this.video.readyState + " paused=" + this.video.paused + " ended=" + this.video.ended + " networkState=" + this.video.networkState + " timeUpdateCount=" + this.timeUpdateCount + " _showThrobberTimer=" + this._showThrobberTimer + " --> " + (show ? "SHOW" : "HIDE")); this.startFade(this.statusOverlay, show, immediate); }, /* * Set the initial state of the controls. The UA widget is normally created along * with video element, but could be attached at any point (eg, if the video is * removed from the document and then reinserted). Thus, some one-time events may * have already fired, and so we'll need to explicitly check the initial state. */ setupInitialState() { this.setPlayButtonState(this.video.paused); this.setFullscreenButtonState(); var duration = Math.round(this.video.duration * 1000); // in ms var currentTime = Math.round(this.video.currentTime * 1000); // in ms this.log("Initial playback position is at " + currentTime + " of " + duration); // It would be nice to retain maxCurrentTimeSeen, but it would be difficult // to determine if the media source changed while we were detached. this.initPositionDurationBox(); this.maxCurrentTimeSeen = currentTime; this.showPosition(currentTime, duration); // If we have metadata, check if this is a