summaryrefslogtreecommitdiff
path: root/Source/WebCore/Modules/mediacontrols/mediaControlsApple.js
diff options
context:
space:
mode:
Diffstat (limited to 'Source/WebCore/Modules/mediacontrols/mediaControlsApple.js')
-rw-r--r--Source/WebCore/Modules/mediacontrols/mediaControlsApple.js2509
1 files changed, 2509 insertions, 0 deletions
diff --git a/Source/WebCore/Modules/mediacontrols/mediaControlsApple.js b/Source/WebCore/Modules/mediacontrols/mediaControlsApple.js
new file mode 100644
index 000000000..d1893734c
--- /dev/null
+++ b/Source/WebCore/Modules/mediacontrols/mediaControlsApple.js
@@ -0,0 +1,2509 @@
+function createControls(root, video, host)
+{
+ return new Controller(root, video, host);
+};
+
+function Controller(root, video, host)
+{
+ this.video = video;
+ this.root = root;
+ this.host = host;
+ this.controls = {};
+ this.listeners = {};
+ this.isLive = false;
+ this.statusHidden = true;
+ this.hasWirelessPlaybackTargets = false;
+ this.canToggleShowControlsButton = false;
+ this.isListeningForPlaybackTargetAvailabilityEvent = false;
+ this.currentTargetIsWireless = false;
+ this.wirelessPlaybackDisabled = false;
+ this.isVolumeSliderActive = false;
+ this.currentDisplayWidth = 0;
+ this._scrubbing = false;
+ this._pageScaleFactor = 1;
+
+ this.addVideoListeners();
+ this.createBase();
+ this.createControls();
+ this.createTimeClones();
+ this.updateBase();
+ this.updateControls();
+ this.updateDuration();
+ this.updateProgress();
+ this.updateTime();
+ this.updateReadyState();
+ this.updatePlaying();
+ this.updateThumbnail();
+ this.updateCaptionButton();
+ this.updateCaptionContainer();
+ this.updateFullscreenButtons();
+ this.updateVolume();
+ this.updateHasAudio();
+ this.updateHasVideo();
+ this.updateWirelessTargetAvailable();
+ this.updateWirelessPlaybackStatus();
+ this.updatePictureInPicturePlaceholder();
+ this.scheduleUpdateLayoutForDisplayedWidth();
+
+ this.listenFor(this.root, 'resize', this.handleRootResize);
+};
+
+/* Enums */
+Controller.InlineControls = 0;
+Controller.FullScreenControls = 1;
+
+Controller.PlayAfterSeeking = 0;
+Controller.PauseAfterSeeking = 1;
+
+/* Globals */
+Controller.gSimulateWirelessPlaybackTarget = false; // Used for testing when there are no wireless targets.
+Controller.gSimulatePictureInPictureAvailable = false; // Used for testing when picture-in-picture is not available.
+
+Controller.prototype = {
+
+ /* Constants */
+ HandledVideoEvents: {
+ loadstart: 'handleLoadStart',
+ error: 'handleError',
+ abort: 'handleAbort',
+ suspend: 'handleSuspend',
+ stalled: 'handleStalled',
+ waiting: 'handleWaiting',
+ emptied: 'handleReadyStateChange',
+ loadedmetadata: 'handleReadyStateChange',
+ loadeddata: 'handleReadyStateChange',
+ canplay: 'handleReadyStateChange',
+ canplaythrough: 'handleReadyStateChange',
+ timeupdate: 'handleTimeUpdate',
+ durationchange: 'handleDurationChange',
+ playing: 'handlePlay',
+ pause: 'handlePause',
+ progress: 'handleProgress',
+ volumechange: 'handleVolumeChange',
+ webkitfullscreenchange: 'handleFullscreenChange',
+ webkitbeginfullscreen: 'handleFullscreenChange',
+ webkitendfullscreen: 'handleFullscreenChange',
+ },
+ PlaceholderPollingDelay: 33,
+ HideControlsDelay: 4 * 1000,
+ RewindAmount: 30,
+ MaximumSeekRate: 8,
+ SeekDelay: 1500,
+ ClassNames: {
+ active: 'active',
+ dropped: 'dropped',
+ exit: 'exit',
+ failed: 'failed',
+ hidden: 'hidden',
+ hiding: 'hiding',
+ threeDigitTime: 'three-digit-time',
+ fourDigitTime: 'four-digit-time',
+ fiveDigitTime: 'five-digit-time',
+ sixDigitTime: 'six-digit-time',
+ list: 'list',
+ muteBox: 'mute-box',
+ muted: 'muted',
+ paused: 'paused',
+ pictureInPicture: 'picture-in-picture',
+ playing: 'playing',
+ returnFromPictureInPicture: 'return-from-picture-in-picture',
+ selected: 'selected',
+ show: 'show',
+ small: 'small',
+ thumbnail: 'thumbnail',
+ thumbnailImage: 'thumbnail-image',
+ thumbnailTrack: 'thumbnail-track',
+ volumeBox: 'volume-box',
+ noVideo: 'no-video',
+ down: 'down',
+ out: 'out',
+ pictureInPictureButton: 'picture-in-picture-button',
+ placeholderShowing: 'placeholder-showing',
+ usesLTRUserInterfaceLayoutDirection: 'uses-ltr-user-interface-layout-direction',
+ appleTV: 'appletv',
+ },
+ KeyCodes: {
+ enter: 13,
+ escape: 27,
+ space: 32,
+ pageUp: 33,
+ pageDown: 34,
+ end: 35,
+ home: 36,
+ left: 37,
+ up: 38,
+ right: 39,
+ down: 40
+ },
+ MinimumTimelineWidth: 80,
+ ButtonWidth: 32,
+
+ extend: function(child)
+ {
+ // This function doesn't actually do what we want it to. In particular it
+ // is not copying the getters and setters to the child class, since they are
+ // not enumerable. What we should do is use ES6 classes, or assign the __proto__
+ // directly.
+ // FIXME: Use ES6 classes.
+
+ for (var property in this) {
+ if (!child.hasOwnProperty(property))
+ child[property] = this[property];
+ }
+ },
+
+ get idiom()
+ {
+ return "apple";
+ },
+
+ UIString: function(developmentString, replaceString, replacementString)
+ {
+ var localized = UIStringTable[developmentString];
+ if (replaceString && replacementString)
+ return localized.replace(replaceString, replacementString);
+
+ if (localized)
+ return localized;
+
+ console.error("Localization for string \"" + developmentString + "\" not found.");
+ return "LOCALIZED STRING NOT FOUND";
+ },
+
+ listenFor: function(element, eventName, handler, useCapture)
+ {
+ if (typeof useCapture === 'undefined')
+ useCapture = false;
+
+ if (!(this.listeners[eventName] instanceof Array))
+ this.listeners[eventName] = [];
+ this.listeners[eventName].push({element:element, handler:handler, useCapture:useCapture});
+ element.addEventListener(eventName, this, useCapture);
+ },
+
+ stopListeningFor: function(element, eventName, handler, useCapture)
+ {
+ if (typeof useCapture === 'undefined')
+ useCapture = false;
+
+ if (!(this.listeners[eventName] instanceof Array))
+ return;
+
+ this.listeners[eventName] = this.listeners[eventName].filter(function(entry) {
+ return !(entry.element === element && entry.handler === handler && entry.useCapture === useCapture);
+ });
+ element.removeEventListener(eventName, this, useCapture);
+ },
+
+ addVideoListeners: function()
+ {
+ for (var name in this.HandledVideoEvents) {
+ this.listenFor(this.video, name, this.HandledVideoEvents[name]);
+ };
+
+ /* text tracks */
+ this.listenFor(this.video.textTracks, 'change', this.handleTextTrackChange);
+ this.listenFor(this.video.textTracks, 'addtrack', this.handleTextTrackAdd);
+ this.listenFor(this.video.textTracks, 'removetrack', this.handleTextTrackRemove);
+
+ /* audio tracks */
+ this.listenFor(this.video.audioTracks, 'change', this.handleAudioTrackChange);
+ this.listenFor(this.video.audioTracks, 'addtrack', this.handleAudioTrackAdd);
+ this.listenFor(this.video.audioTracks, 'removetrack', this.handleAudioTrackRemove);
+
+ /* video tracks */
+ this.listenFor(this.video.videoTracks, 'change', this.updateHasVideo);
+ this.listenFor(this.video.videoTracks, 'addtrack', this.updateHasVideo);
+ this.listenFor(this.video.videoTracks, 'removetrack', this.updateHasVideo);
+
+ /* controls attribute */
+ this.controlsObserver = new MutationObserver(this.handleControlsChange.bind(this));
+ this.controlsObserver.observe(this.video, { attributes: true, attributeFilter: ['controls'] });
+
+ this.listenFor(this.video, 'webkitcurrentplaybacktargetiswirelesschanged', this.handleWirelessPlaybackChange);
+
+ if ('webkitPresentationMode' in this.video)
+ this.listenFor(this.video, 'webkitpresentationmodechanged', this.handlePresentationModeChange);
+ },
+
+ removeVideoListeners: function()
+ {
+ for (var name in this.HandledVideoEvents) {
+ this.stopListeningFor(this.video, name, this.HandledVideoEvents[name]);
+ };
+
+ /* text tracks */
+ this.stopListeningFor(this.video.textTracks, 'change', this.handleTextTrackChange);
+ this.stopListeningFor(this.video.textTracks, 'addtrack', this.handleTextTrackAdd);
+ this.stopListeningFor(this.video.textTracks, 'removetrack', this.handleTextTrackRemove);
+
+ /* audio tracks */
+ this.stopListeningFor(this.video.audioTracks, 'change', this.handleAudioTrackChange);
+ this.stopListeningFor(this.video.audioTracks, 'addtrack', this.handleAudioTrackAdd);
+ this.stopListeningFor(this.video.audioTracks, 'removetrack', this.handleAudioTrackRemove);
+
+ /* video tracks */
+ this.stopListeningFor(this.video.videoTracks, 'change', this.updateHasVideo);
+ this.stopListeningFor(this.video.videoTracks, 'addtrack', this.updateHasVideo);
+ this.stopListeningFor(this.video.videoTracks, 'removetrack', this.updateHasVideo);
+
+ /* controls attribute */
+ this.controlsObserver.disconnect();
+ delete(this.controlsObserver);
+
+ this.stopListeningFor(this.video, 'webkitcurrentplaybacktargetiswirelesschanged', this.handleWirelessPlaybackChange);
+ this.setShouldListenForPlaybackTargetAvailabilityEvent(false);
+
+ if ('webkitPresentationMode' in this.video)
+ this.stopListeningFor(this.video, 'webkitpresentationmodechanged', this.handlePresentationModeChange);
+ },
+
+ handleEvent: function(event)
+ {
+ var preventDefault = false;
+
+ try {
+ if (event.target === this.video) {
+ var handlerName = this.HandledVideoEvents[event.type];
+ var handler = this[handlerName];
+ if (handler && handler instanceof Function)
+ handler.call(this, event);
+ }
+
+ if (!(this.listeners[event.type] instanceof Array))
+ return;
+
+ this.listeners[event.type].forEach(function(entry) {
+ if (entry.element === event.currentTarget && entry.handler instanceof Function)
+ preventDefault |= entry.handler.call(this, event);
+ }, this);
+ } catch(e) {
+ if (window.console)
+ console.error(e);
+ }
+
+ if (preventDefault) {
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ },
+
+ createBase: function()
+ {
+ var base = this.base = document.createElement('div');
+ base.setAttribute('pseudo', '-webkit-media-controls');
+ this.listenFor(base, 'mousemove', this.handleWrapperMouseMove);
+ this.listenFor(this.video, 'mouseout', this.handleWrapperMouseOut);
+ if (this.host.textTrackContainer)
+ base.appendChild(this.host.textTrackContainer);
+ },
+
+ shouldHaveAnyUI: function()
+ {
+ return this.shouldHaveControls() || (this.video.textTracks && this.video.textTracks.length) || this.currentPlaybackTargetIsWireless();
+ },
+
+ shouldShowControls: function()
+ {
+ if (!this.isAudio() && !this.host.allowsInlineMediaPlayback)
+ return true;
+
+ return this.video.controls || this.isFullScreen();
+ },
+
+ shouldHaveControls: function()
+ {
+ return this.shouldShowControls() || this.isFullScreen() || this.presentationMode() === 'picture-in-picture' || this.currentPlaybackTargetIsWireless();
+ },
+
+
+ setNeedsTimelineMetricsUpdate: function()
+ {
+ this.timelineMetricsNeedsUpdate = true;
+ },
+
+ scheduleUpdateLayoutForDisplayedWidth: function()
+ {
+ setTimeout(this.updateLayoutForDisplayedWidth.bind(this), 0);
+ },
+
+ updateTimelineMetricsIfNeeded: function()
+ {
+ if (this.timelineMetricsNeedsUpdate && !this.controlsAreHidden()) {
+ this.timelineLeft = this.controls.timeline.offsetLeft;
+ this.timelineWidth = this.controls.timeline.offsetWidth;
+ this.timelineHeight = this.controls.timeline.offsetHeight;
+ this.timelineMetricsNeedsUpdate = false;
+ }
+ },
+
+ updateBase: function()
+ {
+ if (this.shouldHaveAnyUI()) {
+ if (!this.base.parentNode) {
+ this.root.appendChild(this.base);
+ }
+ } else {
+ if (this.base.parentNode) {
+ this.base.parentNode.removeChild(this.base);
+ }
+ }
+ },
+
+ createControls: function()
+ {
+ var panel = this.controls.panel = document.createElement('div');
+ panel.setAttribute('pseudo', '-webkit-media-controls-panel');
+ panel.setAttribute('aria-label', (this.isAudio() ? this.UIString('Audio Playback') : this.UIString('Video Playback')));
+ panel.setAttribute('role', 'toolbar');
+ this.listenFor(panel, 'mousedown', this.handlePanelMouseDown);
+ this.listenFor(panel, 'transitionend', this.handlePanelTransitionEnd);
+ this.listenFor(panel, 'click', this.handlePanelClick);
+ this.listenFor(panel, 'dblclick', this.handlePanelClick);
+ this.listenFor(panel, 'dragstart', this.handlePanelDragStart);
+
+ var panelBackgroundContainer = this.controls.panelBackgroundContainer = document.createElement('div');
+ panelBackgroundContainer.setAttribute('pseudo', '-webkit-media-controls-panel-background-container');
+
+ var panelTint = this.controls.panelTint = document.createElement('div');
+ panelTint.setAttribute('pseudo', '-webkit-media-controls-panel-tint');
+ this.listenFor(panelTint, 'mousedown', this.handlePanelMouseDown);
+ this.listenFor(panelTint, 'transitionend', this.handlePanelTransitionEnd);
+ this.listenFor(panelTint, 'click', this.handlePanelClick);
+ this.listenFor(panelTint, 'dblclick', this.handlePanelClick);
+ this.listenFor(panelTint, 'dragstart', this.handlePanelDragStart);
+
+ var panelBackground = this.controls.panelBackground = document.createElement('div');
+ panelBackground.setAttribute('pseudo', '-webkit-media-controls-panel-background');
+
+ var rewindButton = this.controls.rewindButton = document.createElement('button');
+ rewindButton.setAttribute('pseudo', '-webkit-media-controls-rewind-button');
+ rewindButton.setAttribute('aria-label', this.UIString('Rewind ##sec## Seconds', '##sec##', this.RewindAmount));
+ this.listenFor(rewindButton, 'click', this.handleRewindButtonClicked);
+
+ var seekBackButton = this.controls.seekBackButton = document.createElement('button');
+ seekBackButton.setAttribute('pseudo', '-webkit-media-controls-seek-back-button');
+ seekBackButton.setAttribute('aria-label', this.UIString('Rewind'));
+ this.listenFor(seekBackButton, 'mousedown', this.handleSeekBackMouseDown);
+ this.listenFor(seekBackButton, 'mouseup', this.handleSeekBackMouseUp);
+
+ var seekForwardButton = this.controls.seekForwardButton = document.createElement('button');
+ seekForwardButton.setAttribute('pseudo', '-webkit-media-controls-seek-forward-button');
+ seekForwardButton.setAttribute('aria-label', this.UIString('Fast Forward'));
+ this.listenFor(seekForwardButton, 'mousedown', this.handleSeekForwardMouseDown);
+ this.listenFor(seekForwardButton, 'mouseup', this.handleSeekForwardMouseUp);
+
+ var playButton = this.controls.playButton = document.createElement('button');
+ playButton.setAttribute('pseudo', '-webkit-media-controls-play-button');
+ playButton.setAttribute('aria-label', this.UIString('Play'));
+ this.listenFor(playButton, 'click', this.handlePlayButtonClicked);
+
+ var statusDisplay = this.controls.statusDisplay = document.createElement('div');
+ statusDisplay.setAttribute('pseudo', '-webkit-media-controls-status-display');
+ statusDisplay.classList.add(this.ClassNames.hidden);
+
+ var timelineBox = this.controls.timelineBox = document.createElement('div');
+ timelineBox.setAttribute('pseudo', '-webkit-media-controls-timeline-container');
+
+ var currentTime = this.controls.currentTime = document.createElement('div');
+ currentTime.setAttribute('pseudo', '-webkit-media-controls-current-time-display');
+ currentTime.setAttribute('aria-label', this.UIString('Elapsed'));
+ currentTime.setAttribute('role', 'timer');
+
+ var timeline = this.controls.timeline = document.createElement('input');
+ timeline.setAttribute('pseudo', '-webkit-media-controls-timeline');
+ timeline.setAttribute('aria-label', this.UIString('Duration'));
+ timeline.type = 'range';
+ timeline.value = 0;
+ this.listenFor(timeline, 'input', this.handleTimelineInput);
+ this.listenFor(timeline, 'change', this.handleTimelineChange);
+ this.listenFor(timeline, 'mouseover', this.handleTimelineMouseOver);
+ this.listenFor(timeline, 'mouseout', this.handleTimelineMouseOut);
+ this.listenFor(timeline, 'mousemove', this.handleTimelineMouseMove);
+ this.listenFor(timeline, 'mousedown', this.handleTimelineMouseDown);
+ this.listenFor(timeline, 'mouseup', this.handleTimelineMouseUp);
+ this.listenFor(timeline, 'keydown', this.handleTimelineKeyDown);
+ timeline.step = .01;
+
+ this.timelineContextName = "_webkit-media-controls-timeline-" + this.host.generateUUID();
+ timeline.style.backgroundImage = '-webkit-canvas(' + this.timelineContextName + ')';
+
+ var thumbnailTrack = this.controls.thumbnailTrack = document.createElement('div');
+ thumbnailTrack.classList.add(this.ClassNames.thumbnailTrack);
+
+ var thumbnail = this.controls.thumbnail = document.createElement('div');
+ thumbnail.classList.add(this.ClassNames.thumbnail);
+
+ var thumbnailImage = this.controls.thumbnailImage = document.createElement('img');
+ thumbnailImage.classList.add(this.ClassNames.thumbnailImage);
+
+ var remainingTime = this.controls.remainingTime = document.createElement('div');
+ remainingTime.setAttribute('pseudo', '-webkit-media-controls-time-remaining-display');
+ remainingTime.setAttribute('aria-label', this.UIString('Remaining'));
+ remainingTime.setAttribute('role', 'timer');
+
+ var muteBox = this.controls.muteBox = document.createElement('div');
+ muteBox.classList.add(this.ClassNames.muteBox);
+ this.listenFor(muteBox, 'mouseover', this.handleMuteBoxOver);
+
+ var muteButton = this.controls.muteButton = document.createElement('button');
+ muteButton.setAttribute('pseudo', '-webkit-media-controls-mute-button');
+ muteButton.setAttribute('aria-label', this.UIString('Mute'));
+ // Make the mute button a checkbox since it only has on/off states.
+ muteButton.setAttribute('role', 'checkbox');
+ this.listenFor(muteButton, 'click', this.handleMuteButtonClicked);
+
+ var minButton = this.controls.minButton = document.createElement('button');
+ minButton.setAttribute('pseudo', '-webkit-media-controls-volume-min-button');
+ minButton.setAttribute('aria-label', this.UIString('Minimum Volume'));
+ this.listenFor(minButton, 'click', this.handleMinButtonClicked);
+
+ var maxButton = this.controls.maxButton = document.createElement('button');
+ maxButton.setAttribute('pseudo', '-webkit-media-controls-volume-max-button');
+ maxButton.setAttribute('aria-label', this.UIString('Maximum Volume'));
+ this.listenFor(maxButton, 'click', this.handleMaxButtonClicked);
+
+ var volumeBox = this.controls.volumeBox = document.createElement('div');
+ volumeBox.setAttribute('pseudo', '-webkit-media-controls-volume-slider-container');
+ volumeBox.classList.add(this.ClassNames.volumeBox);
+
+ var volumeBoxBackground = this.controls.volumeBoxBackground = document.createElement('div');
+ volumeBoxBackground.setAttribute('pseudo', '-webkit-media-controls-volume-slider-container-background');
+
+ var volumeBoxTint = this.controls.volumeBoxTint = document.createElement('div');
+ volumeBoxTint.setAttribute('pseudo', '-webkit-media-controls-volume-slider-container-tint');
+
+ var volume = this.controls.volume = document.createElement('input');
+ volume.setAttribute('pseudo', '-webkit-media-controls-volume-slider');
+ volume.setAttribute('aria-label', this.UIString('Volume'));
+ volume.type = 'range';
+ volume.min = 0;
+ volume.max = 1;
+ volume.step = .05;
+ this.listenFor(volume, 'input', this.handleVolumeSliderInput);
+ this.listenFor(volume, 'change', this.handleVolumeSliderChange);
+ this.listenFor(volume, 'mousedown', this.handleVolumeSliderMouseDown);
+ this.listenFor(volume, 'mouseup', this.handleVolumeSliderMouseUp);
+
+ this.volumeContextName = "_webkit-media-controls-volume-" + this.host.generateUUID();
+ volume.style.backgroundImage = '-webkit-canvas(' + this.volumeContextName + ')';
+
+ var captionButton = this.controls.captionButton = document.createElement('button');
+ captionButton.setAttribute('pseudo', '-webkit-media-controls-toggle-closed-captions-button');
+ captionButton.setAttribute('aria-label', this.UIString('Captions'));
+ captionButton.setAttribute('aria-haspopup', 'true');
+ captionButton.setAttribute('aria-owns', 'audioAndTextTrackMenu');
+ this.listenFor(captionButton, 'click', this.handleCaptionButtonClicked);
+
+ var fullscreenButton = this.controls.fullscreenButton = document.createElement('button');
+ fullscreenButton.setAttribute('pseudo', '-webkit-media-controls-fullscreen-button');
+ fullscreenButton.setAttribute('aria-label', this.UIString('Display Full Screen'));
+ this.listenFor(fullscreenButton, 'click', this.handleFullscreenButtonClicked);
+
+ var pictureInPictureButton = this.controls.pictureInPictureButton = document.createElement('button');
+ pictureInPictureButton.setAttribute('pseudo', '-webkit-media-controls-picture-in-picture-button');
+ pictureInPictureButton.setAttribute('aria-label', this.UIString('Display Picture in Picture'));
+ pictureInPictureButton.classList.add(this.ClassNames.pictureInPictureButton);
+ this.listenFor(pictureInPictureButton, 'click', this.handlePictureInPictureButtonClicked);
+
+ var inlinePlaybackPlaceholder = this.controls.inlinePlaybackPlaceholder = document.createElement('div');
+ inlinePlaybackPlaceholder.setAttribute('pseudo', '-webkit-media-controls-wireless-playback-status');
+ inlinePlaybackPlaceholder.setAttribute('aria-label', this.UIString('Video Playback Placeholder'));
+ this.listenFor(inlinePlaybackPlaceholder, 'click', this.handlePlaceholderClick);
+ this.listenFor(inlinePlaybackPlaceholder, 'dblclick', this.handlePlaceholderClick);
+ if (!Controller.gSimulatePictureInPictureAvailable)
+ inlinePlaybackPlaceholder.classList.add(this.ClassNames.hidden);
+
+ var inlinePlaybackPlaceholderText = this.controls.inlinePlaybackPlaceholderText = document.createElement('div');
+ inlinePlaybackPlaceholderText.setAttribute('pseudo', '-webkit-media-controls-wireless-playback-text');
+
+ var inlinePlaybackPlaceholderTextTop = this.controls.inlinePlaybackPlaceholderTextTop = document.createElement('p');
+ inlinePlaybackPlaceholderTextTop.setAttribute('pseudo', '-webkit-media-controls-wireless-playback-text-top');
+
+ var inlinePlaybackPlaceholderTextBottom = this.controls.inlinePlaybackPlaceholderTextBottom = document.createElement('p');
+ inlinePlaybackPlaceholderTextBottom.setAttribute('pseudo', '-webkit-media-controls-wireless-playback-text-bottom');
+
+ var wirelessTargetPicker = this.controls.wirelessTargetPicker = document.createElement('button');
+ wirelessTargetPicker.setAttribute('pseudo', '-webkit-media-controls-wireless-playback-picker-button');
+ wirelessTargetPicker.setAttribute('aria-label', this.UIString('Choose Wireless Display'));
+ this.listenFor(wirelessTargetPicker, 'click', this.handleWirelessPickerButtonClicked);
+
+ // Show controls button is an accessibility workaround since the controls are now removed from the DOM. http://webkit.org/b/145684
+ var showControlsButton = this.showControlsButton = document.createElement('button');
+ showControlsButton.setAttribute('pseudo', '-webkit-media-show-controls');
+ this.showShowControlsButton(false);
+ showControlsButton.setAttribute('aria-label', this.UIString('Show Controls'));
+ this.listenFor(showControlsButton, 'click', this.handleShowControlsClick);
+ this.base.appendChild(showControlsButton);
+
+ if (!Controller.gSimulateWirelessPlaybackTarget)
+ wirelessTargetPicker.classList.add(this.ClassNames.hidden);
+ },
+
+ createTimeClones: function()
+ {
+ var currentTimeClone = this.currentTimeClone = document.createElement('div');
+ currentTimeClone.setAttribute('pseudo', '-webkit-media-controls-current-time-display');
+ currentTimeClone.setAttribute('aria-hidden', 'true');
+ currentTimeClone.classList.add('clone');
+ this.base.appendChild(currentTimeClone);
+
+ var remainingTimeClone = this.remainingTimeClone = document.createElement('div');
+ remainingTimeClone.setAttribute('pseudo', '-webkit-media-controls-time-remaining-display');
+ remainingTimeClone.setAttribute('aria-hidden', 'true');
+ remainingTimeClone.classList.add('clone');
+ this.base.appendChild(remainingTimeClone);
+ },
+
+ setControlsType: function(type)
+ {
+ if (type === this.controlsType)
+ return;
+ this.controlsType = type;
+
+ this.reconnectControls();
+ this.updateShouldListenForPlaybackTargetAvailabilityEvent();
+ },
+
+ setIsLive: function(live)
+ {
+ if (live === this.isLive)
+ return;
+ this.isLive = live;
+
+ this.updateStatusDisplay();
+
+ this.reconnectControls();
+ },
+
+ reconnectControls: function()
+ {
+ this.disconnectControls();
+
+ if (this.controlsType === Controller.InlineControls)
+ this.configureInlineControls();
+ else if (this.controlsType == Controller.FullScreenControls)
+ this.configureFullScreenControls();
+ if (this.shouldHaveControls() || this.currentPlaybackTargetIsWireless())
+ this.addControls();
+ },
+
+ disconnectControls: function(event)
+ {
+ for (var item in this.controls) {
+ var control = this.controls[item];
+ if (control && control.parentNode)
+ control.parentNode.removeChild(control);
+ }
+ },
+
+ configureInlineControls: function()
+ {
+ this.controls.inlinePlaybackPlaceholder.appendChild(this.controls.inlinePlaybackPlaceholderText);
+ this.controls.inlinePlaybackPlaceholderText.appendChild(this.controls.inlinePlaybackPlaceholderTextTop);
+ this.controls.inlinePlaybackPlaceholderText.appendChild(this.controls.inlinePlaybackPlaceholderTextBottom);
+ this.controls.panel.appendChild(this.controls.panelBackgroundContainer);
+ this.controls.panelBackgroundContainer.appendChild(this.controls.panelBackground);
+ this.controls.panelBackgroundContainer.appendChild(this.controls.panelTint);
+ this.controls.panel.appendChild(this.controls.playButton);
+ if (!this.isLive)
+ this.controls.panel.appendChild(this.controls.rewindButton);
+ this.controls.panel.appendChild(this.controls.statusDisplay);
+ if (!this.isLive) {
+ this.controls.panel.appendChild(this.controls.timelineBox);
+ this.controls.timelineBox.appendChild(this.controls.currentTime);
+ this.controls.timelineBox.appendChild(this.controls.thumbnailTrack);
+ this.controls.thumbnailTrack.appendChild(this.controls.timeline);
+ this.controls.thumbnailTrack.appendChild(this.controls.thumbnail);
+ this.controls.thumbnail.appendChild(this.controls.thumbnailImage);
+ this.controls.timelineBox.appendChild(this.controls.remainingTime);
+ }
+ this.controls.panel.appendChild(this.controls.muteBox);
+ this.controls.muteBox.appendChild(this.controls.volumeBox);
+ this.controls.volumeBox.appendChild(this.controls.volumeBoxBackground);
+ this.controls.volumeBox.appendChild(this.controls.volumeBoxTint);
+ this.controls.volumeBox.appendChild(this.controls.volume);
+ this.controls.muteBox.appendChild(this.controls.muteButton);
+ this.controls.panel.appendChild(this.controls.wirelessTargetPicker);
+ this.controls.panel.appendChild(this.controls.captionButton);
+ if (!this.isAudio()) {
+ this.updatePictureInPictureButton();
+ this.controls.panel.appendChild(this.controls.fullscreenButton);
+ }
+
+ this.controls.panel.style.removeProperty('left');
+ this.controls.panel.style.removeProperty('top');
+ this.controls.panel.style.removeProperty('bottom');
+ },
+
+ configureFullScreenControls: function()
+ {
+ this.controls.inlinePlaybackPlaceholder.appendChild(this.controls.inlinePlaybackPlaceholderText);
+ this.controls.inlinePlaybackPlaceholderText.appendChild(this.controls.inlinePlaybackPlaceholderTextTop);
+ this.controls.inlinePlaybackPlaceholderText.appendChild(this.controls.inlinePlaybackPlaceholderTextBottom);
+ this.controls.panel.appendChild(this.controls.panelBackground);
+ this.controls.panel.appendChild(this.controls.panelTint);
+ this.controls.panel.appendChild(this.controls.volumeBox);
+ this.controls.volumeBox.appendChild(this.controls.minButton);
+ this.controls.volumeBox.appendChild(this.controls.volume);
+ this.controls.volumeBox.appendChild(this.controls.maxButton);
+ this.controls.panel.appendChild(this.controls.seekBackButton);
+ this.controls.panel.appendChild(this.controls.playButton);
+ this.controls.panel.appendChild(this.controls.seekForwardButton);
+ this.controls.panel.appendChild(this.controls.wirelessTargetPicker);
+ this.controls.panel.appendChild(this.controls.captionButton);
+ if (!this.isAudio()) {
+ this.updatePictureInPictureButton();
+ this.controls.panel.appendChild(this.controls.fullscreenButton);
+ }
+ if (!this.isLive) {
+ this.controls.panel.appendChild(this.controls.timelineBox);
+ this.controls.timelineBox.appendChild(this.controls.currentTime);
+ this.controls.timelineBox.appendChild(this.controls.thumbnailTrack);
+ this.controls.thumbnailTrack.appendChild(this.controls.timeline);
+ this.controls.thumbnailTrack.appendChild(this.controls.thumbnail);
+ this.controls.thumbnail.appendChild(this.controls.thumbnailImage);
+ this.controls.timelineBox.appendChild(this.controls.remainingTime);
+ } else
+ this.controls.panel.appendChild(this.controls.statusDisplay);
+ },
+
+ updateControls: function()
+ {
+ if (this.isFullScreen())
+ this.setControlsType(Controller.FullScreenControls);
+ else
+ this.setControlsType(Controller.InlineControls);
+
+ this.setNeedsUpdateForDisplayedWidth();
+ this.updateLayoutForDisplayedWidth();
+ this.setNeedsTimelineMetricsUpdate();
+
+ if (this.shouldShowControls()) {
+ this.controls.panel.classList.add(this.ClassNames.show);
+ this.controls.panel.classList.remove(this.ClassNames.hidden);
+ this.resetHideControlsTimer();
+ this.showShowControlsButton(false);
+ } else {
+ this.controls.panel.classList.remove(this.ClassNames.show);
+ this.controls.panel.classList.add(this.ClassNames.hidden);
+ this.showShowControlsButton(true);
+ }
+ },
+
+ isPlayable: function()
+ {
+ return this.video.readyState > HTMLMediaElement.HAVE_NOTHING && !this.video.error;
+ },
+
+ updateStatusDisplay: function(event)
+ {
+ this.updateShouldListenForPlaybackTargetAvailabilityEvent();
+ if (this.video.error !== null)
+ this.controls.statusDisplay.innerText = this.UIString('Error');
+ else if (this.isLive && this.video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA)
+ this.controls.statusDisplay.innerText = this.UIString('Live Broadcast');
+ else if (!this.isPlayable() && this.video.networkState === HTMLMediaElement.NETWORK_LOADING)
+ this.controls.statusDisplay.innerText = this.UIString('Loading');
+ else
+ this.controls.statusDisplay.innerText = '';
+
+ this.setStatusHidden(!this.isLive && this.isPlayable());
+ },
+
+ handleLoadStart: function(event)
+ {
+ this.updateStatusDisplay();
+ this.updateProgress();
+ },
+
+ handleError: function(event)
+ {
+ this.updateStatusDisplay();
+ },
+
+ handleAbort: function(event)
+ {
+ this.updateStatusDisplay();
+ },
+
+ handleSuspend: function(event)
+ {
+ this.updateStatusDisplay();
+ },
+
+ handleStalled: function(event)
+ {
+ this.updateStatusDisplay();
+ this.updateProgress();
+ },
+
+ handleWaiting: function(event)
+ {
+ this.updateStatusDisplay();
+ },
+
+ handleReadyStateChange: function(event)
+ {
+ this.updateReadyState();
+ this.updateDuration();
+ this.updateCaptionButton();
+ this.updateCaptionContainer();
+ this.updateFullscreenButtons();
+ this.updateWirelessTargetAvailable();
+ this.updateWirelessTargetPickerButton();
+ this.updateProgress();
+ this.updateControls();
+ },
+
+ handleTimeUpdate: function(event)
+ {
+ if (!this.scrubbing) {
+ this.updateTime();
+ this.updateProgress();
+ }
+ this.drawTimelineBackground();
+ },
+
+ handleDurationChange: function(event)
+ {
+ this.updateDuration();
+ this.updateTime();
+ this.updateProgress();
+ },
+
+ handlePlay: function(event)
+ {
+ this.setPlaying(true);
+ },
+
+ handlePause: function(event)
+ {
+ this.setPlaying(false);
+ },
+
+ handleProgress: function(event)
+ {
+ this.updateProgress();
+ },
+
+ handleVolumeChange: function(event)
+ {
+ this.updateVolume();
+ },
+
+ handleTextTrackChange: function(event)
+ {
+ this.updateCaptionContainer();
+ },
+
+ handleTextTrackAdd: function(event)
+ {
+ var track = event.track;
+
+ if (this.trackHasThumbnails(track) && track.mode === 'disabled')
+ track.mode = 'hidden';
+
+ this.updateThumbnail();
+ this.updateCaptionButton();
+ this.updateCaptionContainer();
+ },
+
+ handleTextTrackRemove: function(event)
+ {
+ this.updateThumbnail();
+ this.updateCaptionButton();
+ this.updateCaptionContainer();
+ },
+
+ handleAudioTrackChange: function(event)
+ {
+ this.updateHasAudio();
+ },
+
+ handleAudioTrackAdd: function(event)
+ {
+ this.updateHasAudio();
+ this.updateCaptionButton();
+ },
+
+ handleAudioTrackRemove: function(event)
+ {
+ this.updateHasAudio();
+ this.updateCaptionButton();
+ },
+
+ presentationMode: function() {
+ if ('webkitPresentationMode' in this.video)
+ return this.video.webkitPresentationMode;
+
+ if (this.isFullScreen())
+ return 'fullscreen';
+
+ return 'inline';
+ },
+
+ isFullScreen: function()
+ {
+ if (!this.video.webkitDisplayingFullscreen)
+ return false;
+
+ if ('webkitPresentationMode' in this.video && this.video.webkitPresentationMode === 'picture-in-picture')
+ return false;
+
+ return true;
+ },
+
+ updatePictureInPictureButton: function()
+ {
+ var shouldShowPictureInPictureButton = (Controller.gSimulatePictureInPictureAvailable || ('webkitSupportsPresentationMode' in this.video && this.video.webkitSupportsPresentationMode('picture-in-picture'))) && this.hasVideo();
+ if (shouldShowPictureInPictureButton) {
+ if (!this.controls.pictureInPictureButton.parentElement) {
+ if (this.controls.fullscreenButton.parentElement == this.controls.panel)
+ this.controls.panel.insertBefore(this.controls.pictureInPictureButton, this.controls.fullscreenButton);
+ else
+ this.controls.panel.appendChild(this.controls.pictureInPictureButton);
+ }
+ this.controls.pictureInPictureButton.classList.remove(this.ClassNames.hidden);
+ } else
+ this.controls.pictureInPictureButton.classList.add(this.ClassNames.hidden);
+ },
+
+ timelineStepFromVideoDuration: function()
+ {
+ var step;
+ var duration = this.video.duration;
+ if (duration <= 10)
+ step = .5;
+ else if (duration <= 60)
+ step = 1;
+ else if (duration <= 600)
+ step = 10;
+ else if (duration <= 3600)
+ step = 30;
+ else
+ step = 60;
+
+ return step;
+ },
+
+ incrementTimelineValue: function()
+ {
+ var value = this.video.currentTime + this.timelineStepFromVideoDuration();
+ return value > this.video.duration ? this.video.duration : value;
+ },
+
+ decrementTimelineValue: function()
+ {
+ var value = this.video.currentTime - this.timelineStepFromVideoDuration();
+ return value < 0 ? 0 : value;
+ },
+
+ showInlinePlaybackPlaceholderWhenSafe: function() {
+ if (this.presentationMode() != 'picture-in-picture')
+ return;
+
+ if (!this.host.isVideoLayerInline) {
+ this.controls.inlinePlaybackPlaceholder.classList.remove(this.ClassNames.hidden);
+ this.base.classList.add(this.ClassNames.placeholderShowing);
+ } else
+ setTimeout(this.showInlinePlaybackPlaceholderWhenSafe.bind(this), this.PlaceholderPollingDelay);
+ },
+
+ shouldReturnVideoLayerToInline: function()
+ {
+ var presentationMode = this.presentationMode();
+ return presentationMode === 'inline' || presentationMode === 'fullscreen';
+ },
+
+ updatePictureInPicturePlaceholder: function()
+ {
+ var presentationMode = this.presentationMode();
+
+ switch (presentationMode) {
+ case 'inline':
+ this.controls.panel.classList.remove(this.ClassNames.pictureInPicture);
+ this.controls.inlinePlaybackPlaceholder.classList.add(this.ClassNames.hidden);
+ this.controls.inlinePlaybackPlaceholder.classList.remove(this.ClassNames.pictureInPicture);
+ this.controls.inlinePlaybackPlaceholderTextTop.classList.remove(this.ClassNames.pictureInPicture);
+ this.controls.inlinePlaybackPlaceholderTextBottom.classList.remove(this.ClassNames.pictureInPicture);
+ this.base.classList.remove(this.ClassNames.placeholderShowing);
+
+ this.controls.pictureInPictureButton.classList.remove(this.ClassNames.returnFromPictureInPicture);
+ break;
+ case 'picture-in-picture':
+ this.controls.panel.classList.add(this.ClassNames.pictureInPicture);
+ this.controls.inlinePlaybackPlaceholder.classList.add(this.ClassNames.pictureInPicture);
+ this.showInlinePlaybackPlaceholderWhenSafe();
+
+ this.controls.inlinePlaybackPlaceholderTextTop.innerText = this.UIString('This video is playing in Picture in Picture');
+ this.controls.inlinePlaybackPlaceholderTextTop.classList.add(this.ClassNames.pictureInPicture);
+ this.controls.inlinePlaybackPlaceholderTextBottom.innerText = "";
+ this.controls.inlinePlaybackPlaceholderTextBottom.classList.add(this.ClassNames.pictureInPicture);
+
+ this.controls.pictureInPictureButton.classList.add(this.ClassNames.returnFromPictureInPicture);
+ break;
+ default:
+ this.controls.panel.classList.remove(this.ClassNames.pictureInPicture);
+ this.controls.inlinePlaybackPlaceholder.classList.remove(this.ClassNames.pictureInPicture);
+ this.controls.inlinePlaybackPlaceholderTextTop.classList.remove(this.ClassNames.pictureInPicture);
+ this.controls.inlinePlaybackPlaceholderTextBottom.classList.remove(this.ClassNames.pictureInPicture);
+
+ this.controls.pictureInPictureButton.classList.remove(this.ClassNames.returnFromPictureInPicture);
+ break;
+ }
+ },
+
+ handlePresentationModeChange: function(event)
+ {
+ this.updatePictureInPicturePlaceholder();
+ this.updateControls();
+ this.updateCaptionContainer();
+ this.resetHideControlsTimer();
+ if (this.presentationMode() != 'fullscreen' && this.video.paused && this.controlsAreHidden())
+ this.showControls();
+ this.host.setPreparedToReturnVideoLayerToInline(this.shouldReturnVideoLayerToInline());
+ },
+
+ handleFullscreenChange: function(event)
+ {
+ this.updateBase();
+ this.updateControls();
+ this.updateFullscreenButtons();
+ this.updateWirelessPlaybackStatus();
+
+ if (this.isFullScreen()) {
+ this.controls.fullscreenButton.classList.add(this.ClassNames.exit);
+ this.controls.fullscreenButton.setAttribute('aria-label', this.UIString('Exit Full Screen'));
+ this.host.enteredFullscreen();
+ } else {
+ this.controls.fullscreenButton.classList.remove(this.ClassNames.exit);
+ this.controls.fullscreenButton.setAttribute('aria-label', this.UIString('Display Full Screen'));
+ this.host.exitedFullscreen();
+ }
+
+ if ('webkitPresentationMode' in this.video)
+ this.handlePresentationModeChange(event);
+ },
+
+ handleShowControlsClick: function(event)
+ {
+ if (!this.video.controls && !this.isFullScreen())
+ return;
+
+ if (this.controlsAreHidden())
+ this.showControls(true);
+ },
+
+ handleWrapperMouseMove: function(event)
+ {
+ if (!this.video.controls && !this.isFullScreen())
+ return;
+
+ if (this.controlsAreHidden())
+ this.showControls();
+ this.resetHideControlsTimer();
+
+ if (!this.isDragging)
+ return;
+ var delta = new WebKitPoint(event.clientX - this.initialDragLocation.x, event.clientY - this.initialDragLocation.y);
+ this.controls.panel.style.left = this.initialOffset.x + delta.x + 'px';
+ this.controls.panel.style.top = this.initialOffset.y + delta.y + 'px';
+ event.stopPropagation()
+ },
+
+ handleWrapperMouseOut: function(event)
+ {
+ this.hideControls();
+ this.clearHideControlsTimer();
+ },
+
+ handleWrapperMouseUp: function(event)
+ {
+ this.isDragging = false;
+ this.stopListeningFor(this.base, 'mouseup', 'handleWrapperMouseUp', true);
+ },
+
+ handlePanelMouseDown: function(event)
+ {
+ if (event.target != this.controls.panelTint && event.target != this.controls.inlinePlaybackPlaceholder)
+ return;
+
+ if (!this.isFullScreen())
+ return;
+
+ this.listenFor(this.base, 'mouseup', this.handleWrapperMouseUp, true);
+ this.isDragging = true;
+ this.initialDragLocation = new WebKitPoint(event.clientX, event.clientY);
+ this.initialOffset = new WebKitPoint(
+ parseInt(this.controls.panel.style.left) | 0,
+ parseInt(this.controls.panel.style.top) | 0
+ );
+ },
+
+ handlePanelTransitionEnd: function(event)
+ {
+ var opacity = window.getComputedStyle(this.controls.panel).opacity;
+ if (!parseInt(opacity) && !this.controlsAlwaysVisible() && (this.video.controls || this.isFullScreen())) {
+ this.base.removeChild(this.controls.inlinePlaybackPlaceholder);
+ this.base.removeChild(this.controls.panel);
+ }
+ },
+
+ handlePanelClick: function(event)
+ {
+ // Prevent clicks in the panel from playing or pausing the video in a MediaDocument.
+ event.preventDefault();
+ },
+
+ handlePanelDragStart: function(event)
+ {
+ // Prevent drags in the panel from triggering a drag event on the <video> element.
+ event.preventDefault();
+ },
+
+ handlePlaceholderClick: function(event)
+ {
+ // Prevent clicks in the placeholder from playing or pausing the video in a MediaDocument.
+ event.preventDefault();
+ },
+
+ handleRewindButtonClicked: function(event)
+ {
+ var newTime = Math.max(
+ this.video.currentTime - this.RewindAmount,
+ this.video.seekable.start(0));
+ this.video.currentTime = newTime;
+ return true;
+ },
+
+ canPlay: function()
+ {
+ return this.video.paused || this.video.ended || this.video.readyState < HTMLMediaElement.HAVE_METADATA;
+ },
+
+ handlePlayButtonClicked: function(event)
+ {
+ if (this.canPlay()) {
+ this.canToggleShowControlsButton = true;
+ this.video.play();
+ } else
+ this.video.pause();
+ return true;
+ },
+
+ handleTimelineInput: function(event)
+ {
+ if (this.scrubbing)
+ this.video.pause();
+
+ this.video.fastSeek(this.controls.timeline.value);
+ this.updateControlsWhileScrubbing();
+ },
+
+ handleTimelineChange: function(event)
+ {
+ this.video.currentTime = this.controls.timeline.value;
+ this.updateProgress();
+ },
+
+ handleTimelineDown: function(event)
+ {
+ this.controls.thumbnail.classList.add(this.ClassNames.show);
+ },
+
+ handleTimelineUp: function(event)
+ {
+ this.controls.thumbnail.classList.remove(this.ClassNames.show);
+ },
+
+ handleTimelineMouseOver: function(event)
+ {
+ this.controls.thumbnail.classList.add(this.ClassNames.show);
+ },
+
+ handleTimelineMouseOut: function(event)
+ {
+ this.controls.thumbnail.classList.remove(this.ClassNames.show);
+ },
+
+ handleTimelineMouseMove: function(event)
+ {
+ if (this.controls.thumbnail.classList.contains(this.ClassNames.hidden))
+ return;
+
+ this.updateTimelineMetricsIfNeeded();
+ this.controls.thumbnail.classList.add(this.ClassNames.show);
+ var localPoint = webkitConvertPointFromPageToNode(this.controls.timeline, new WebKitPoint(event.clientX, event.clientY));
+ var percent = (localPoint.x - this.timelineLeft) / this.timelineWidth;
+ percent = Math.max(Math.min(1, percent), 0);
+ this.controls.thumbnail.style.left = percent * 100 + '%';
+
+ var thumbnailTime = percent * this.video.duration;
+ for (var i = 0; i < this.video.textTracks.length; ++i) {
+ var track = this.video.textTracks[i];
+ if (!this.trackHasThumbnails(track))
+ continue;
+
+ if (!track.cues)
+ continue;
+
+ for (var j = 0; j < track.cues.length; ++j) {
+ var cue = track.cues[j];
+ if (thumbnailTime >= cue.startTime && thumbnailTime < cue.endTime) {
+ this.controls.thumbnailImage.src = cue.text;
+ return;
+ }
+ }
+ }
+ },
+
+ handleTimelineMouseDown: function(event)
+ {
+ this.scrubbing = true;
+ },
+
+ handleTimelineMouseUp: function(event)
+ {
+ this.scrubbing = false;
+ },
+
+ handleTimelineKeyDown: function(event)
+ {
+ if (event.keyCode == this.KeyCodes.left)
+ this.controls.timeline.value = this.decrementTimelineValue();
+ else if (event.keyCode == this.KeyCodes.right)
+ this.controls.timeline.value = this.incrementTimelineValue();
+ },
+
+ handleMuteButtonClicked: function(event)
+ {
+ this.video.muted = !this.video.muted;
+ if (this.video.muted)
+ this.controls.muteButton.setAttribute('aria-checked', 'true');
+ else
+ this.controls.muteButton.setAttribute('aria-checked', 'false');
+ this.drawVolumeBackground();
+ return true;
+ },
+
+ handleMuteBoxOver: function(event)
+ {
+ this.drawVolumeBackground();
+ },
+
+ handleMinButtonClicked: function(event)
+ {
+ if (this.video.muted) {
+ this.video.muted = false;
+ this.controls.muteButton.setAttribute('aria-checked', 'false');
+ }
+ this.video.volume = 0;
+ return true;
+ },
+
+ handleMaxButtonClicked: function(event)
+ {
+ if (this.video.muted) {
+ this.video.muted = false;
+ this.controls.muteButton.setAttribute('aria-checked', 'false');
+ }
+ this.video.volume = 1;
+ },
+
+ updateVideoVolume: function()
+ {
+ if (this.video.muted) {
+ this.video.muted = false;
+ this.controls.muteButton.setAttribute('aria-checked', 'false');
+ }
+ this.video.volume = this.controls.volume.value;
+ this.controls.volume.setAttribute('aria-valuetext', `${parseInt(this.controls.volume.value * 100)}%`);
+ },
+
+ handleVolumeSliderInput: function(event)
+ {
+ this.updateVideoVolume();
+ this.drawVolumeBackground();
+ },
+
+ handleVolumeSliderChange: function(event)
+ {
+ this.updateVideoVolume();
+ },
+
+ handleVolumeSliderMouseDown: function(event)
+ {
+ this.isVolumeSliderActive = true;
+ this.drawVolumeBackground();
+ },
+
+ handleVolumeSliderMouseUp: function(event)
+ {
+ this.isVolumeSliderActive = false;
+ this.drawVolumeBackground();
+ },
+
+ handleCaptionButtonClicked: function(event)
+ {
+ if (this.captionMenu)
+ this.destroyCaptionMenu();
+ else
+ this.buildCaptionMenu();
+ return true;
+ },
+
+ hasVideo: function()
+ {
+ return this.video.videoTracks && this.video.videoTracks.length;
+ },
+
+ updateFullscreenButtons: function()
+ {
+ var shouldBeHidden = !this.video.webkitSupportsFullscreen || !this.hasVideo();
+ this.controls.fullscreenButton.classList.toggle(this.ClassNames.hidden, shouldBeHidden && !this.isFullScreen());
+ this.updatePictureInPictureButton();
+ this.setNeedsUpdateForDisplayedWidth();
+ this.updateLayoutForDisplayedWidth();
+ },
+
+ handleFullscreenButtonClicked: function(event)
+ {
+ if (this.isFullScreen())
+ this.video.webkitExitFullscreen();
+ else
+ this.video.webkitEnterFullscreen();
+ return true;
+ },
+
+ updateWirelessTargetPickerButton: function() {
+ var wirelessTargetPickerColor;
+ if (this.controls.wirelessTargetPicker.classList.contains('playing'))
+ wirelessTargetPickerColor = "-apple-wireless-playback-target-active";
+ else
+ wirelessTargetPickerColor = "rgba(255,255,255,0.45)";
+ if (window.devicePixelRatio == 2)
+ this.controls.wirelessTargetPicker.style.backgroundImage = "url(\"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 15' stroke='" + wirelessTargetPickerColor + "'><defs> <clipPath fill-rule='evenodd' id='cut-hole'><path d='M 0,0.5 L 16,0.5 L 16,15.5 L 0,15.5 z M 0,14.5 L 16,14.5 L 8,5 z'/></clipPath></defs><rect fill='none' clip-path='url(#cut-hole)' x='0.5' y='2' width='15' height='8'/><path stroke='none' fill='" + wirelessTargetPickerColor +"' d='M 3.5,13.25 L 12.5,13.25 L 8,8 z'/></svg>\")";
+ else
+ this.controls.wirelessTargetPicker.style.backgroundImage = "url(\"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 15' stroke='" + wirelessTargetPickerColor + "'><defs> <clipPath fill-rule='evenodd' id='cut-hole'><path d='M 0,1 L 16,1 L 16,16 L 0,16 z M 0,15 L 16,15 L 8,5.5 z'/></clipPath></defs><rect fill='none' clip-path='url(#cut-hole)' x='0.5' y='2.5' width='15' height='8'/><path stroke='none' fill='" + wirelessTargetPickerColor +"' d='M 2.75,14 L 13.25,14 L 8,8.75 z'/></svg>\")";
+ },
+
+ handleControlsChange: function()
+ {
+ try {
+ this.updateBase();
+
+ if (this.shouldHaveControls() && !this.hasControls())
+ this.addControls();
+ else if (!this.shouldHaveControls() && this.hasControls())
+ this.removeControls();
+ } catch(e) {
+ if (window.console)
+ console.error(e);
+ }
+ },
+
+ nextRate: function()
+ {
+ return Math.min(this.MaximumSeekRate, Math.abs(this.video.playbackRate * 2));
+ },
+
+ handleSeekBackMouseDown: function(event)
+ {
+ this.actionAfterSeeking = (this.canPlay() ? Controller.PauseAfterSeeking : Controller.PlayAfterSeeking);
+ this.video.play();
+ this.video.playbackRate = this.nextRate() * -1;
+ this.seekInterval = setInterval(this.seekBackFaster.bind(this), this.SeekDelay);
+ },
+
+ seekBackFaster: function()
+ {
+ this.video.playbackRate = this.nextRate() * -1;
+ },
+
+ handleSeekBackMouseUp: function(event)
+ {
+ this.video.playbackRate = this.video.defaultPlaybackRate;
+ if (this.actionAfterSeeking === Controller.PauseAfterSeeking)
+ this.video.pause();
+ else if (this.actionAfterSeeking === Controller.PlayAfterSeeking)
+ this.video.play();
+ if (this.seekInterval)
+ clearInterval(this.seekInterval);
+ },
+
+ handleSeekForwardMouseDown: function(event)
+ {
+ this.actionAfterSeeking = (this.canPlay() ? Controller.PauseAfterSeeking : Controller.PlayAfterSeeking);
+ this.video.play();
+ this.video.playbackRate = this.nextRate();
+ this.seekInterval = setInterval(this.seekForwardFaster.bind(this), this.SeekDelay);
+ },
+
+ seekForwardFaster: function()
+ {
+ this.video.playbackRate = this.nextRate();
+ },
+
+ handleSeekForwardMouseUp: function(event)
+ {
+ this.video.playbackRate = this.video.defaultPlaybackRate;
+ if (this.actionAfterSeeking === Controller.PauseAfterSeeking)
+ this.video.pause();
+ else if (this.actionAfterSeeking === Controller.PlayAfterSeeking)
+ this.video.play();
+ if (this.seekInterval)
+ clearInterval(this.seekInterval);
+ },
+
+ updateDuration: function()
+ {
+ var duration = this.video.duration;
+ this.controls.timeline.min = 0;
+ this.controls.timeline.max = duration;
+
+ this.setIsLive(duration === Number.POSITIVE_INFINITY);
+
+ var timeControls = [this.controls.currentTime, this.controls.remainingTime, this.currentTimeClone, this.remainingTimeClone];
+
+ function removeTimeClass(className) {
+ for (let element of timeControls)
+ element.classList.remove(className);
+ }
+
+ function addTimeClass(className) {
+ for (let element of timeControls)
+ element.classList.add(className);
+ }
+
+ // Reset existing style.
+ removeTimeClass(this.ClassNames.threeDigitTime);
+ removeTimeClass(this.ClassNames.fourDigitTime);
+ removeTimeClass(this.ClassNames.fiveDigitTime);
+ removeTimeClass(this.ClassNames.sixDigitTime);
+
+ if (duration >= 60*60*10)
+ addTimeClass(this.ClassNames.sixDigitTime);
+ else if (duration >= 60*60)
+ addTimeClass(this.ClassNames.fiveDigitTime);
+ else if (duration >= 60*10)
+ addTimeClass(this.ClassNames.fourDigitTime);
+ else
+ addTimeClass(this.ClassNames.threeDigitTime);
+ },
+
+ progressFillStyle: function(context)
+ {
+ var height = this.timelineHeight;
+ var gradient = context.createLinearGradient(0, 0, 0, height);
+ gradient.addColorStop(0, 'rgb(2, 2, 2)');
+ gradient.addColorStop(1, 'rgb(23, 23, 23)');
+ return gradient;
+ },
+
+ updateProgress: function()
+ {
+ this.updateTimelineMetricsIfNeeded();
+ this.drawTimelineBackground();
+ },
+
+ addRoundedRect: function(ctx, x, y, width, height, radius) {
+ ctx.moveTo(x + radius, y);
+ ctx.arcTo(x + width, y, x + width, y + radius, radius);
+ ctx.lineTo(x + width, y + height - radius);
+ ctx.arcTo(x + width, y + height, x + width - radius, y + height, radius);
+ ctx.lineTo(x + radius, y + height);
+ ctx.arcTo(x, y + height, x, y + height - radius, radius);
+ ctx.lineTo(x, y + radius);
+ ctx.arcTo(x, y, x + radius, y, radius);
+ },
+
+ drawTimelineBackground: function() {
+ var dpr = window.devicePixelRatio;
+ var width = this.timelineWidth * dpr;
+ var height = this.timelineHeight * dpr;
+
+ if (!width || !height)
+ return;
+
+ var played = this.controls.timeline.value / this.controls.timeline.max;
+ var buffered = 0;
+ for (var i = 0, end = this.video.buffered.length; i < end; ++i)
+ buffered = Math.max(this.video.buffered.end(i), buffered);
+
+ buffered /= this.video.duration;
+
+ var ctx = document.getCSSCanvasContext('2d', this.timelineContextName, width, height);
+
+ width /= dpr;
+ height /= dpr;
+
+ ctx.save();
+ ctx.scale(dpr, dpr);
+ ctx.clearRect(0, 0, width, height);
+
+ var timelineHeight = 3;
+ var trackHeight = 1;
+ var scrubberWidth = 3;
+ var scrubberHeight = 15;
+ var borderSize = 2;
+ var scrubberPosition = Math.max(0, Math.min(width - scrubberWidth, Math.round(width * played)));
+
+ // Draw buffered section.
+ ctx.save();
+ if (this.isAudio())
+ ctx.fillStyle = "rgb(71, 71, 71)";
+ else
+ ctx.fillStyle = "rgb(30, 30, 30)";
+ ctx.fillRect(1, 8, Math.round(width * buffered) - borderSize, trackHeight);
+ ctx.restore();
+
+ // Draw timeline border.
+ ctx.save();
+ ctx.beginPath();
+ this.addRoundedRect(ctx, scrubberPosition, 7, width - scrubberPosition, timelineHeight, timelineHeight / 2.0);
+ this.addRoundedRect(ctx, scrubberPosition + 1, 8, width - scrubberPosition - borderSize , trackHeight, trackHeight / 2.0);
+ ctx.closePath();
+ ctx.clip("evenodd");
+ if (this.isAudio())
+ ctx.fillStyle = "rgb(71, 71, 71)";
+ else
+ ctx.fillStyle = "rgb(30, 30, 30)";
+ ctx.fillRect(0, 0, width, height);
+ ctx.restore();
+
+ // Draw played section.
+ ctx.save();
+ ctx.beginPath();
+ this.addRoundedRect(ctx, 0, 7, width, timelineHeight, timelineHeight / 2.0);
+ ctx.closePath();
+ ctx.clip();
+ if (this.isAudio())
+ ctx.fillStyle = "rgb(116, 116, 116)";
+ else
+ ctx.fillStyle = "rgb(75, 75, 75)";
+ ctx.fillRect(0, 0, width * played, height);
+ ctx.restore();
+
+ // Draw the scrubber.
+ ctx.save();
+ ctx.clearRect(scrubberPosition - 1, 0, scrubberWidth + borderSize, height, 0);
+ ctx.beginPath();
+ this.addRoundedRect(ctx, scrubberPosition, 1, scrubberWidth, scrubberHeight, 1);
+ ctx.closePath();
+ ctx.clip();
+ if (this.isAudio())
+ ctx.fillStyle = "rgb(181, 181, 181)";
+ else
+ ctx.fillStyle = "rgb(140, 140, 140)";
+ ctx.fillRect(0, 0, width, height);
+ ctx.restore();
+
+ ctx.restore();
+ },
+
+ drawVolumeBackground: function() {
+ var dpr = window.devicePixelRatio;
+ var width = this.controls.volume.offsetWidth * dpr;
+ var height = this.controls.volume.offsetHeight * dpr;
+
+ if (!width || !height)
+ return;
+
+ var ctx = document.getCSSCanvasContext('2d', this.volumeContextName, width, height);
+
+ width /= dpr;
+ height /= dpr;
+
+ ctx.save();
+ ctx.scale(dpr, dpr);
+ ctx.clearRect(0, 0, width, height);
+
+ var seekerPosition = this.controls.volume.value;
+ var trackHeight = 1;
+ var timelineHeight = 3;
+ var scrubberRadius = 3.5;
+ var scrubberDiameter = 2 * scrubberRadius;
+ var borderSize = 2;
+
+ var scrubberPosition = Math.round(seekerPosition * (width - scrubberDiameter - borderSize));
+
+
+ // Draw portion of volume under slider thumb.
+ ctx.save();
+ ctx.beginPath();
+ this.addRoundedRect(ctx, 0, 3, scrubberPosition + 2, timelineHeight, timelineHeight / 2.0);
+ ctx.closePath();
+ ctx.clip();
+ ctx.fillStyle = "rgb(75, 75, 75)";
+ ctx.fillRect(0, 0, width, height);
+ ctx.restore();
+
+ // Draw portion of volume above slider thumb.
+ ctx.save();
+ ctx.beginPath();
+ this.addRoundedRect(ctx, scrubberPosition, 3, width - scrubberPosition, timelineHeight, timelineHeight / 2.0);
+ ctx.closePath();
+ ctx.clip();
+ ctx.fillStyle = "rgb(30, 30, 30)";
+ ctx.fillRect(0, 0, width, height);
+ ctx.restore();
+
+ // Clear a hole in the slider for the scrubber.
+ ctx.save();
+ ctx.beginPath();
+ this.addRoundedRect(ctx, scrubberPosition, 0, scrubberDiameter + borderSize, height, (scrubberDiameter + borderSize) / 2.0);
+ ctx.closePath();
+ ctx.clip();
+ ctx.clearRect(0, 0, width, height);
+ ctx.restore();
+
+ // Draw scrubber.
+ ctx.save();
+ ctx.beginPath();
+ this.addRoundedRect(ctx, scrubberPosition + 1, 1, scrubberDiameter, scrubberDiameter, scrubberRadius);
+ ctx.closePath();
+ ctx.clip();
+ if (this.isVolumeSliderActive)
+ ctx.fillStyle = "white";
+ else
+ ctx.fillStyle = "rgb(140, 140, 140)";
+ ctx.fillRect(0, 0, width, height);
+ ctx.restore();
+
+ ctx.restore();
+ },
+
+ formatTimeDescription: function(time)
+ {
+ if (isNaN(time))
+ time = 0;
+ var absTime = Math.abs(time);
+ var intSeconds = Math.floor(absTime % 60).toFixed(0);
+ var intMinutes = Math.floor((absTime / 60) % 60).toFixed(0);
+ var intHours = Math.floor(absTime / (60 * 60)).toFixed(0);
+
+ var secondString = intSeconds == 1 ? 'Second' : 'Seconds';
+ var minuteString = intMinutes == 1 ? 'Minute' : 'Minutes';
+ var hourString = intHours == 1 ? 'Hour' : 'Hours';
+ if (intHours > 0)
+ return `${intHours} ${this.UIString(hourString)}, ${intMinutes} ${this.UIString(minuteString)}, ${intSeconds} ${this.UIString(secondString)}`;
+ if (intMinutes > 0)
+ return `${intMinutes} ${this.UIString(minuteString)}, ${intSeconds} ${this.UIString(secondString)}`;
+ return `${intSeconds} ${this.UIString(secondString)}`;
+ },
+
+ formatTime: function(time)
+ {
+ if (isNaN(time))
+ time = 0;
+ var absTime = Math.abs(time);
+ var intSeconds = Math.floor(absTime % 60).toFixed(0);
+ var intMinutes = Math.floor((absTime / 60) % 60).toFixed(0);
+ var intHours = Math.floor(absTime / (60 * 60)).toFixed(0);
+ var sign = time < 0 ? '-' : String();
+
+ if (intHours > 0)
+ return sign + intHours + ':' + String('00' + intMinutes).slice(-2) + ":" + String('00' + intSeconds).slice(-2);
+
+ return sign + String('00' + intMinutes).slice(-2) + ":" + String('00' + intSeconds).slice(-2)
+ },
+
+ updatePlaying: function()
+ {
+ this.setPlaying(!this.canPlay());
+ },
+
+ setPlaying: function(isPlaying)
+ {
+ if (!this.video.controls && !this.isFullScreen())
+ return;
+
+ if (this.isPlaying === isPlaying)
+ return;
+ this.isPlaying = isPlaying;
+
+ if (!isPlaying) {
+ this.controls.panel.classList.add(this.ClassNames.paused);
+ if (this.controls.panelBackground)
+ this.controls.panelBackground.classList.add(this.ClassNames.paused);
+ this.controls.playButton.classList.add(this.ClassNames.paused);
+ this.controls.playButton.setAttribute('aria-label', this.UIString('Play'));
+ this.showControls();
+ } else {
+ this.controls.panel.classList.remove(this.ClassNames.paused);
+ if (this.controls.panelBackground)
+ this.controls.panelBackground.classList.remove(this.ClassNames.paused);
+ this.controls.playButton.classList.remove(this.ClassNames.paused);
+ this.controls.playButton.setAttribute('aria-label', this.UIString('Pause'));
+ this.resetHideControlsTimer();
+ this.canToggleShowControlsButton = true;
+ }
+ },
+
+ updateForShowingControls: function()
+ {
+ this.updateLayoutForDisplayedWidth();
+ this.setNeedsTimelineMetricsUpdate();
+ this.updateTime();
+ this.updateProgress();
+ this.drawVolumeBackground();
+ this.drawTimelineBackground();
+ this.controls.panel.classList.add(this.ClassNames.show);
+ this.controls.panel.classList.remove(this.ClassNames.hidden);
+ if (this.controls.panelBackground) {
+ this.controls.panelBackground.classList.add(this.ClassNames.show);
+ this.controls.panelBackground.classList.remove(this.ClassNames.hidden);
+ }
+ },
+
+ showShowControlsButton: function (shouldShow) {
+ this.showControlsButton.hidden = !shouldShow;
+ if (shouldShow && this.shouldHaveControls())
+ this.showControlsButton.focus();
+ },
+
+ showControls: function(focusControls)
+ {
+ this.updateShouldListenForPlaybackTargetAvailabilityEvent();
+ if (!this.video.controls && !this.isFullScreen())
+ return;
+
+ this.updateForShowingControls();
+ if (this.shouldHaveControls() && !this.controls.panel.parentElement) {
+ this.base.appendChild(this.controls.inlinePlaybackPlaceholder);
+ this.base.appendChild(this.controls.panel);
+ if (focusControls)
+ this.controls.playButton.focus();
+ }
+ this.showShowControlsButton(false);
+ },
+
+ hideControls: function()
+ {
+ if (this.controlsAlwaysVisible())
+ return;
+
+ this.clearHideControlsTimer();
+ this.updateShouldListenForPlaybackTargetAvailabilityEvent();
+ this.controls.panel.classList.remove(this.ClassNames.show);
+ if (this.controls.panelBackground)
+ this.controls.panelBackground.classList.remove(this.ClassNames.show);
+ this.showShowControlsButton(this.isPlayable() && this.isPlaying && this.canToggleShowControlsButton);
+ },
+
+ setNeedsUpdateForDisplayedWidth: function()
+ {
+ this.currentDisplayWidth = 0;
+ },
+
+ scheduleUpdateLayoutForDisplayedWidth: function()
+ {
+ setTimeout(this.updateLayoutForDisplayedWidth.bind(this), 0);
+ },
+
+ isControlVisible: function(control)
+ {
+ if (!control)
+ return false;
+ if (!this.root.contains(control))
+ return false;
+ return !control.classList.contains(this.ClassNames.hidden)
+ },
+
+ updateLayoutForDisplayedWidth: function()
+ {
+ if (!this.controls || !this.controls.panel)
+ return;
+
+ var visibleWidth = this.controls.panel.getBoundingClientRect().width;
+ if (this._pageScaleFactor > 1)
+ visibleWidth *= this._pageScaleFactor;
+
+ if (visibleWidth <= 0 || visibleWidth == this.currentDisplayWidth)
+ return;
+
+ this.currentDisplayWidth = visibleWidth;
+
+ // Filter all the buttons which are not explicitly hidden.
+ var buttons = [this.controls.playButton, this.controls.rewindButton, this.controls.captionButton,
+ this.controls.fullscreenButton, this.controls.pictureInPictureButton,
+ this.controls.wirelessTargetPicker, this.controls.muteBox];
+ var visibleButtons = buttons.filter(this.isControlVisible, this);
+
+ // This tells us how much room we need in order to display every visible button.
+ var visibleButtonWidth = this.ButtonWidth * visibleButtons.length;
+
+ var currentTimeWidth = this.currentTimeClone.getBoundingClientRect().width;
+ var remainingTimeWidth = this.remainingTimeClone.getBoundingClientRect().width;
+
+ // Check if there is enough room for the scrubber.
+ var shouldDropTimeline = (visibleWidth - visibleButtonWidth - currentTimeWidth - remainingTimeWidth) < this.MinimumTimelineWidth;
+ this.controls.timeline.classList.toggle(this.ClassNames.dropped, shouldDropTimeline);
+ this.controls.currentTime.classList.toggle(this.ClassNames.dropped, shouldDropTimeline);
+ this.controls.thumbnailTrack.classList.toggle(this.ClassNames.dropped, shouldDropTimeline);
+ this.controls.remainingTime.classList.toggle(this.ClassNames.dropped, shouldDropTimeline);
+
+ // Then controls in the following order:
+ var removeOrder = [this.controls.wirelessTargetPicker, this.controls.pictureInPictureButton,
+ this.controls.captionButton, this.controls.muteBox, this.controls.rewindButton,
+ this.controls.fullscreenButton];
+ removeOrder.forEach(function(control) {
+ var shouldDropControl = visibleWidth < visibleButtonWidth && this.isControlVisible(control);
+ control.classList.toggle(this.ClassNames.dropped, shouldDropControl);
+ if (shouldDropControl)
+ visibleButtonWidth -= this.ButtonWidth;
+ }, this);
+ },
+
+ controlsAlwaysVisible: function()
+ {
+ if (this.presentationMode() === 'picture-in-picture')
+ return true;
+
+ return this.isAudio() || this.currentPlaybackTargetIsWireless() || this.scrubbing;
+ },
+
+ controlsAreHidden: function()
+ {
+ return !this.controlsAlwaysVisible() && !this.controls.panel.classList.contains(this.ClassNames.show) && !this.controls.panel.parentElement;
+ },
+
+ removeControls: function()
+ {
+ if (this.controls.panel.parentNode)
+ this.controls.panel.parentNode.removeChild(this.controls.panel);
+ this.destroyCaptionMenu();
+ },
+
+ addControls: function()
+ {
+ this.base.appendChild(this.controls.inlinePlaybackPlaceholder);
+ this.base.appendChild(this.controls.panel);
+ this.updateControls();
+ },
+
+ hasControls: function()
+ {
+ return this.controls.panel.parentElement;
+ },
+
+ updateTime: function()
+ {
+ var currentTime = this.video.currentTime;
+ var timeRemaining = currentTime - this.video.duration;
+ this.currentTimeClone.innerText = this.controls.currentTime.innerText = this.formatTime(currentTime);
+ this.controls.currentTime.setAttribute('aria-label', `${this.UIString('Elapsed')} ${this.formatTimeDescription(currentTime)}`);
+ this.controls.timeline.value = this.video.currentTime;
+ this.remainingTimeClone.innerText = this.controls.remainingTime.innerText = this.formatTime(timeRemaining);
+ this.controls.remainingTime.setAttribute('aria-label', `${this.UIString('Remaining')} ${this.formatTimeDescription(timeRemaining)}`);
+
+ // Mark the timeline value in percentage format in accessibility.
+ var timeElapsedPercent = currentTime / this.video.duration;
+ timeElapsedPercent = Math.max(Math.min(1, timeElapsedPercent), 0);
+ this.controls.timeline.setAttribute('aria-valuetext', `${parseInt(timeElapsedPercent * 100)}%`);
+ },
+
+ updateControlsWhileScrubbing: function()
+ {
+ if (!this.scrubbing)
+ return;
+
+ var currentTime = (this.controls.timeline.value / this.controls.timeline.max) * this.video.duration;
+ var timeRemaining = currentTime - this.video.duration;
+ this.currentTimeClone.innerText = this.controls.currentTime.innerText = this.formatTime(currentTime);
+ this.remainingTimeClone.innerText = this.controls.remainingTime.innerText = this.formatTime(timeRemaining);
+ this.drawTimelineBackground();
+ },
+
+ updateReadyState: function()
+ {
+ this.updateStatusDisplay();
+ },
+
+ setStatusHidden: function(hidden)
+ {
+ if (this.statusHidden === hidden)
+ return;
+
+ this.statusHidden = hidden;
+
+ if (hidden) {
+ this.controls.statusDisplay.classList.add(this.ClassNames.hidden);
+ this.controls.currentTime.classList.remove(this.ClassNames.hidden);
+ this.controls.timeline.classList.remove(this.ClassNames.hidden);
+ this.controls.remainingTime.classList.remove(this.ClassNames.hidden);
+ this.setNeedsTimelineMetricsUpdate();
+ this.showControls();
+ } else {
+ this.controls.statusDisplay.classList.remove(this.ClassNames.hidden);
+ this.controls.currentTime.classList.add(this.ClassNames.hidden);
+ this.controls.timeline.classList.add(this.ClassNames.hidden);
+ this.controls.remainingTime.classList.add(this.ClassNames.hidden);
+ this.hideControls();
+ }
+ this.updateWirelessTargetAvailable();
+ },
+
+ trackHasThumbnails: function(track)
+ {
+ return track.kind === 'thumbnails' || (track.kind === 'metadata' && track.label === 'thumbnails');
+ },
+
+ updateThumbnail: function()
+ {
+ for (var i = 0; i < this.video.textTracks.length; ++i) {
+ var track = this.video.textTracks[i];
+ if (this.trackHasThumbnails(track)) {
+ this.controls.thumbnail.classList.remove(this.ClassNames.hidden);
+ return;
+ }
+ }
+
+ this.controls.thumbnail.classList.add(this.ClassNames.hidden);
+ },
+
+ updateCaptionButton: function()
+ {
+ var audioTracks = this.host.sortedTrackListForMenu(this.video.audioTracks);
+ var textTracks = this.host.sortedTrackListForMenu(this.video.textTracks);
+
+ if ((textTracks && textTracks.length) || (audioTracks && audioTracks.length > 1))
+ this.controls.captionButton.classList.remove(this.ClassNames.hidden);
+ else
+ this.controls.captionButton.classList.add(this.ClassNames.hidden);
+ this.setNeedsUpdateForDisplayedWidth();
+ this.updateLayoutForDisplayedWidth();
+ },
+
+ updateCaptionContainer: function()
+ {
+ if (!this.host.textTrackContainer)
+ return;
+
+ var hasClosedCaptions = this.video.webkitHasClosedCaptions;
+ var hasHiddenClass = this.host.textTrackContainer.classList.contains(this.ClassNames.hidden);
+
+ if (hasClosedCaptions && hasHiddenClass)
+ this.host.textTrackContainer.classList.remove(this.ClassNames.hidden);
+ else if (!hasClosedCaptions && !hasHiddenClass)
+ this.host.textTrackContainer.classList.add(this.ClassNames.hidden);
+
+ this.updateBase();
+ this.host.updateTextTrackContainer();
+ },
+
+ buildCaptionMenu: function()
+ {
+ var audioTracks = this.host.sortedTrackListForMenu(this.video.audioTracks);
+ var textTracks = this.host.sortedTrackListForMenu(this.video.textTracks);
+
+ if ((!textTracks || !textTracks.length) && (!audioTracks || !audioTracks.length))
+ return;
+
+ this.captionMenu = document.createElement('div');
+ this.captionMenu.setAttribute('pseudo', '-webkit-media-controls-closed-captions-container');
+ this.captionMenu.setAttribute('id', 'audioAndTextTrackMenu');
+ this.base.appendChild(this.captionMenu);
+ this.captionMenuItems = [];
+
+ var offItem = this.host.captionMenuOffItem;
+ var automaticItem = this.host.captionMenuAutomaticItem;
+ var displayMode = this.host.captionDisplayMode;
+
+ var list = document.createElement('div');
+ this.captionMenu.appendChild(list);
+ list.classList.add(this.ClassNames.list);
+
+ if (audioTracks && audioTracks.length > 1) {
+ var heading = document.createElement('h3');
+ heading.id = 'webkitMediaControlsAudioTrackHeading'; // for AX menu label
+ list.appendChild(heading);
+ heading.innerText = this.UIString('Audio');
+
+ var ul = document.createElement('ul');
+ ul.setAttribute('role', 'menu');
+ ul.setAttribute('aria-labelledby', 'webkitMediaControlsAudioTrackHeading');
+ list.appendChild(ul);
+
+ for (var i = 0; i < audioTracks.length; ++i) {
+ var menuItem = document.createElement('li');
+ menuItem.setAttribute('role', 'menuitemradio');
+ menuItem.setAttribute('tabindex', '-1');
+ this.captionMenuItems.push(menuItem);
+ this.listenFor(menuItem, 'click', this.audioTrackItemSelected);
+ this.listenFor(menuItem, 'keyup', this.handleAudioTrackItemKeyUp);
+ ul.appendChild(menuItem);
+
+ var track = audioTracks[i];
+ menuItem.innerText = this.host.displayNameForTrack(track);
+ menuItem.track = track;
+
+ var itemCheckmark = document.createElement("img");
+ itemCheckmark.classList.add("checkmark-container");
+ menuItem.insertBefore(itemCheckmark, menuItem.firstChild);
+
+ if (track.enabled) {
+ menuItem.classList.add(this.ClassNames.selected);
+ menuItem.setAttribute('tabindex', '0');
+ menuItem.setAttribute('aria-checked', 'true');
+ }
+ }
+ }
+
+ if (textTracks && textTracks.length > 2) {
+ var heading = document.createElement('h3');
+ heading.id = 'webkitMediaControlsClosedCaptionsHeading'; // for AX menu label
+ list.appendChild(heading);
+ heading.innerText = this.UIString('Subtitles');
+
+ var ul = document.createElement('ul');
+ ul.setAttribute('role', 'menu');
+ ul.setAttribute('aria-labelledby', 'webkitMediaControlsClosedCaptionsHeading');
+ list.appendChild(ul);
+
+ for (var i = 0; i < textTracks.length; ++i) {
+ var menuItem = document.createElement('li');
+ menuItem.setAttribute('role', 'menuitemradio');
+ menuItem.setAttribute('tabindex', '-1');
+ this.captionMenuItems.push(menuItem);
+ this.listenFor(menuItem, 'click', this.captionItemSelected);
+ this.listenFor(menuItem, 'keyup', this.handleCaptionItemKeyUp);
+ ul.appendChild(menuItem);
+
+ var track = textTracks[i];
+ menuItem.innerText = this.host.displayNameForTrack(track);
+ menuItem.track = track;
+
+ var itemCheckmark = document.createElement("img");
+ itemCheckmark.classList.add("checkmark-container");
+ menuItem.insertBefore(itemCheckmark, menuItem.firstChild);
+
+ if (track === offItem) {
+ var offMenu = menuItem;
+ continue;
+ }
+
+ if (track === automaticItem) {
+ if (displayMode === 'automatic') {
+ menuItem.classList.add(this.ClassNames.selected);
+ menuItem.setAttribute('tabindex', '0');
+ menuItem.setAttribute('aria-checked', 'true');
+ }
+ continue;
+ }
+
+ if (displayMode != 'automatic' && track.mode === 'showing') {
+ var trackMenuItemSelected = true;
+ menuItem.classList.add(this.ClassNames.selected);
+ menuItem.setAttribute('tabindex', '0');
+ menuItem.setAttribute('aria-checked', 'true');
+ }
+
+ }
+
+ if (offMenu && (displayMode === 'forced-only' || displayMode === 'manual') && !trackMenuItemSelected) {
+ offMenu.classList.add(this.ClassNames.selected);
+ offMenu.setAttribute('tabindex', '0');
+ offMenu.setAttribute('aria-checked', 'true');
+ }
+ }
+
+ // focus first selected menuitem
+ for (var i = 0, c = this.captionMenuItems.length; i < c; i++) {
+ var item = this.captionMenuItems[i];
+ if (item.classList.contains(this.ClassNames.selected)) {
+ item.focus();
+ break;
+ }
+ }
+
+ },
+
+ captionItemSelected: function(event)
+ {
+ this.host.setSelectedTextTrack(event.target.track);
+ this.destroyCaptionMenu();
+ },
+
+ focusSiblingCaptionItem: function(event)
+ {
+ var currentItem = event.target;
+ var pendingItem = false;
+ switch(event.keyCode) {
+ case this.KeyCodes.left:
+ case this.KeyCodes.up:
+ pendingItem = currentItem.previousSibling;
+ break;
+ case this.KeyCodes.right:
+ case this.KeyCodes.down:
+ pendingItem = currentItem.nextSibling;
+ break;
+ }
+ if (pendingItem) {
+ currentItem.setAttribute('tabindex', '-1');
+ pendingItem.setAttribute('tabindex', '0');
+ pendingItem.focus();
+ }
+ },
+
+ handleCaptionItemKeyUp: function(event)
+ {
+ switch (event.keyCode) {
+ case this.KeyCodes.enter:
+ case this.KeyCodes.space:
+ this.captionItemSelected(event);
+ break;
+ case this.KeyCodes.escape:
+ this.destroyCaptionMenu();
+ break;
+ case this.KeyCodes.left:
+ case this.KeyCodes.up:
+ case this.KeyCodes.right:
+ case this.KeyCodes.down:
+ this.focusSiblingCaptionItem(event);
+ break;
+ default:
+ return;
+ }
+ // handled
+ event.stopPropagation();
+ event.preventDefault();
+ },
+
+ audioTrackItemSelected: function(event)
+ {
+ for (var i = 0; i < this.video.audioTracks.length; ++i) {
+ var track = this.video.audioTracks[i];
+ track.enabled = (track == event.target.track);
+ }
+
+ this.destroyCaptionMenu();
+ },
+
+ focusSiblingAudioTrackItem: function(event)
+ {
+ var currentItem = event.target;
+ var pendingItem = false;
+ switch(event.keyCode) {
+ case this.KeyCodes.left:
+ case this.KeyCodes.up:
+ pendingItem = currentItem.previousSibling;
+ break;
+ case this.KeyCodes.right:
+ case this.KeyCodes.down:
+ pendingItem = currentItem.nextSibling;
+ break;
+ }
+ if (pendingItem) {
+ currentItem.setAttribute('tabindex', '-1');
+ pendingItem.setAttribute('tabindex', '0');
+ pendingItem.focus();
+ }
+ },
+
+ handleAudioTrackItemKeyUp: function(event)
+ {
+ switch (event.keyCode) {
+ case this.KeyCodes.enter:
+ case this.KeyCodes.space:
+ this.audioTrackItemSelected(event);
+ break;
+ case this.KeyCodes.escape:
+ this.destroyCaptionMenu();
+ break;
+ case this.KeyCodes.left:
+ case this.KeyCodes.up:
+ case this.KeyCodes.right:
+ case this.KeyCodes.down:
+ this.focusSiblingAudioTrackItem(event);
+ break;
+ default:
+ return;
+ }
+ // handled
+ event.stopPropagation();
+ event.preventDefault();
+ },
+
+ destroyCaptionMenu: function()
+ {
+ if (!this.captionMenu)
+ return;
+
+ this.captionMenuItems.forEach(function(item){
+ this.stopListeningFor(item, 'click', this.captionItemSelected);
+ this.stopListeningFor(item, 'keyup', this.handleCaptionItemKeyUp);
+ }, this);
+
+ // FKA and AX: focus the trigger before destroying the element with focus
+ if (this.controls.captionButton)
+ this.controls.captionButton.focus();
+
+ if (this.captionMenu.parentNode)
+ this.captionMenu.parentNode.removeChild(this.captionMenu);
+ delete this.captionMenu;
+ delete this.captionMenuItems;
+ },
+
+ updateHasAudio: function()
+ {
+ if (this.video.audioTracks.length && !this.currentPlaybackTargetIsWireless())
+ this.controls.muteBox.classList.remove(this.ClassNames.hidden);
+ else
+ this.controls.muteBox.classList.add(this.ClassNames.hidden);
+
+ this.setNeedsUpdateForDisplayedWidth();
+ this.updateLayoutForDisplayedWidth();
+ },
+
+ updateHasVideo: function()
+ {
+ this.controls.panel.classList.toggle(this.ClassNames.noVideo, !this.hasVideo());
+ // The availability of the picture-in-picture button as well as the full-screen
+ // button depends no the value returned by hasVideo(), so make sure we invalidate
+ // the availability of both controls.
+ this.updateFullscreenButtons();
+ },
+
+ updateVolume: function()
+ {
+ if (this.video.muted || !this.video.volume) {
+ this.controls.muteButton.classList.add(this.ClassNames.muted);
+ this.controls.volume.value = 0;
+ } else {
+ this.controls.muteButton.classList.remove(this.ClassNames.muted);
+ this.controls.volume.value = this.video.volume;
+ }
+ this.controls.volume.setAttribute('aria-valuetext', `${parseInt(this.controls.volume.value * 100)}%`);
+ this.drawVolumeBackground();
+ },
+
+ isAudio: function()
+ {
+ return this.video instanceof HTMLAudioElement;
+ },
+
+ clearHideControlsTimer: function()
+ {
+ if (this.hideTimer)
+ clearTimeout(this.hideTimer);
+ this.hideTimer = null;
+ },
+
+ resetHideControlsTimer: function()
+ {
+ if (this.hideTimer) {
+ clearTimeout(this.hideTimer);
+ this.hideTimer = null;
+ }
+
+ if (this.isPlaying)
+ this.hideTimer = setTimeout(this.hideControls.bind(this), this.HideControlsDelay);
+ },
+
+ handlePictureInPictureButtonClicked: function(event) {
+ if (!('webkitSetPresentationMode' in this.video))
+ return;
+
+ if (this.presentationMode() === 'picture-in-picture')
+ this.video.webkitSetPresentationMode('inline');
+ else
+ this.video.webkitSetPresentationMode('picture-in-picture');
+ },
+
+ currentPlaybackTargetIsWireless: function() {
+ if (Controller.gSimulateWirelessPlaybackTarget)
+ return true;
+
+ if (!this.currentTargetIsWireless || this.wirelessPlaybackDisabled)
+ return false;
+
+ return true;
+ },
+
+ updateShouldListenForPlaybackTargetAvailabilityEvent: function() {
+ var shouldListen = true;
+ if (this.video.error)
+ shouldListen = false;
+ if (!this.isAudio() && !this.video.paused && this.controlsAreHidden())
+ shouldListen = false;
+ if (document.hidden)
+ shouldListen = false;
+
+ this.setShouldListenForPlaybackTargetAvailabilityEvent(shouldListen);
+ },
+
+ updateWirelessPlaybackStatus: function() {
+ if (this.currentPlaybackTargetIsWireless()) {
+ var deviceName = "";
+ var deviceType = "";
+ var type = this.host.externalDeviceType;
+ if (type == "airplay") {
+ deviceType = this.UIString('##WIRELESS_PLAYBACK_DEVICE_TYPE##');
+ deviceName = this.UIString('##WIRELESS_PLAYBACK_DEVICE_NAME##', '##DEVICE_NAME##', this.host.externalDeviceDisplayName || "Apple TV");
+ } else if (type == "tvout") {
+ deviceType = this.UIString('##TVOUT_DEVICE_TYPE##');
+ deviceName = this.UIString('##TVOUT_DEVICE_NAME##');
+ }
+
+ this.controls.inlinePlaybackPlaceholderTextTop.innerText = deviceType;
+ this.controls.inlinePlaybackPlaceholderTextBottom.innerText = deviceName;
+ this.controls.inlinePlaybackPlaceholder.setAttribute('aria-label', deviceType + ", " + deviceName);
+ this.controls.inlinePlaybackPlaceholder.classList.add(this.ClassNames.appleTV);
+ this.controls.inlinePlaybackPlaceholder.classList.remove(this.ClassNames.hidden);
+ this.controls.wirelessTargetPicker.classList.add(this.ClassNames.playing);
+ if (!this.isFullScreen() && (this.video.offsetWidth <= 250 || this.video.offsetHeight <= 200)) {
+ this.controls.inlinePlaybackPlaceholder.classList.add(this.ClassNames.small);
+ this.controls.inlinePlaybackPlaceholderTextTop.classList.add(this.ClassNames.small);
+ this.controls.inlinePlaybackPlaceholderTextBottom.classList.add(this.ClassNames.small);
+ } else {
+ this.controls.inlinePlaybackPlaceholder.classList.remove(this.ClassNames.small);
+ this.controls.inlinePlaybackPlaceholderTextTop.classList.remove(this.ClassNames.small);
+ this.controls.inlinePlaybackPlaceholderTextBottom.classList.remove(this.ClassNames.small);
+ }
+ this.controls.volumeBox.classList.add(this.ClassNames.hidden);
+ this.controls.muteBox.classList.add(this.ClassNames.hidden);
+ this.updateBase();
+ this.showControls();
+ } else {
+ this.controls.inlinePlaybackPlaceholder.classList.add(this.ClassNames.hidden);
+ this.controls.inlinePlaybackPlaceholder.classList.remove(this.ClassNames.appleTV);
+ this.controls.wirelessTargetPicker.classList.remove(this.ClassNames.playing);
+ this.controls.volumeBox.classList.remove(this.ClassNames.hidden);
+ this.controls.muteBox.classList.remove(this.ClassNames.hidden);
+ }
+ this.setNeedsUpdateForDisplayedWidth();
+ this.updateLayoutForDisplayedWidth();
+ this.reconnectControls();
+ this.updateWirelessTargetPickerButton();
+ },
+
+ updateWirelessTargetAvailable: function() {
+ this.currentTargetIsWireless = this.video.webkitCurrentPlaybackTargetIsWireless;
+ this.wirelessPlaybackDisabled = this.video.webkitWirelessVideoPlaybackDisabled;
+
+ var wirelessPlaybackTargetsAvailable = Controller.gSimulateWirelessPlaybackTarget || this.hasWirelessPlaybackTargets;
+ if (this.wirelessPlaybackDisabled)
+ wirelessPlaybackTargetsAvailable = false;
+
+ if (wirelessPlaybackTargetsAvailable && this.isPlayable())
+ this.controls.wirelessTargetPicker.classList.remove(this.ClassNames.hidden);
+ else
+ this.controls.wirelessTargetPicker.classList.add(this.ClassNames.hidden);
+ this.setNeedsUpdateForDisplayedWidth();
+ this.updateLayoutForDisplayedWidth();
+ },
+
+ handleWirelessPickerButtonClicked: function(event)
+ {
+ this.video.webkitShowPlaybackTargetPicker();
+ return true;
+ },
+
+ handleWirelessPlaybackChange: function(event) {
+ this.updateWirelessTargetAvailable();
+ this.updateWirelessPlaybackStatus();
+ this.setNeedsTimelineMetricsUpdate();
+ },
+
+ handleWirelessTargetAvailableChange: function(event) {
+ var wirelessPlaybackTargetsAvailable = event.availability == "available";
+ if (this.hasWirelessPlaybackTargets === wirelessPlaybackTargetsAvailable)
+ return;
+
+ this.hasWirelessPlaybackTargets = wirelessPlaybackTargetsAvailable;
+ this.updateWirelessTargetAvailable();
+ this.setNeedsTimelineMetricsUpdate();
+ },
+
+ setShouldListenForPlaybackTargetAvailabilityEvent: function(shouldListen) {
+ if (!window.WebKitPlaybackTargetAvailabilityEvent || this.isListeningForPlaybackTargetAvailabilityEvent == shouldListen)
+ return;
+
+ if (shouldListen && this.video.error)
+ return;
+
+ this.isListeningForPlaybackTargetAvailabilityEvent = shouldListen;
+ if (shouldListen)
+ this.listenFor(this.video, 'webkitplaybacktargetavailabilitychanged', this.handleWirelessTargetAvailableChange);
+ else
+ this.stopListeningFor(this.video, 'webkitplaybacktargetavailabilitychanged', this.handleWirelessTargetAvailableChange);
+ },
+
+ get scrubbing()
+ {
+ return this._scrubbing;
+ },
+
+ set scrubbing(flag)
+ {
+ if (this._scrubbing == flag)
+ return;
+ this._scrubbing = flag;
+
+ if (this._scrubbing)
+ this.wasPlayingWhenScrubbingStarted = !this.video.paused;
+ else if (this.wasPlayingWhenScrubbingStarted && this.video.paused) {
+ this.video.play();
+ this.resetHideControlsTimer();
+ }
+ },
+
+ get pageScaleFactor()
+ {
+ return this._pageScaleFactor;
+ },
+
+ set pageScaleFactor(newScaleFactor)
+ {
+ if (this._pageScaleFactor === newScaleFactor)
+ return;
+
+ this._pageScaleFactor = newScaleFactor;
+ },
+
+ set usesLTRUserInterfaceLayoutDirection(usesLTRUserInterfaceLayoutDirection)
+ {
+ this.controls.volumeBox.classList.toggle(this.ClassNames.usesLTRUserInterfaceLayoutDirection, usesLTRUserInterfaceLayoutDirection);
+ },
+
+ handleRootResize: function(event)
+ {
+ this.updateLayoutForDisplayedWidth();
+ this.setNeedsTimelineMetricsUpdate();
+ this.updateTimelineMetricsIfNeeded();
+ this.drawTimelineBackground();
+ },
+
+ getCurrentControlsStatus: function ()
+ {
+ var result = {
+ idiom: this.idiom,
+ status: "ok"
+ };
+
+ var elements = [
+ {
+ name: "Show Controls",
+ object: this.showControlsButton,
+ extraProperties: ["hidden"],
+ },
+ {
+ name: "Status Display",
+ object: this.controls.statusDisplay,
+ styleValues: ["display"],
+ extraProperties: ["textContent"],
+ },
+ {
+ name: "Play Button",
+ object: this.controls.playButton,
+ extraProperties: ["hidden"],
+ },
+ {
+ name: "Rewind Button",
+ object: this.controls.rewindButton,
+ extraProperties: ["hidden"],
+ },
+ {
+ name: "Timeline Box",
+ object: this.controls.timelineBox,
+ },
+ {
+ name: "Mute Box",
+ object: this.controls.muteBox,
+ extraProperties: ["hidden"],
+ },
+ {
+ name: "Fullscreen Button",
+ object: this.controls.fullscreenButton,
+ extraProperties: ["hidden"],
+ },
+ {
+ name: "AppleTV Device Picker",
+ object: this.controls.wirelessTargetPicker,
+ styleValues: ["display"],
+ extraProperties: ["hidden"],
+ },
+ {
+ name: "Picture-in-picture Button",
+ object: this.controls.pictureInPictureButton,
+ extraProperties: ["parentElement", "hidden"],
+ },
+ {
+ name: "Caption Button",
+ object: this.controls.captionButton,
+ extraProperties: ["hidden"],
+ },
+ {
+ name: "Timeline",
+ object: this.controls.timeline,
+ extraProperties: ["hidden"],
+ },
+ {
+ name: "Current Time",
+ object: this.controls.currentTime,
+ extraProperties: ["hidden"],
+ },
+ {
+ name: "Thumbnail Track",
+ object: this.controls.thumbnailTrack,
+ extraProperties: ["hidden"],
+ },
+ {
+ name: "Time Remaining",
+ object: this.controls.remainingTime,
+ extraProperties: ["hidden"],
+ },
+ {
+ name: "Track Menu",
+ object: this.captionMenu,
+ },
+ {
+ name: "Inline playback placeholder",
+ object: this.controls.inlinePlaybackPlaceholder,
+ },
+ {
+ name: "Media Controls Panel",
+ object: this.controls.panel,
+ extraProperties: ["hidden"],
+ },
+ {
+ name: "Control Base Element",
+ object: this.base || null,
+ },
+ ];
+
+ elements.forEach(function (element) {
+ var obj = element.object;
+ delete element.object;
+
+ element.computedStyle = {};
+ if (obj && element.styleValues) {
+ var computedStyle = window.getComputedStyle(obj);
+ element.styleValues.forEach(function (propertyName) {
+ element.computedStyle[propertyName] = computedStyle[propertyName];
+ });
+ delete element.styleValues;
+ }
+
+ element.bounds = obj ? obj.getBoundingClientRect() : null;
+ element.className = obj ? obj.className : null;
+ element.ariaLabel = obj ? obj.getAttribute('aria-label') : null;
+
+ if (element.extraProperties) {
+ element.extraProperties.forEach(function (property) {
+ element[property] = obj ? obj[property] : null;
+ });
+ delete element.extraProperties;
+ }
+
+ element.element = obj;
+ });
+
+ result.elements = elements;
+
+ return JSON.stringify(result);
+ }
+
+};