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.hasVisualMedia = false; this.addVideoListeners(); this.createBase(); this.createControls(); this.updateBase(); this.updateControls(); this.updateDuration(); this.updateProgress(); this.updateTime(); this.updateReadyState(); this.updatePlaying(); this.updateThumbnail(); this.updateCaptionButton(); this.updateCaptionContainer(); this.updateFullscreenButton(); this.updateVolume(); this.updateHasAudio(); this.updateHasVideo(); }; /* Enums */ Controller.InlineControls = 0; Controller.FullScreenControls = 1; Controller.PlayAfterSeeking = 0; Controller.PauseAfterSeeking = 1; /* Globals */ Controller.gLastTimelineId = 0; 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', }, HideControlsDelay: 4 * 1000, RewindAmount: 30, MaximumSeekRate: 8, SeekDelay: 1500, ClassNames: { active: 'active', exit: 'exit', failed: 'failed', hidden: 'hidden', hiding: 'hiding', hourLongTime: 'hour-long-time', list: 'list', muteBox: 'mute-box', muted: 'muted', paused: 'paused', playing: 'playing', selected: 'selected', show: 'show', thumbnail: 'thumbnail', thumbnailImage: 'thumbnail-image', thumbnailTrack: 'thumbnail-track', volumeBox: 'volume-box', noVideo: 'no-video', down: 'down', out: 'out', }, KeyCodes: { enter: 13, escape: 27, space: 32, pageUp: 33, pageDown: 34, end: 35, home: 36, left: 37, up: 38, right: 39, down: 40 }, extend: function(child) { for (var property in this) { if (!child.hasOwnProperty(property)) child[property] = this[property]; } }, 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.updateHasAudio); this.listenFor(this.video.audioTracks, 'addtrack', this.updateHasAudio); this.listenFor(this.video.audioTracks, 'removetrack', this.updateHasAudio); /* 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'] }); }, 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.updateHasAudio); this.stopListeningFor(this.video.audioTracks, 'addtrack', this.updateHasAudio); this.stopListeningFor(this.video.audioTracks, 'removetrack', this.updateHasAudio); /* 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); }, 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(base, 'mouseout', this.handleWrapperMouseOut); if (this.host.textTrackContainer) base.appendChild(this.host.textTrackContainer); }, shouldHaveAnyUI: function() { return this.shouldHaveControls() || (this.video.textTracks && this.video.textTracks.length); }, shouldHaveControls: function() { return this.video.controls || this.isFullScreen(); }, setNeedsTimelineMetricsUpdate: function() { this.timelineMetricsNeedsUpdate = true; }, updateTimelineMetricsIfNeeded: function() { if (this.timelineMetricsNeedsUpdate) { 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 panelCompositedParent = this.controls.panelCompositedParent = document.createElement('div'); panelCompositedParent.setAttribute('pseudo', '-webkit-media-controls-panel-composited-parent'); 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); 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'); this.timelineID = ++Controller.gLastTimelineId; timeline.setAttribute('pseudo', '-webkit-media-controls-timeline'); timeline.setAttribute('aria-label', this.UIString('Duration')); timeline.style.backgroundImage = '-webkit-canvas(timeline-' + this.timelineID + ')'; timeline.type = 'range'; timeline.value = 0; this.listenFor(timeline, 'input', 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); timeline.step = .01; 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); var muteButton = this.controls.muteButton = document.createElement('button'); muteButton.setAttribute('pseudo', '-webkit-media-controls-mute-button'); muteButton.setAttribute('aria-label', this.UIString('Mute')); 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 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 = .01; this.listenFor(volume, 'input', this.handleVolumeSliderInput); 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', 'audioTrackMenu'); 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); }, setControlsType: function(type) { if (type === this.controlsType) return; this.controlsType = type; this.reconnectControls(); }, 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.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() { if (!this.isLive) this.controls.panel.appendChild(this.controls.rewindButton); this.controls.panel.appendChild(this.controls.playButton); 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.volume); this.controls.muteBox.appendChild(this.controls.muteButton); this.controls.panel.appendChild(this.controls.captionButton); if (!this.isAudio()) 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.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.captionButton); if (!this.isAudio()) 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.setNeedsTimelineMetricsUpdate(); }, updateStatusDisplay: function(event) { 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.video.networkState === HTMLMediaElement.NETWORK_LOADING) this.controls.statusDisplay.innerText = this.UIString('Loading'); else this.controls.statusDisplay.innerText = ''; this.setStatusHidden(!this.isLive && this.video.readyState > HTMLMediaElement.HAVE_NOTHING && !this.video.error); }, 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.hasVisualMedia = this.video.videoTracks && this.video.videoTracks.length > 0; this.updateReadyState(); this.updateDuration(); this.updateCaptionButton(); this.updateCaptionContainer(); this.updateFullscreenButton(); this.updateProgress(); }, handleTimeUpdate: function(event) { if (!this.scrubbing) this.updateTime(); }, handleDurationChange: function(event) { this.updateDuration(); this.updateTime(true); this.updateProgress(true); }, 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(); }, isFullScreen: function() { return this.video.webkitDisplayingFullscreen; }, handleFullscreenChange: function(event) { this.updateBase(); this.updateControls(); 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(); } }, handleWrapperMouseMove: function(event) { 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.panel) 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) > 0) this.controls.panel.classList.remove(this.ClassNames.hidden); else this.controls.panel.classList.add(this.ClassNames.hidden); }, handlePanelClick: function(event) { // Prevent clicks in the panel 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.video.play(); else this.video.pause(); return true; }, handleTimelineChange: function(event) { this.video.fastSeek(this.controls.timeline.value); }, 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; // Do a precise seek when we lift the mouse: this.video.currentTime = this.controls.timeline.value; }, handleMuteButtonClicked: function(event) { this.video.muted = !this.video.muted; if (this.video.muted) this.controls.muteButton.setAttribute('aria-label', this.UIString('Unmute')); return true; }, handleMinButtonClicked: function(event) { if (this.video.muted) { this.video.muted = false; this.controls.muteButton.setAttribute('aria-label', this.UIString('Mute')); } this.video.volume = 0; return true; }, handleMaxButtonClicked: function(event) { if (this.video.muted) { this.video.muted = false; this.controls.muteButton.setAttribute('aria-label', this.UIString('Mute')); } this.video.volume = 1; }, handleVolumeSliderInput: function(event) { if (this.video.muted) { this.video.muted = false; this.controls.muteButton.setAttribute('aria-label', this.UIString('Mute')); } this.video.volume = this.controls.volume.value; }, handleCaptionButtonClicked: function(event) { if (this.captionMenu) this.destroyCaptionMenu(); else this.buildCaptionMenu(); return true; }, updateFullscreenButton: function() { this.controls.fullscreenButton.classList.toggle(this.ClassNames.hidden, (!this.video.webkitSupportsFullscreen || !this.hasVisualMedia)); }, handleFullscreenButtonClicked: function(event) { if (this.isFullScreen()) this.video.webkitExitFullscreen(); else this.video.webkitEnterFullscreen(); return true; }, handleControlsChange: function() { try { this.updateBase(); if (this.shouldHaveControls()) this.addControls(); else 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); this.controls.currentTime.classList.toggle(this.ClassNames.hourLongTime, duration >= 60*60); this.controls.remainingTime.classList.toggle(this.ClassNames.hourLongTime, duration >= 60*60); }, 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(forceUpdate) { if (!forceUpdate && this.controlsAreHidden()) return; this.updateTimelineMetricsIfNeeded(); var width = this.timelineWidth; var height = this.timelineHeight; var context = document.getCSSCanvasContext('2d', 'timeline-' + this.timelineID, width, height); context.clearRect(0, 0, width, height); context.fillStyle = this.progressFillStyle(context); var duration = this.video.duration; var buffered = this.video.buffered; for (var i = 0, end = buffered.length; i < end; ++i) { var startTime = buffered.start(i); var endTime = buffered.end(i); var startX = width * startTime / duration; var endX = width * endTime / duration; context.fillRect(startX, 0, endX - startX, height); } }, 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.isPlaying === isPlaying) return; this.isPlaying = isPlaying; if (!isPlaying) { this.controls.panel.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); this.controls.playButton.classList.remove(this.ClassNames.paused); this.controls.playButton.setAttribute('aria-label', this.UIString('Pause')); this.hideControls(); this.resetHideControlsTimer(); } }, showControls: function() { this.controls.panel.classList.add(this.ClassNames.show); this.controls.panel.classList.remove(this.ClassNames.hidden); this.updateTime(); this.setNeedsTimelineMetricsUpdate(); }, hideControls: function() { this.controls.panel.classList.remove(this.ClassNames.show); }, controlsAreAlwaysVisible: function() { return this.controls.panel.classList.contains(this.ClassNames.noVideo); }, controlsAreHidden: function() { if (this.controlsAreAlwaysVisible()) return false; var panel = this.controls.panel; return (!panel.classList.contains(this.ClassNames.show) || panel.classList.contains(this.ClassNames.hidden)) && (panel.parentElement.querySelector(':hover') !== panel); }, removeControls: function() { if (this.controls.panel.parentNode) this.controls.panel.parentNode.removeChild(this.controls.panel); this.destroyCaptionMenu(); }, addControls: function() { this.base.appendChild(this.controls.panelCompositedParent); this.controls.panelCompositedParent.appendChild(this.controls.panel); this.setNeedsTimelineMetricsUpdate(); }, updateTime: function(forceUpdate) { if (!forceUpdate && this.controlsAreHidden()) return; var currentTime = this.video.currentTime; var timeRemaining = currentTime - this.video.duration; this.controls.currentTime.innerText = this.formatTime(currentTime); this.controls.timeline.value = this.video.currentTime; this.controls.remainingTime.innerText = this.formatTime(timeRemaining); }, 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(); } 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); } }, 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() { if (this.video.webkitHasClosedCaptions) this.controls.captionButton.classList.remove(this.ClassNames.hidden); else this.controls.captionButton.classList.add(this.ClassNames.hidden); }, 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 tracks = this.host.sortedTrackListForMenu(this.video.textTracks); if (!tracks || !tracks.length) return; this.captionMenu = document.createElement('div'); this.captionMenu.setAttribute('pseudo', '-webkit-media-controls-closed-captions-container'); this.captionMenu.setAttribute('id', 'audioTrackMenu'); 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); 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 < tracks.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 = tracks[i]; menuItem.innerText = this.host.displayNameForTrack(track); menuItem.track = track; 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' && !trackMenuItemSelected) { offMenu.classList.add(this.ClassNames.selected); menuItem.setAttribute('tabindex', '0'); menuItem.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(); }, 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.controls.muteBox.classList.remove(this.ClassNames.hidden); else this.controls.muteBox.classList.add(this.ClassNames.hidden); }, updateHasVideo: function() { if (this.video.videoTracks.length) this.controls.panel.classList.remove(this.ClassNames.noVideo); else this.controls.panel.classList.add(this.ClassNames.noVideo); }, 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; } }, 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 = setTimeout(this.hideControls.bind(this), this.HideControlsDelay); }, };