| 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.hasVisualMedia = false; |
| |
| this.addVideoListeners(); |
| this.createBase(); |
| this.createControls(); |
| this.updateBase(); |
| this.updateControls(); |
| this.updateDuration(); |
| this.updateTime(); |
| this.updatePlaying(); |
| this.updateCaptionButton(); |
| this.updateCaptionContainer(); |
| this.updateFullscreenButton(); |
| this.updateVolume(); |
| this.updateHasAudio(); |
| this.updateHasVideo(); |
| }; |
| |
| Controller.prototype = { |
| |
| /* Constants */ |
| HandledVideoEvents: { |
| emptied: 'handleReadyStateChange', |
| loadedmetadata: 'handleReadyStateChange', |
| loadeddata: 'handleReadyStateChange', |
| canplay: 'handleReadyStateChange', |
| canplaythrough: 'handleReadyStateChange', |
| timeupdate: 'handleTimeUpdate', |
| durationchange: 'handleDurationChange', |
| playing: 'handlePlay', |
| pause: 'handlePause', |
| volumechange: 'handleVolumeChange', |
| webkitfullscreenchange: 'handleFullscreenChange', |
| webkitbeginfullscreen: 'handleFullscreenChange', |
| webkitendfullscreen: 'handleFullscreenChange', |
| }, |
| HideControlsDelay: 4 * 1000, |
| ClassNames: { |
| exit: 'exit', |
| hidden: 'hidden', |
| hiding: 'hiding', |
| muteBox: 'mute-box', |
| muted: 'muted', |
| paused: 'paused', |
| selected: 'selected', |
| show: 'show', |
| noVideo: 'no-video', |
| noDuration: 'no-duration', |
| down: 'down', |
| out: 'out', |
| }, |
| KeyCodes: { |
| enter: 13, |
| escape: 27, |
| space: 32, |
| left: 37, |
| up: 38, |
| right: 39, |
| down: 40 |
| }, |
| |
| 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); |
| }, |
| |
| isAudio: function() |
| { |
| return this.video instanceof HTMLAudioElement; |
| }, |
| |
| isFullScreen: function() |
| { |
| return this.video.webkitDisplayingFullscreen; |
| }, |
| |
| shouldHaveControls: function() |
| { |
| if (!this.isAudio() && !this.host.allowsInlineMediaPlayback) |
| return true; |
| |
| return this.video.controls || this.isFullScreen(); |
| }, |
| |
| updateBase: function() |
| { |
| if (this.shouldHaveControls() || (this.video.textTracks && this.video.textTracks.length)) { |
| if (!this.base.parentNode) { |
| this.root.appendChild(this.base); |
| } |
| } else { |
| if (this.base.parentNode) { |
| this.base.parentNode.removeChild(this.base); |
| } |
| } |
| }, |
| |
| createControls: function() |
| { |
| var enclosure = this.controls.enclosure = document.createElement('div'); |
| enclosure.setAttribute('pseudo', '-webkit-media-controls-enclosure'); |
| |
| var panel = this.controls.panel = document.createElement('div'); |
| panel.setAttribute('pseudo', '-webkit-media-controls-panel'); |
| panel.setAttribute('aria-label', (this.isAudio() ? 'Audio Playback' : '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 playButton = this.controls.playButton = document.createElement('button'); |
| playButton.setAttribute('pseudo', '-webkit-media-controls-play-button'); |
| playButton.setAttribute('aria-label', 'Play'); |
| this.listenFor(playButton, 'click', this.handlePlayButtonClicked); |
| |
| 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', 'Elapsed'); |
| currentTime.setAttribute('role', 'timer'); |
| |
| var timeline = this.controls.timeline = document.createElement('input'); |
| timeline.setAttribute('pseudo', '-webkit-media-controls-timeline'); |
| timeline.setAttribute('aria-label', 'Duration'); |
| timeline.type = 'range'; |
| timeline.value = 0; |
| timeline.step = .01; |
| this.listenFor(timeline, 'input', this.handleTimelineChange); |
| this.listenFor(timeline, 'mouseup', this.handleTimelineMouseUp); |
| |
| var muteBox = this.controls.muteBox = document.createElement('div'); |
| muteBox.classList.add(this.ClassNames.muteBox); |
| this.listenFor(muteBox, 'mouseout', this.handleVolumeBoxMouseOut); |
| |
| var muteButton = this.controls.muteButton = document.createElement('button'); |
| muteButton.setAttribute('pseudo', '-webkit-media-controls-mute-button'); |
| muteButton.setAttribute('aria-label', 'Mute'); |
| this.listenFor(muteButton, 'click', this.handleMuteButtonClicked); |
| this.listenFor(muteButton, 'mouseover', this.handleMuteButtonMouseOver); |
| |
| var volumeBox = this.controls.volumeBox = document.createElement('div'); |
| volumeBox.setAttribute('pseudo', '-webkit-media-controls-volume-slider-container'); |
| volumeBox.classList.add(this.ClassNames.hiding); |
| this.listenFor(volumeBox, 'mouseover', this.handleMuteButtonMouseOver); |
| |
| var volume = this.controls.volume = document.createElement('input'); |
| volume.setAttribute('pseudo', '-webkit-media-controls-volume-slider'); |
| volume.setAttribute('aria-label', 'Volume'); |
| volume.type = 'range'; |
| volume.min = 0; |
| volume.max = 1; |
| volume.step = .01; |
| this.listenFor(volume, 'input', this.handleVolumeSliderInput); |
| this.listenFor(volume, 'mouseover', this.handleMuteButtonMouseOver); |
| |
| var captionButton = this.controls.captionButton = document.createElement('button'); |
| captionButton.setAttribute('pseudo', '-webkit-media-controls-toggle-closed-captions-button'); |
| captionButton.setAttribute('aria-label', 'Captions'); |
| captionButton.setAttribute('aria-haspopup', 'true'); |
| captionButton.setAttribute('aria-owns', 'audioTrackMenu'); |
| this.listenFor(captionButton, 'click', this.handleCaptionButtonClicked); |
| this.listenFor(captionButton, 'mouseover', this.handleCaptionButtonMouseOver); |
| this.listenFor(captionButton, 'mouseout', this.handleCaptionButtonMouseOut); |
| |
| var fullscreenButton = this.controls.fullscreenButton = document.createElement('button'); |
| fullscreenButton.setAttribute('pseudo', '-webkit-media-controls-fullscreen-button'); |
| fullscreenButton.setAttribute('aria-label', 'Display Full Screen'); |
| fullscreenButton.disabled = true; |
| this.listenFor(fullscreenButton, 'click', this.handleFullscreenButtonClicked); |
| }, |
| |
| configureControls: function() |
| { |
| this.controls.panel.appendChild(this.controls.playButton); |
| this.controls.panel.appendChild(this.controls.timeline); |
| this.controls.panel.appendChild(this.controls.currentTime); |
| this.controls.panel.appendChild(this.controls.muteBox); |
| this.controls.muteBox.appendChild(this.controls.muteButton); |
| this.controls.muteBox.appendChild(this.controls.volumeBox); |
| this.controls.volumeBox.appendChild(this.controls.volume); |
| this.controls.panel.appendChild(this.controls.captionButton); |
| if (!this.isAudio()) |
| this.controls.panel.appendChild(this.controls.fullscreenButton); |
| this.controls.enclosure.appendChild(this.controls.panel); |
| }, |
| |
| disconnectControls: function(event) |
| { |
| for (var item in this.controls) { |
| var control = this.controls[item]; |
| if (control && control.parentNode) |
| control.parentNode.removeChild(control); |
| } |
| }, |
| |
| reconnectControls: function() |
| { |
| this.disconnectControls(); |
| this.configureControls(); |
| |
| if (this.shouldHaveControls()) |
| this.addControls(); |
| }, |
| |
| showControls: function() |
| { |
| this.controls.panel.classList.remove(this.ClassNames.hidden); |
| this.controls.panel.classList.add(this.ClassNames.show); |
| |
| this.updateTime(); |
| }, |
| |
| hideControls: function() |
| { |
| this.controls.panel.classList.remove(this.ClassNames.show); |
| }, |
| |
| resetHideControlsTimer: function() |
| { |
| if (this.hideTimer) |
| clearTimeout(this.hideTimer); |
| this.hideTimer = setTimeout(this.hideControls.bind(this), this.HideControlsDelay); |
| }, |
| |
| clearHideControlsTimer: function() |
| { |
| if (this.hideTimer) |
| clearTimeout(this.hideTimer); |
| this.hideTimer = null; |
| }, |
| |
| controlsAreAlwaysVisible: function() |
| { |
| return this.isAudio() || 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); |
| }, |
| |
| addControls: function() |
| { |
| this.base.appendChild(this.controls.enclosure); |
| }, |
| |
| removeControls: function() |
| { |
| if (this.controls.enclosure.parentNode) |
| this.controls.enclosure.parentNode.removeChild(this.controls.enclosure); |
| this.hideCaptionMenu(); |
| }, |
| |
| updateControls: function() |
| { |
| this.reconnectControls(); |
| }, |
| |
| setIsLive: function(live) |
| { |
| if (live === this.isLive) |
| return; |
| this.isLive = live; |
| |
| this.reconnectControls(); |
| this.controls.timeline.disabled = this.isLive; |
| }, |
| |
| updateDuration: function() |
| { |
| var duration = this.video.duration; |
| this.setIsLive(duration === Number.POSITIVE_INFINITY); |
| this.controls.timeline.min = 0; |
| this.controls.timeline.max = this.isLive ? 0 : duration; |
| if (this.isLive || isNaN(duration)) |
| this.timeDigitsCount = 4; |
| else if (duration < 10 * 60) /* Ten minutes */ |
| this.timeDigitsCount = 3; |
| else if (duration < 60 * 60) /* One hour */ |
| this.timeDigitsCount = 4; |
| else if (duration < 10 * 60 * 60) /* Ten hours */ |
| this.timeDigitsCount = 5; |
| else |
| this.timeDigitsCount = 6; |
| }, |
| |
| formatTime: function(time) |
| { |
| if (isNaN(time)) |
| return '00:00'; |
| |
| const absTime = Math.abs(time); |
| const seconds = Math.floor(absTime % 60).toFixed(0); |
| const minutes = Math.floor((absTime / 60) % 60).toFixed(0); |
| const hours = Math.floor(absTime / (60 * 60)).toFixed(0); |
| |
| function prependZeroIfNeeded(value) { |
| if (value < 10) |
| return `0${value}`; |
| return `${value}`; |
| } |
| |
| switch (this.timeDigitsCount) { |
| case 3: |
| return minutes + ':' + prependZeroIfNeeded(seconds); |
| case 4: |
| return prependZeroIfNeeded(minutes) + ':' + prependZeroIfNeeded(seconds); |
| case 5: |
| return hours + ':' + prependZeroIfNeeded(minutes) + ':' + prependZeroIfNeeded(seconds); |
| case 6: |
| return prependZeroIfNeeded(hours) + ':' + prependZeroIfNeeded(minutes) + ':' + prependZeroIfNeeded(seconds); |
| } |
| }, |
| |
| updateTime: function(forceUpdate) |
| { |
| if (!forceUpdate && this.controlsAreHidden()) |
| return; |
| |
| var currentTime = this.video.currentTime; |
| this.controls.timeline.value = currentTime; |
| this.controls.currentTime.innerText = this.formatTime(currentTime); |
| if (!this.isLive) { |
| var duration = this.video.duration; |
| this.controls.currentTime.innerText += " / " + this.formatTime(duration); |
| this.controls.currentTime.classList.toggle(this.ClassNames.noDuration, !duration); |
| this.controls.timeline.disabled = !duration; |
| } else |
| this.controls.currentTime.classList.remove(this.ClassNames.noDuration); |
| }, |
| |
| 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', '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', 'Pause'); |
| |
| this.hideControls(); |
| this.resetHideControlsTimer(); |
| } |
| }, |
| |
| updatePlaying: function() |
| { |
| this.setPlaying(!this.canPlay()); |
| if (!this.canPlay()) |
| this.showControls(); |
| }, |
| |
| 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(); |
| }, |
| |
| updateFullscreenButton: function() |
| { |
| if (this.video.readyState > HTMLMediaElement.HAVE_NOTHING && !this.hasVisualMedia) { |
| this.controls.fullscreenButton.classList.add(this.ClassNames.hidden); |
| return; |
| } |
| |
| this.controls.fullscreenButton.disabled = !this.host.supportsFullscreen; |
| }, |
| |
| 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; |
| } |
| }, |
| |
| updateHasAudio: function() |
| { |
| this.controls.muteButton.disabled = this.video.audioTracks.length == 0; |
| }, |
| |
| updateHasVideo: function() |
| { |
| if (this.video.videoTracks.length) |
| this.controls.panel.classList.remove(this.ClassNames.noVideo); |
| else |
| this.controls.panel.classList.add(this.ClassNames.noVideo); |
| }, |
| |
| handleReadyStateChange: function(event) |
| { |
| this.hasVisualMedia = this.video.videoTracks && this.video.videoTracks.length > 0; |
| this.updateVolume(); |
| this.updateDuration(); |
| this.updateCaptionButton(); |
| this.updateCaptionContainer(); |
| this.updateFullscreenButton(); |
| }, |
| |
| handleTimeUpdate: function(event) |
| { |
| this.updateTime(); |
| }, |
| |
| handleDurationChange: function(event) |
| { |
| this.updateDuration(); |
| this.updateTime(true); |
| }, |
| |
| handlePlay: function(event) |
| { |
| this.setPlaying(true); |
| }, |
| |
| handlePause: function(event) |
| { |
| this.setPlaying(false); |
| }, |
| |
| handleVolumeChange: function(event) |
| { |
| this.updateVolume(); |
| }, |
| |
| handleFullscreenChange: function(event) |
| { |
| this.updateBase(); |
| this.updateControls(); |
| |
| if (this.isFullScreen()) { |
| this.controls.fullscreenButton.classList.add(this.ClassNames.exit); |
| this.controls.fullscreenButton.setAttribute('aria-label', 'Exit Full Screen'); |
| this.host.enteredFullscreen(); |
| } else { |
| this.controls.fullscreenButton.classList.remove(this.ClassNames.exit); |
| this.controls.fullscreenButton.setAttribute('aria-label', 'Display Full Screen'); |
| this.host.exitedFullscreen(); |
| } |
| }, |
| |
| handleTextTrackChange: function(event) |
| { |
| this.updateCaptionContainer(); |
| }, |
| |
| handleTextTrackAdd: function(event) |
| { |
| this.updateCaptionButton(); |
| this.updateCaptionContainer(); |
| }, |
| |
| handleTextTrackRemove: function(event) |
| { |
| this.updateCaptionButton(); |
| this.updateCaptionContainer(); |
| }, |
| |
| handleControlsChange: function() |
| { |
| try { |
| this.updateBase(); |
| |
| if (this.shouldHaveControls()) |
| this.addControls(); |
| else |
| this.removeControls(); |
| } catch(e) { |
| if (window.console) |
| console.error(e); |
| } |
| }, |
| |
| 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(); |
| }, |
| |
| 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); |
| }, |
| |
| handleTimelineMouseUp: function(event) |
| { |
| /* 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', 'Unmute'); |
| return true; |
| }, |
| |
| handleMuteButtonMouseOver: function(event) |
| { |
| if (this.video.offsetTop + this.controls.enclosure.offsetTop < 105) { |
| this.controls.volumeBox.classList.add(this.ClassNames.down); |
| this.controls.panel.classList.add(this.ClassNames.down); |
| } else { |
| this.controls.volumeBox.classList.remove(this.ClassNames.down); |
| this.controls.panel.classList.remove(this.ClassNames.down); |
| } |
| this.controls.volumeBox.classList.remove(this.ClassNames.hiding); |
| |
| return true; |
| }, |
| |
| handleVolumeBoxMouseOut: function(event) |
| { |
| this.controls.volumeBox.classList.add(this.ClassNames.hiding); |
| return true; |
| }, |
| |
| handleVolumeSliderInput: function(event) |
| { |
| if (this.video.muted) { |
| this.video.muted = false; |
| this.controls.muteButton.setAttribute('aria-label', 'Mute'); |
| } |
| this.video.volume = this.controls.volume.value; |
| }, |
| |
| handleFullscreenButtonClicked: function(event) |
| { |
| if (this.isFullScreen()) |
| this.video.webkitExitFullscreen(); |
| else |
| this.video.webkitEnterFullscreen(); |
| return true; |
| }, |
| |
| 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.listenFor(this.captionMenu, 'mouseout', this.handleCaptionMenuMouseOut); |
| this.listenFor(this.captionMenu, 'transitionend', this.captionMenuTransitionEnd); |
| this.base.appendChild(this.captionMenu); |
| this.captionMenu.captionMenuTreeElements = []; |
| 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); |
| this.captionMenu.captionMenuTreeElements.push(list); |
| |
| var heading = document.createElement('h3'); |
| heading.id = 'webkitMediaControlsClosedCaptionsHeading'; |
| list.appendChild(heading); |
| heading.innerText = 'Subtitles'; |
| this.captionMenu.captionMenuTreeElements.push(heading); |
| |
| var ul = document.createElement('ul'); |
| ul.setAttribute('role', 'menu'); |
| ul.setAttribute('aria-labelledby', 'webkitMediaControlsClosedCaptionsHeading'); |
| list.appendChild(ul); |
| this.captionMenu.captionMenuTreeElements.push(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; |
| } |
| } |
| |
| /* Caption menu has to be centered to the caption button. */ |
| var captionButtonCenter = this.controls.panel.offsetLeft + this.controls.captionButton.offsetLeft + |
| this.controls.captionButton.offsetWidth / 2; |
| var captionMenuLeft = (captionButtonCenter - this.captionMenu.offsetWidth / 2); |
| if (captionMenuLeft + this.captionMenu.offsetWidth > this.controls.panel.offsetLeft + this.controls.panel.offsetWidth) |
| this.captionMenu.classList.add(this.ClassNames.out); |
| this.captionMenu.style.left = captionMenuLeft + 'px'; |
| /* As height is not in the css, it needs to be specified to animate it. */ |
| this.captionMenu.height = this.captionMenu.offsetHeight; |
| this.captionMenu.style.height = 0; |
| }, |
| |
| captionItemSelected: function(event) |
| { |
| this.host.setSelectedTextTrack(event.target.track); |
| this.hideCaptionMenu(); |
| }, |
| |
| 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.hideCaptionMenu(); |
| break; |
| case this.KeyCodes.left: |
| case this.KeyCodes.up: |
| case this.KeyCodes.right: |
| case this.KeyCodes.down: |
| this.focusSiblingCaptionItem(event); |
| break; |
| default: |
| return; |
| } |
| |
| event.stopPropagation(); |
| event.preventDefault(); |
| }, |
| |
| showCaptionMenu: function() |
| { |
| if (!this.captionMenu) |
| this.buildCaptionMenu(); |
| this.captionMenu.style.height = this.captionMenu.height + 'px'; |
| }, |
| |
| hideCaptionMenu: function() |
| { |
| if (!this.captionMenu) |
| return; |
| this.captionMenu.style.height = 0; |
| }, |
| |
| captionMenuTransitionEnd: function(event) |
| { |
| if (this.captionMenu.offsetHeight !== 0) |
| 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; |
| }, |
| |
| captionMenuContainsNode: function(node) |
| { |
| return this.captionMenu.captionMenuTreeElements.find((item) => item == node) |
| || this.captionMenuItems.find((item) => item == node); |
| }, |
| |
| handleCaptionButtonClicked: function(event) |
| { |
| this.showCaptionMenu(); |
| return true; |
| }, |
| |
| handleCaptionButtonMouseOver: function(event) |
| { |
| this.showCaptionMenu(); |
| return true; |
| }, |
| |
| handleCaptionButtonMouseOut: function(event) |
| { |
| if (this.captionMenu && !this.captionMenuContainsNode(event.relatedTarget)) |
| this.hideCaptionMenu(); |
| return true; |
| }, |
| |
| handleCaptionMenuMouseOut: function(event) |
| { |
| if (event.relatedTarget != this.controls.captionButton && !this.captionMenuContainsNode(event.relatedTarget)) |
| this.hideCaptionMenu(); |
| return true; |
| }, |
| }; |