diff options
Diffstat (limited to 'Source/WebCore/Modules/mediacontrols/mediaControlsApple.js')
-rw-r--r-- | Source/WebCore/Modules/mediacontrols/mediaControlsApple.js | 2509 |
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); + } + +}; |