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