UNPKG

dashjs

Version:

A reference client implementation for the playback of MPEG DASH via Javascript and compliant browsers.

1,178 lines (1,011 loc) 47 kB
/** * The copyright in this software is being made available under the BSD License, * included below. This software may be subject to other third party and contributor * rights, including patent rights, and no such rights are granted under this license. * * Copyright (c) 2013, Dash Industry Forum. * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation and/or * other materials provided with the distribution. * * Neither the name of Dash Industry Forum nor the names of its * contributors may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ /** * @module ControlBar * @param {object=} dashjsMediaPlayer - dashjs reference * @param {boolean=} displayUTCTimeCodes - true if time is displayed in UTC format, false otherwise */ // eslint-disable-next-line no-unused-vars var ControlBar = function (dashjsMediaPlayer, displayUTCTimeCodes) { var player = this.player = dashjsMediaPlayer; var self = this; var captionMenu = null; var bitrateListMenu = null; var trackSwitchMenu = null; var menuHandlersList = { bitrate: null, caption: null, track: null }; var lastVolumeLevel = NaN; var seeking = false; var videoControllerVisibleTimeout = 0; var liveThresholdSecs = 1; var textTrackList = {}; var forceQuality = false; var video, videoContainer, videoController, playPauseBtn, bitrateListBtn, captionBtn, trackSwitchBtn, seekbar, seekbarPlay, seekbarBuffer, muteBtn, nativeTextTracks, volumebar, fullscreenBtn, timeDisplay, durationDisplay, thumbnailContainer, thumbnailElem, thumbnailTimeLabel, idSuffix, seekbarBufferInterval; //************************************************************************************ // THUMBNAIL CONSTANTS //************************************************************************************ // Maximum percentage of player height that the thumbnail will fill var maxPercentageThumbnailScreen = 0.15; // Separation between the control bar and the thumbnail (in px) var bottomMarginThumbnail = 10; // Maximum scale so small thumbs are not scaled too high var maximumScale = 2; var initControls = function (suffix) { idSuffix = suffix; videoController = document.getElementById(getControlId('videoController')); playPauseBtn = document.getElementById(getControlId('playPauseBtn')); bitrateListBtn = document.getElementById(getControlId('bitrateListBtn')); captionBtn = document.getElementById(getControlId('captionBtn')); trackSwitchBtn = document.getElementById(getControlId('trackSwitchBtn')); seekbar = document.getElementById(getControlId('seekbar')); seekbarPlay = document.getElementById(getControlId('seekbar-play')); seekbarBuffer = document.getElementById(getControlId('seekbar-buffer')); muteBtn = document.getElementById(getControlId('muteBtn')); volumebar = document.getElementById(getControlId('volumebar')); fullscreenBtn = document.getElementById(getControlId('fullscreenBtn')); timeDisplay = document.getElementById(getControlId('videoTime')); durationDisplay = document.getElementById(getControlId('videoDuration')); thumbnailContainer = document.getElementById(getControlId('thumbnail-container')); thumbnailElem = document.getElementById(getControlId('thumbnail-elem')); thumbnailTimeLabel = document.getElementById(getControlId('thumbnail-time-label')); }; var addPlayerEventsListeners = function () { self.player.on(dashjs.MediaPlayer.events.PLAYBACK_STARTED, _onPlayStart, this); self.player.on(dashjs.MediaPlayer.events.PLAYBACK_PAUSED, _onPlaybackPaused, this); self.player.on(dashjs.MediaPlayer.events.PLAYBACK_TIME_UPDATED, _onPlayTimeUpdate, this); self.player.on(dashjs.MediaPlayer.events.STREAM_ACTIVATED, _onStreamActivated, this); self.player.on(dashjs.MediaPlayer.events.STREAM_DEACTIVATED, _onStreamDeactivated, this); self.player.on(dashjs.MediaPlayer.events.STREAM_TEARDOWN_COMPLETE, _onStreamTeardownComplete, this); self.player.on(dashjs.MediaPlayer.events.TEXT_TRACKS_ADDED, _onTracksAdded, this); self.player.on(dashjs.MediaPlayer.events.BUFFER_LEVEL_UPDATED, _onBufferLevelUpdated, this); self.player.on(dashjs.MediaPlayer.events.NEW_TRACK_SELECTED, _onNewTrackSelected, this); self.player.on(dashjs.Protection.events.KEY_STATUSES_MAP_UPDATED, _onKeyStatusChanged, this); }; var removePlayerEventsListeners = function () { self.player.off(dashjs.MediaPlayer.events.PLAYBACK_STARTED, _onPlayStart, this); self.player.off(dashjs.MediaPlayer.events.PLAYBACK_PAUSED, _onPlaybackPaused, this); self.player.off(dashjs.MediaPlayer.events.PLAYBACK_TIME_UPDATED, _onPlayTimeUpdate, this); self.player.off(dashjs.MediaPlayer.events.STREAM_ACTIVATED, _onStreamActivated, this); self.player.off(dashjs.MediaPlayer.events.STREAM_DEACTIVATED, _onStreamDeactivated, this); self.player.off(dashjs.MediaPlayer.events.STREAM_TEARDOWN_COMPLETE, _onStreamTeardownComplete, this); self.player.off(dashjs.MediaPlayer.events.TEXT_TRACKS_ADDED, _onTracksAdded, this); self.player.off(dashjs.MediaPlayer.events.BUFFER_LEVEL_UPDATED, _onBufferLevelUpdated, this); self.player.off(dashjs.MediaPlayer.events.NEW_TRACK_SELECTED, _onNewTrackSelected, this); self.player.off(dashjs.Protection.events.KEY_STATUSES_MAP_UPDATED, _onKeyStatusChanged, this); }; var getControlId = function (id) { return id + (idSuffix ? idSuffix : ''); }; var setPlayer = function (player) { if (self.player) { removePlayerEventsListeners(); } player = self.player = player; addPlayerEventsListeners(); }; //************************************************************************************ // PLAYBACK //************************************************************************************ var togglePlayPauseBtnState = function () { if (self.player.isPaused()) { setPlayBtn(); } else { setPauseBtn(); } }; var setPlayBtn = function () { var span = document.getElementById(getControlId('iconPlayPause')); if (span !== null) { span.classList.remove('icon-pause'); span.classList.add('icon-play'); } }; var setPauseBtn = function () { var span = document.getElementById(getControlId('iconPlayPause')); if (span !== null) { span.classList.remove('icon-play'); span.classList.add('icon-pause'); } }; var _onPlayPauseClick = function (/*e*/) { togglePlayPauseBtnState.call(this); self.player.isPaused() ? self.player.play() : self.player.pause(); }; var _onPlaybackPaused = function (/*e*/) { togglePlayPauseBtnState(); }; var _onPlayStart = function (/*e*/) { setTime(displayUTCTimeCodes ? self.player.timeAsUTC() : self.player.timeInDvrWindow()); updateDuration(); togglePlayPauseBtnState(); if (seekbarBufferInterval) { clearInterval(seekbarBufferInterval); } }; var _onPlayTimeUpdate = function (/*e*/) { updateDuration(); if (!seeking) { setTime(displayUTCTimeCodes ? player.timeAsUTC() : player.timeInDvrWindow()); if (seekbarPlay) { seekbarPlay.style.width = Math.max((player.timeInDvrWindow() / player.duration() * 100), 0) + '%'; } if (seekbar.getAttribute('type') === 'range') { seekbar.value = player.timeInDvrWindow(); } } }; var getBufferLevel = function () { var bufferLevel = 0; if (self.player.getDashMetrics) { var dashMetrics = self.player.getDashMetrics(); if (dashMetrics) { bufferLevel = dashMetrics.getCurrentBufferLevel('video', true); if (!bufferLevel) { bufferLevel = dashMetrics.getCurrentBufferLevel('audio', true); } } } return bufferLevel; }; //************************************************************************************ // VOLUME //************************************************************************************ var toggleMuteBtnState = function () { var span = document.getElementById(getControlId('iconMute')); if (self.player.isMuted()) { span.classList.remove('icon-mute-off'); span.classList.add('icon-mute-on'); } else { span.classList.remove('icon-mute-on'); span.classList.add('icon-mute-off'); } }; var onMuteClick = function (/*e*/) { if (self.player.isMuted() && !isNaN(lastVolumeLevel)) { setVolume(lastVolumeLevel); } else { lastVolumeLevel = parseFloat(volumebar.value); setVolume(0); } self.player.setMute(self.player.getVolume() === 0); toggleMuteBtnState(); }; var setVolume = function (value) { if (typeof value === 'number') { volumebar.value = value; } self.player.setVolume(parseFloat(volumebar.value)); self.player.setMute(self.player.getVolume() === 0); if (isNaN(lastVolumeLevel)) { lastVolumeLevel = self.player.getVolume(); } toggleMuteBtnState(); }; //************************************************************************************ // SEEKING // ************************************************************************************ var calculateTimeByEvent = function (event) { var seekbarRect = seekbar.getBoundingClientRect(); return Math.floor(self.player.duration() * (event.clientX - seekbarRect.left) / seekbarRect.width); }; var onSeeking = function (event) { //TODO Add call to seek in trick-mode once implemented. Preview Frames. seeking = true; var mouseTime = calculateTimeByEvent(event); if (seekbarPlay) { seekbarPlay.style.width = (mouseTime / self.player.duration() * 100) + '%'; } setTime(mouseTime); document.addEventListener('mousemove', onSeekBarMouseMove, true); document.addEventListener('mouseup', onSeeked, true); }; var onSeeked = function (event) { seeking = false; document.removeEventListener('mousemove', onSeekBarMouseMove, true); document.removeEventListener('mouseup', onSeeked, true); // seeking var mouseTime = calculateTimeByEvent(event); if (!isNaN(mouseTime)) { mouseTime = mouseTime < 0 ? 0 : mouseTime; self.player.seek(mouseTime); } onSeekBarMouseMoveOut(event); if (seekbarPlay) { seekbarPlay.style.width = (mouseTime / self.player.duration() * 100) + '%'; } }; var onSeekBarMouseMove = function (event) { if (!thumbnailContainer || !thumbnailElem) return; // Take into account page offset and seekbar position var elem = videoContainer || video; var videoContainerRect = elem.getBoundingClientRect(); var seekbarRect = seekbar.getBoundingClientRect(); var videoControllerRect = videoController.getBoundingClientRect(); // Calculate time position given mouse position var left = event.clientX - seekbarRect.left; var mouseTime = calculateTimeByEvent(event); if (isNaN(mouseTime)) return; // Update timer and play progress bar if mousedown (mouse click down) if (seeking) { setTime(mouseTime); if (seekbarPlay) { seekbarPlay.style.width = (mouseTime / self.player.duration() * 100) + '%'; } } // Get thumbnail information if (self.player.provideThumbnail) { self.player.provideThumbnail(mouseTime, function (thumbnail) { if (!thumbnail) return; // Adjust left variable for positioning thumbnail with regards to its viewport left += (seekbarRect.left - videoContainerRect.left); // Take into account thumbnail control var ctrlWidth = parseInt(window.getComputedStyle(thumbnailElem).width); if (!isNaN(ctrlWidth)) { left -= ctrlWidth / 2; } var scale = (videoContainerRect.height * maxPercentageThumbnailScreen) / thumbnail.height; if (scale > maximumScale) { scale = maximumScale; } // Set thumbnail control position thumbnailContainer.style.left = left + 'px'; thumbnailContainer.style.display = ''; thumbnailContainer.style.bottom += Math.round(videoControllerRect.height + bottomMarginThumbnail) + 'px'; thumbnailContainer.style.height = Math.round(thumbnail.height) + 'px'; var backgroundStyle = 'url("' + thumbnail.url + '") ' + (thumbnail.x > 0 ? '-' + thumbnail.x : '0') + 'px ' + (thumbnail.y > 0 ? '-' + thumbnail.y : '0') + 'px'; thumbnailElem.style.background = backgroundStyle; thumbnailElem.style.width = thumbnail.width + 'px'; thumbnailElem.style.height = thumbnail.height + 'px'; thumbnailElem.style.transform = 'scale(' + scale + ',' + scale + ')'; if (thumbnailTimeLabel) { thumbnailTimeLabel.textContent = displayUTCTimeCodes ? self.player.formatUTC(mouseTime) : self.player.convertToTimeCode(mouseTime); } }); } }; var onSeekBarMouseMoveOut = function (/*e*/) { if (!thumbnailContainer) return; thumbnailContainer.style.display = 'none'; }; var seekLive = function () { self.player.seekToOriginalLive(); }; //************************************************************************************ // TIME/DURATION //************************************************************************************ var setDuration = function (value) { if (self.player.isDynamic()) { durationDisplay.textContent = '● LIVE'; if (!durationDisplay.onclick) { durationDisplay.onclick = seekLive; durationDisplay.classList.add('live-icon'); } } else if (!isNaN(value) && isFinite(value)) { durationDisplay.textContent = displayUTCTimeCodes ? self.player.formatUTC(value) : self.player.convertToTimeCode(value); durationDisplay.classList.remove('live-icon'); } }; var setTime = function (value) { if (value < 0) { return; } if (self.player.isDynamic() && self.player.duration()) { var liveDelay = Math.max(self.player.duration() - value, 0); var targetLiveDelay = self.player.getTargetLiveDelay(); if (liveDelay < targetLiveDelay + liveThresholdSecs) { durationDisplay.classList.add('live'); } else { durationDisplay.classList.remove('live'); } timeDisplay.textContent = '- ' + self.player.convertToTimeCode(liveDelay); } else if (!isNaN(value)) { timeDisplay.textContent = displayUTCTimeCodes ? self.player.formatUTC(value) : self.player.convertToTimeCode(value); } }; var updateDuration = function () { var duration = self.player.duration(); if (duration !== parseFloat(seekbar.max)) { //check if duration changes for live streams.. setDuration(displayUTCTimeCodes ? self.player.getDvrWindow().endAsUtc : duration); seekbar.max = duration; } }; //************************************************************************************ // FULLSCREEN //************************************************************************************ var onFullScreenChange = function (/*e*/) { var icon; if (isFullscreen()) { enterFullscreen(); icon = fullscreenBtn.querySelector('.icon-fullscreen-enter'); icon.classList.remove('icon-fullscreen-enter'); icon.classList.add('icon-fullscreen-exit'); } else { exitFullscreen(); icon = fullscreenBtn.querySelector('.icon-fullscreen-exit'); icon.classList.remove('icon-fullscreen-exit'); icon.classList.add('icon-fullscreen-enter'); } }; var isFullscreen = function () { return document.fullscreenElement || document.msFullscreenElement || document.mozFullScreen || document.webkitIsFullScreen; }; var enterFullscreen = function () { var element = videoContainer || video; if (!document.fullscreenElement) { if (element.requestFullscreen) { element.requestFullscreen(); } else if (element.msRequestFullscreen) { element.msRequestFullscreen(); } else if (element.mozRequestFullScreen) { element.mozRequestFullScreen(); } else { element.webkitRequestFullScreen(); } } videoController.classList.add('video-controller-fullscreen'); window.addEventListener('mousemove', onFullScreenMouseMove); onFullScreenMouseMove(); }; var onFullScreenMouseMove = function () { clearFullscreenState(); videoControllerVisibleTimeout = setTimeout(function () { videoController.classList.add('hide'); }, 4000); }; var clearFullscreenState = function () { clearTimeout(videoControllerVisibleTimeout); videoController.classList.remove('hide'); }; var exitFullscreen = function () { window.removeEventListener('mousemove', onFullScreenMouseMove); clearFullscreenState(); if (document.fullscreenElement) { if (document.exitFullscreen) { document.exitFullscreen(); } else if (document.mozCancelFullScreen) { document.mozCancelFullScreen(); } else if (document.msExitFullscreen) { document.msExitFullscreen(); } else { document.webkitCancelFullScreen(); } } videoController.classList.remove('video-controller-fullscreen'); }; var onFullscreenClick = function (/*e*/) { if (!isFullscreen()) { enterFullscreen(); } else { exitFullscreen(); } if (captionMenu) { captionMenu.classList.add('hide'); } if (bitrateListMenu) { bitrateListMenu.classList.add('hide'); } if (trackSwitchMenu) { trackSwitchMenu.classList.add('hide'); } }; //************************************************************************************ // Audio Video MENU //************************************************************************************ var _onStreamDeactivated = function (e) { if (e.streamInfo && textTrackList[e.streamInfo.id]) { delete textTrackList[e.streamInfo.id]; } }; var _onStreamActivated = function (e) { var streamInfo = e.streamInfo; updateDuration(); //Bitrate Menu createBitrateSwitchMenu(); //Track Switch Menu createTrackSwitchMenu(); //Text Switch Menu createCaptionSwitchMenu(streamInfo); }; var createBitrateSwitchMenu = function () { var contentFunc; if (bitrateListBtn) { destroyMenu(bitrateListMenu, bitrateListBtn, menuHandlersList.bitrate); bitrateListMenu = null; var availableBitrates = { menuType: 'bitrate' }; availableBitrates.audio = self.player.getRepresentationsByType && self.player.getRepresentationsByType('audio') || []; availableBitrates.video = self.player.getRepresentationsByType && self.player.getRepresentationsByType('video') || []; availableBitrates.images = self.player.getRepresentationsByType && self.player.getRepresentationsByType('image') || []; if (availableBitrates.audio.length >= 1 || availableBitrates.video.length >= 1 || availableBitrates.images.length >= 1) { contentFunc = function (element, index) { var result = isNaN(index) ? ' Auto Switch' : Math.floor(element.bitrateInKbit) + ' kbps'; result += element && element.width && element.height ? ' (' + element.width + 'x' + element.height + ')' : ''; result += element && element.codecs ? ' (' + element.codecs + ')' : ''; return result; }; bitrateListMenu = createMenu(availableBitrates, contentFunc); var func = function () { onMenuClick(bitrateListMenu, bitrateListBtn); }; menuHandlersList.bitrate = func; bitrateListBtn.addEventListener('click', func); bitrateListBtn.classList.remove('hide'); } else { bitrateListBtn.classList.add('hide'); } } }; var createTrackSwitchMenu = function () { var contentFunc; if (trackSwitchBtn) { destroyMenu(trackSwitchMenu, trackSwitchBtn, menuHandlersList.track); trackSwitchMenu = null; var availableTracks = { menuType: 'track' }; availableTracks.audio = self.player.getTracksFor('audio'); availableTracks.video = self.player.getTracksFor('video'); // these return empty arrays so no need to check for null if (availableTracks.audio.length > 1 || availableTracks.video.length > 1) { contentFunc = function (element) { var label = getLabelForLocale(element.labels); var info = ''; if (element.lang) { info += 'Language - ' + element.lang + ' '; } if (element.roles[0]) { info += '- Role: ' + element.roles[0].value + ' '; } if (element.codec) { info += '- Codec: ' + element.codec + ' '; } if (element.id) { info += '- Id: ' + element.id + ' '; } return label || info }; trackSwitchMenu = createMenu(availableTracks, contentFunc); var func = function () { onMenuClick(trackSwitchMenu, trackSwitchBtn); }; menuHandlersList.track = func; trackSwitchBtn.addEventListener('click', func); trackSwitchBtn.classList.remove('hide'); } } }; // Match up the current dashjs text tracks against native video element tracks by ensuring they have matching properties var _matchTrackWithNativeTrack = function (track, nativeTrack) { let label = track.id !== undefined ? track.id.toString() : track.lang; return !!( (track.kind === nativeTrack.kind) && (track.lang === nativeTrack.language) && (track.isTTML === nativeTrack.isTTML) && (track.isEmbedded === nativeTrack.isEmbedded) && (label === nativeTrack.label) ); } // Compare track information against native video element tracks to get the current track mode var _getNativeVideoTrackMode = function (track) { const nativeTracks = video.textTracks; let trackMode; for (let i = 0; i < nativeTracks.length; i++) { const nativeTrack = nativeTracks[i]; if (_matchTrackWithNativeTrack(track, nativeTrack)) { trackMode = nativeTrack.mode; break; } } ; return (trackMode === undefined) ? 'showing' : trackMode; }; var createCaptionSwitchMenu = function (streamId) { // Subtitles/Captions Menu //XXX we need to add two layers for captions & subtitles if present. var activeStreamInfo = player.getActiveStream().getStreamInfo(); if (captionBtn && (!activeStreamInfo.id || activeStreamInfo.id === streamId)) { destroyMenu(captionMenu, captionBtn, menuHandlersList.caption); captionMenu = null; var tracks = textTrackList[streamId] || []; var contentFunc = function (element, index) { if (isNaN(index)) { return { mode: 'showing', text: 'OFF' }; } var label = getLabelForLocale(element.labels); var trackText; if (label) { trackText = label + ' : ' + element.type; } else { trackText = element.lang + ' : ' + element.kind; } return { mode: _getNativeVideoTrackMode(element), text: trackText } }; captionMenu = createMenu({ menuType: 'caption', arr: tracks }, contentFunc); var func = function () { onMenuClick(captionMenu, captionBtn); }; menuHandlersList.caption = func; captionBtn.addEventListener('click', func); captionBtn.classList.remove('hide'); } }; var _onTracksChanged = function () { var activeStreamInfo = player.getActiveStream().getStreamInfo(); createCaptionSwitchMenu(activeStreamInfo.id); } var _onTracksAdded = function (e) { // Subtitles/Captions Menu //XXX we need to add two layers for captions & subtitles if present. if (!textTrackList[e.streamId]) { textTrackList[e.streamId] = []; } textTrackList[e.streamId] = textTrackList[e.streamId].concat(e.tracks); nativeTextTracks = video.textTracks; nativeTextTracks.addEventListener('change', _onTracksChanged); createCaptionSwitchMenu(e.streamId); }; var _onNewTrackSelected = function () { createTrackSwitchMenu(); createBitrateSwitchMenu(); } var _onKeyStatusChanged = function () { createBitrateSwitchMenu(); } var _onBufferLevelUpdated = function () { if (seekbarBuffer) { seekbarBuffer.style.width = ((player.timeInDvrWindow() + getBufferLevel()) / player.duration() * 100) + '%'; } }; var _onStreamTeardownComplete = function (/*e*/) { setPlayBtn(); timeDisplay.textContent = '00:00'; }; var createMenu = function (info, contentFunc) { var menuType = info.menuType; var el = document.createElement('div'); el.id = menuType + 'Menu'; el.classList.add('menu'); el.classList.add('hide'); el.classList.add('unselectable'); el.classList.add('menu-item-unselected'); videoController.appendChild(el); switch (menuType) { case 'caption': el.appendChild(document.createElement('ul')); el = createMenuContent(el, getMenuContent(menuType, info.arr, contentFunc), 'caption', menuType + '-list'); setMenuItemsState(getMenuInitialIndex(info, menuType), menuType + '-list'); break; case 'track': case 'bitrate': if (info.video.length >= 1) { el.appendChild(createMediaTypeMenu('video')); el = createMenuContent(el, getMenuContent(menuType, info.video, contentFunc), 'video', 'video-' + menuType + '-list'); setMenuItemsState(getMenuInitialIndex(info.video, menuType, 'video'), 'video-' + menuType + '-list'); } if (info.audio.length >= 1) { el.appendChild(createMediaTypeMenu('audio')); el = createMenuContent(el, getMenuContent(menuType, info.audio, contentFunc), 'audio', 'audio-' + menuType + '-list'); setMenuItemsState(getMenuInitialIndex(info.audio, menuType, 'audio'), 'audio-' + menuType + '-list'); } if (info.images && info.images.length >= 1) { el.appendChild(createMediaTypeMenu('image')); el = createMenuContent(el, getMenuContent(menuType, info.images, contentFunc, false), 'image', 'image-' + menuType + '-list'); setMenuItemsState(getMenuInitialIndex(info.images, menuType, 'image'), 'image-' + menuType + '-list'); } break; } window.addEventListener('resize', handleMenuPositionOnResize, true); return el; }; var getMenuInitialIndex = function (info, menuType, mediaType) { if (menuType === 'track') { var mediaInfo = self.player.getCurrentTrackFor(mediaType); var idx = 0; info.some(function (element, index) { if (isTracksEqual(element, mediaInfo)) { idx = index; return true; } }); return idx; } else if (menuType === 'bitrate') { var cfg = self.player.getSettings(); if (cfg.streaming && cfg.streaming.abr && cfg.streaming.abr.initialBitrate) { return cfg.streaming.abr.initialBitrate['mediaType'] | 0; } return 0; } else if (menuType === 'caption') { return self.player.getCurrentTextTrackIndex() + 1; } }; var isTracksEqual = function (t1, t2) { var sameId = t1.id === t2.id; var sameViewpoint = t1.viewpoint === t2.viewpoint; var sameLang = t1.lang === t2.lang; var sameRoles = t1.roles.toString() === t2.roles.toString(); var sameAccessibility = (!t1.accessibility && !t2.accessibility) || (t1.accessibility && t2.accessibility && t1.accessibility.toString() === t2.accessibility.toString()); var sameAudioChannelConfiguration = (!t1.audioChannelConfiguration && !t2.audioChannelConfiguration) || (t1.audioChannelConfiguration && t2.audioChannelConfiguration && t1.audioChannelConfiguration.toString() === t2.audioChannelConfiguration.toString()); return (sameId && sameViewpoint && sameLang && sameRoles && sameAccessibility && sameAudioChannelConfiguration); }; var getMenuContent = function (type, arr, contentFunc, autoswitch) { autoswitch = (autoswitch !== undefined) ? autoswitch : true; var content = []; arr.forEach(function (element, index) { content.push(contentFunc(element, index)); }); if (type !== 'track' && autoswitch) { content.unshift(contentFunc(null, NaN)); } return content; }; var getBrowserLocale = function () { return (navigator.languages && navigator.languages.length) ? navigator.languages : [navigator.language]; }; var getLabelForLocale = function (labels) { var locales = getBrowserLocale(); for (var i = 0; i < labels.length; i++) { for (var j = 0; j < locales.length; j++) { if (labels[i].lang && locales[j] && locales[j].indexOf(labels[i].lang) > -1) { return labels[i].text; } } } return labels.length === 1 ? labels[0].text : null; }; var createMediaTypeMenu = function (type) { var div = document.createElement('div'); var title = document.createElement('div'); var content = document.createElement('ul'); div.id = type; title.textContent = type.charAt(0).toUpperCase() + type.slice(1); title.classList.add('menu-sub-menu-title'); content.id = type + 'Content'; content.classList.add(type + '-menu-content'); div.appendChild(title); div.appendChild(content); return div; }; var createMenuContent = function (menu, arr, mediaType, name) { for (var i = 0; i < arr.length; i++) { var item = document.createElement('li'); item.id = name + 'Item_' + i; item.index = i; item.mediaType = mediaType; item.name = name; item.selected = false; if (isObject(arr[i])) { // text tracks need extra properties item.mode = arr[i].mode; item.textContent = arr[i].text; } else { // Other tracks will just have their text item.textContent = arr[i]; } item.onmouseover = function (/*e*/) { if (this.selected !== true) { this.classList.add('menu-item-over'); } }; item.onmouseout = function (/*e*/) { this.classList.remove('menu-item-over'); }; item.onclick = setMenuItemsState.bind(item); var el; if (mediaType === 'caption') { el = menu.querySelector('ul'); } else { el = menu.querySelector('.' + mediaType + '-menu-content'); } if (mediaType === 'caption') { if (item.mode !== 'disabled') { el.appendChild(item); } } else { el.appendChild(item); } } return menu; }; var onMenuClick = function (menu, btn) { if (menu.classList.contains('hide')) { menu.classList.remove('hide'); menu.onmouseleave = function (/*e*/) { this.classList.add('hide'); }; } else { menu.classList.add('hide'); } menu.style.position = isFullscreen() ? 'fixed' : 'absolute'; positionMenu(menu, btn); }; var setMenuItemsState = function (value, type) { try { var item = typeof value === 'number' ? document.getElementById(type + 'Item_' + value) : this; if (item) { var nodes = item.parentElement.children; for (var i = 0; i < nodes.length; i++) { nodes[i].selected = false; nodes[i].classList.remove('menu-item-selected'); nodes[i].classList.add('menu-item-unselected'); } item.selected = true; item.classList.remove('menu-item-over'); item.classList.remove('menu-item-unselected'); item.classList.add('menu-item-selected'); if (type === undefined) { // User clicked so type is part of item binding. switch (item.name) { case 'video-bitrate-list': case 'audio-bitrate-list': var cfg = { 'streaming': { 'abr': { 'autoSwitchBitrate': {} } } }; if (item.index > 0) { cfg.streaming.abr.autoSwitchBitrate[item.mediaType] = false; self.player.updateSettings(cfg); self.player.setRepresentationForTypeByIndex(item.mediaType, item.index - 1, forceQuality); } else { cfg.streaming.abr.autoSwitchBitrate[item.mediaType] = true; self.player.updateSettings(cfg); } break; case 'image-bitrate-list': player.setRepresentationForTypeByIndex(item.mediaType, item.index); break; case 'caption-list': self.player.setTextTrack(item.index - 1); break; case 'video-track-list': case 'audio-track-list': self.player.setCurrentTrack(self.player.getTracksFor(item.mediaType)[item.index]); break; } } } } catch (e) { console.error(e); } }; var handleMenuPositionOnResize = function (/*e*/) { if (captionMenu) { positionMenu(captionMenu, captionBtn); } if (bitrateListMenu) { positionMenu(bitrateListMenu, bitrateListBtn); } if (trackSwitchMenu) { positionMenu(trackSwitchMenu, trackSwitchBtn); } }; var positionMenu = function (menu, btn) { if (btn.offsetLeft + menu.clientWidth >= videoController.clientWidth) { menu.style.right = '0px'; menu.style.left = ''; } else { menu.style.left = btn.offsetLeft + 'px'; } var menu_y = videoController.offsetTop - menu.offsetHeight; menu.style.top = menu_y + 'px'; }; var destroyMenu = function (menu, btn, handler) { try { if (menu && videoController) { btn.removeEventListener('click', handler); videoController.removeChild(menu); } } catch (e) { } }; var removeMenu = function (menu, btn) { try { if (menu) { videoController.removeChild(menu); menu = null; btn.classList.add('hide'); } } catch (e) { } }; //************************************************************************************ //IE FIX //************************************************************************************ var coerceIEInputAndChangeEvents = function (slider, addChange) { var fireChange = function (/*e*/) { var changeEvent = document.createEvent('Event'); changeEvent.initEvent('change', true, true); changeEvent.forceChange = true; slider.dispatchEvent(changeEvent); }; this.addEventListener('change', function (e) { var inputEvent; if (!e.forceChange && e.target.getAttribute('type') === 'range') { e.stopPropagation(); inputEvent = document.createEvent('Event'); inputEvent.initEvent('input', true, true); e.target.dispatchEvent(inputEvent); if (addChange) { e.target.removeEventListener('mouseup', fireChange);//TODO can not clean up this event on destroy. refactor needed! e.target.addEventListener('mouseup', fireChange); } } }, true); }; var isIE = function () { return !!navigator.userAgent.match(/Trident.*rv[ :]*11\./); }; //************************************************************************************ //Utilities //************************************************************************************ var isObject = function (obj) { return typeof obj === 'object' && !Array.isArray(obj) && obj !== null; } //************************************************************************************ // PUBLIC API //************************************************************************************ return { setVolume: setVolume, setDuration: setDuration, setTime: setTime, setPlayer: setPlayer, removeMenu: removeMenu, initialize: function (suffix) { if (!player) { throw new Error('Please pass an instance of MediaPlayer.js when instantiating the ControlBar Object'); } video = player.getVideoElement(); if (!video) { throw new Error('Please call initialize after you have called attachView on MediaPlayer.js'); } displayUTCTimeCodes = displayUTCTimeCodes === undefined ? false : displayUTCTimeCodes; initControls(suffix); video.controls = false; videoContainer = video.parentNode; captionBtn.classList.add('hide'); if (trackSwitchBtn) { trackSwitchBtn.classList.add('hide'); } addPlayerEventsListeners(); playPauseBtn.addEventListener('click', _onPlayPauseClick); muteBtn.addEventListener('click', onMuteClick); fullscreenBtn.addEventListener('click', onFullscreenClick); seekbar.addEventListener('mousedown', onSeeking, true); seekbar.addEventListener('mousemove', onSeekBarMouseMove, true); // set passive to true for scroll blocking listeners (https://www.chromestatus.com/feature/5745543795965952) seekbar.addEventListener('touchmove', onSeekBarMouseMove, { passive: true }); seekbar.addEventListener('mouseout', onSeekBarMouseMoveOut, true); seekbar.addEventListener('touchcancel', onSeekBarMouseMoveOut, true); seekbar.addEventListener('touchend', onSeekBarMouseMoveOut, true); volumebar.addEventListener('input', setVolume, true); document.addEventListener('fullscreenchange', onFullScreenChange, false); document.addEventListener('MSFullscreenChange', onFullScreenChange, false); document.addEventListener('mozfullscreenchange', onFullScreenChange, false); document.addEventListener('webkitfullscreenchange', onFullScreenChange, false); //IE 11 Input Fix. if (isIE()) { coerceIEInputAndChangeEvents(seekbar, true); coerceIEInputAndChangeEvents(volumebar, false); } }, show: function () { videoController.classList.remove('hide'); }, hide: function () { videoController.classList.add('hide'); }, disable: function () { videoController.classList.add('disable'); }, enable: function () { videoController.classList.remove('disable'); }, forceQualitySwitch: function (value) { forceQuality = value; }, resetSelectionMenus: function () { if (menuHandlersList.bitrate) { bitrateListBtn.removeEventListener('click', menuHandlersList.bitrate); } if (menuHandlersList.track) { trackSwitchBtn.removeEventListener('click', menuHandlersList.track); } if (menuHandlersList.caption) { captionBtn.removeEventListener('click', menuHandlersList.caption); nativeTextTracks.removeEventListener('change', _onTracksChanged); } if (captionMenu) { this.removeMenu(captionMenu, captionBtn); } if (trackSwitchMenu) { this.removeMenu(trackSwitchMenu, trackSwitchBtn); } if (bitrateListMenu) { this.removeMenu(bitrateListMenu, bitrateListBtn); } }, reset: function () { window.removeEventListener('resize', handleMenuPositionOnResize); this.resetSelectionMenus(); menuHandlersList = []; seeking = false; if (seekbarPlay) { seekbarPlay.style.width = '0%'; } if (seekbarBuffer) { seekbarBuffer.style.width = '0%'; } }, destroy: function () { this.reset(); playPauseBtn.removeEventListener('click', _onPlayPauseClick); muteBtn.removeEventListener('click', onMuteClick); fullscreenBtn.removeEventListener('click', onFullscreenClick); seekbar.removeEventListener('mousedown', onSeeking); volumebar.removeEventListener('input', setVolume); seekbar.removeEventListener('mousemove', onSeekBarMouseMove); seekbar.removeEventListener('touchmove', onSeekBarMouseMove); seekbar.removeEventListener('mouseout', onSeekBarMouseMoveOut); seekbar.removeEventListener('touchcancel', onSeekBarMouseMoveOut); seekbar.removeEventListener('touchend', onSeekBarMouseMoveOut); removePlayerEventsListeners(); document.removeEventListener('fullscreenchange', onFullScreenChange); document.removeEventListener('MSFullscreenChange', onFullScreenChange); document.removeEventListener('mozfullscreenchange', onFullScreenChange); document.removeEventListener('webkitfullscreenchange', onFullScreenChange); } }; };