diff --git a/src/index.html b/src/index.html index 1285b6535..780e7dd79 100644 --- a/src/index.html +++ b/src/index.html @@ -15,6 +15,9 @@ + + + FreeTube Player @@ -104,12 +107,6 @@ -
- -
-
@@ -141,22 +138,14 @@ + - + + + + + + diff --git a/src/js/events.js b/src/js/events.js index 11e2dbae6..622002a5f 100644 --- a/src/js/events.js +++ b/src/js/events.js @@ -245,9 +245,9 @@ let fullscreenVideo = function (event) { $(document).on('click', '#showComments', showComments); -$(document).on('click', '.videoPlayer', playPauseVideo); +// $(document).on('click', '.videoPlayer', playPauseVideo); -$(document).on('dblclick', '.videoPlayer', fullscreenVideo); +// $(document).on('dblclick', '.videoPlayer', fullscreenVideo); $(document).on('keydown', videoShortcutHandler); diff --git a/src/js/player.js b/src/js/player.js index d170b1c75..7c1b60792 100644 --- a/src/js/player.js +++ b/src/js/player.js @@ -19,6 +19,8 @@ * File for functions related to videos. */ + let checkedSettings = false; + /** * Display the video player and play a video * @@ -31,6 +33,7 @@ function playVideo(videoId, playlistId = '') { let youtubedlFinished = false; let invidiousFinished = false; + checkedSettings = false; playerView.playerSeen = true; playerView.firstLoad = true; playerView.videoId = videoId; @@ -41,9 +44,11 @@ function playVideo(videoId, playlistId = '') { playerView.video720p = undefined; playerView.valid720p = true; playerView.videoUrl = ''; + playerView.videoDash = 'https://invidio.us/api/manifest/dash/' + videoId + '.mpd'; playerView.embededHtml = ""; let videoHtml = ''; + let player; const checkSavedVideo = videoIsSaved(videoId); @@ -467,7 +472,30 @@ function clickMiniPlayer(videoId) { } function checkVideoSettings() { - let player = document.getElementById('videoPlayer'); + //let player = document.getElementById('videoPlayer'); + + if (checkedSettings) { + return; + } + + checkedSettings = true; + console.log('checking Settings'); + + let player = new MediaElementPlayer('player', { + features: ['playpause', 'current', 'progress', 'duration', 'volume', 'loop', 'stop', 'speed', 'quality', 'fullscreen'], + + speeds: ['2', '1.75', '1.5', '1.25', '1', '0.75', '0.5', '0.25'], + defaultSpeed: '1', + qualityText: 'Quality', + defaultQuality: 'auto', + stretching: 'fill', + + success: function(mediaElement, originalNode, instance) { + console.log(mediaElement,originalNode,instance); + } + }); + + return; if (autoplay) { player.play(); diff --git a/src/js/plugins/context-menu/context-menu-i18n.js b/src/js/plugins/context-menu/context-menu-i18n.js new file mode 100644 index 000000000..e5831e2e8 --- /dev/null +++ b/src/js/plugins/context-menu/context-menu-i18n.js @@ -0,0 +1,107 @@ +'use strict'; + +if (mejs.i18n.ca !== undefined) { + mejs.i18n.ca['mejs.fullscreen-off'] = 'Desconnectar pantalla completaa'; + mejs.i18n.ca['mejs.fullscreen-on'] = 'Anar a pantalla completa'; + mejs.i18n.ca['mejs.download-video'] = 'Descarregar vídeo'; +} +if (mejs.i18n.cs !== undefined) { + mejs.i18n.cs['mejs.fullscreen-off'] = 'Vypnout režim celá obrazovka'; + mejs.i18n.cs['mejs.fullscreen-on'] = 'Na celou obrazovku'; + mejs.i18n.cs['mejs.download-video'] = 'Stáhnout video'; +} +if (mejs.i18n.de !== undefined) { + mejs.i18n.de['mejs.fullscreen-off'] = 'Vollbildmodus beenden'; + mejs.i18n.de['mejs.fullscreen-on'] = 'Vollbild'; + mejs.i18n.de['mejs.download-video'] = 'Video herunterladen'; +} +if (mejs.i18n.es !== undefined) { + mejs.i18n.es['mejs.fullscreen-off'] = 'Desconectar pantalla completa'; + mejs.i18n.es['mejs.fullscreen-on'] = 'Ir a pantalla completa'; + mejs.i18n.es['mejs.download-video'] = 'Descargar vídeo'; +} +if (mejs.i18n.fa !== undefined) { + mejs.i18n.fa['mejs.fullscreen-off'] = 'تمام صفحه را خاموش کنید'; + mejs.i18n.fa['mejs.fullscreen-on'] = 'برو تمام صفحه'; + mejs.i18n.fa['mejs.download-video'] = 'دانلود فیلم'; +} +if (mejs.i18n.fr !== undefined) { + mejs.i18n.fr['mejs.fullscreen-off'] = 'Quitter le mode plein écran'; + mejs.i18n.fr['mejs.fullscreen-on'] = 'Afficher en plein écran'; + mejs.i18n.fr['mejs.download-video'] = 'Télécharger la vidéo'; +} +if (mejs.i18n.hr !== undefined) { + mejs.i18n.hr['mejs.fullscreen-off'] = 'Isključi puni zaslon'; + mejs.i18n.hr['mejs.fullscreen-on'] = 'Uključi puni zaslon'; + mejs.i18n.hr['mejs.download-video'] = 'Preuzmi video'; +} +if (mejs.i18n.hu !== undefined) { + mejs.i18n.hu['mejs.fullscreen-off'] = 'Teljes képernyő kikapcsolása'; + mejs.i18n.hu['mejs.fullscreen-on'] = 'Átlépés teljes képernyős módra'; + mejs.i18n.hu['mejs.download-video'] = 'Videó letöltése'; +} +if (mejs.i18n.it !== undefined) { + mejs.i18n.it['mejs.fullscreen-off'] = 'Disattivare lo schermo intero'; + mejs.i18n.it['mejs.fullscreen-on'] = 'Attivare lo schermo intero'; + mejs.i18n.it['mejs.download-video'] = 'Scaricare il video'; +} +if (mejs.i18n.ja !== undefined) { + mejs.i18n.ja['mejs.fullscreen-off'] = '全画面をオフにする'; + mejs.i18n.ja['mejs.fullscreen-on'] = '全画面にする'; + mejs.i18n.ja['mejs.download-video'] = '動画をダウンロードする'; +} +if (mejs.i18n.ko !== undefined) { + mejs.i18n.ko['mejs.fullscreen-off'] = '전체화면 해제'; + mejs.i18n.ko['mejs.fullscreen-on'] = '전체화면 가기'; + mejs.i18n.ko['mejs.download-video'] = '비디오 다운로드'; +} +if (mejs.i18n.nl !== undefined) { + mejs.i18n.nl['mejs.fullscreen-off'] = 'Volledig scherm uitschakelen'; + mejs.i18n.nl['mejs.fullscreen-on'] = 'Volledig scherm'; + mejs.i18n.nl['mejs.download-video'] = 'Video downloaden'; +} +if (mejs.i18n.pl !== undefined) { + mejs.i18n.pl['mejs.fullscreen-off'] = 'Wyłącz pełny ekran'; + mejs.i18n.pl['mejs.fullscreen-on'] = 'Przejdź na pełny ekran'; + mejs.i18n.pl['mejs.download-video'] = 'Pobierz wideo'; +} +if (mejs.i18n.pt !== undefined) { + mejs.i18n.pt['mejs.fullscreen-off'] = 'Desligar ecrã completo'; + mejs.i18n.pt['mejs.fullscreen-on'] = 'Ir para ecrã completo'; + mejs.i18n.pt['mejs.download-video'] = 'Descarregar o vídeo'; +} +if (mejs.i18n.ro !== undefined) { + mejs.i18n.ro['mejs.fullscreen-off'] = 'Opreşte ecranul complet'; + mejs.i18n.ro['mejs.fullscreen-on'] = 'Treci la ecran complet'; + mejs.i18n.ro['mejs.download-video'] = 'Descarcă fişierul video'; +} +if (mejs.i18n.ru !== undefined) { + mejs.i18n.ru['mejs.fullscreen-off'] = 'Выключить полноэкранный режим'; + mejs.i18n.ru['mejs.fullscreen-on'] = 'Перейти в полноэкранный режим'; + mejs.i18n.ru['mejs.download-video'] = 'Скачать видео'; +} +if (mejs.i18n.sk !== undefined) { + mejs.i18n.sk['mejs.fullscreen-off'] = 'Vypnúť celú obrazovku'; + mejs.i18n.sk['mejs.fullscreen-on'] = 'Prejsť na celú obrazovku'; + mejs.i18n.sk['mejs.download-video'] = 'Prevziať video'; +} +if (mejs.i18n.sv !== undefined) { + mejs.i18n.sv['mejs.fullscreen-off'] = 'Stäng av Fullskärmläge'; + mejs.i18n.sv['mejs.fullscreen-on'] = 'Visa i Fullskärmsläge'; + mejs.i18n.sv['mejs.download-video'] = 'Ladda ner Video'; +} +if (mejs.i18n.uk !== undefined) { + mejs.i18n.uk['mejs.fullscreen-off'] = 'Вимкнути повноекранний режим'; + mejs.i18n.uk['mejs.fullscreen-on'] = 'Увійти в повноекранний режим'; + mejs.i18n.uk['mejs.download-video'] = 'Скачати відео'; +} +if (mejs.i18n.zh !== undefined) { + mejs.i18n.zh['mejs.fullscreen-off'] = '關閉全屏'; + mejs.i18n.zh['mejs.fullscreen-on'] = '轉向全屏'; + mejs.i18n.zh['mejs.download-video'] = '下載視頻'; +} +if (mejs.i18n['zh-CN'] !== undefined) { + mejs.i18n['zh-CN']['mejs.fullscreen-off'] = '关闭全屏'; + mejs.i18n['zh-CN']['mejs.fullscreen-on'] = '转向全屏'; + mejs.i18n['zh-CN']['mejs.download-video'] = '下载视频'; +} \ No newline at end of file diff --git a/src/js/plugins/context-menu/context-menu.css b/src/js/plugins/context-menu/context-menu.css new file mode 100644 index 000000000..9d3fcced9 --- /dev/null +++ b/src/js/plugins/context-menu/context-menu.css @@ -0,0 +1,34 @@ +.mejs__contextmenu, +.mejs-contextmenu { + background: #fff; + border: solid 1px #999; + border-radius: 4px; + left: 0; + padding: 10px; + position: absolute; + top: 0; + width: 150px; + z-index: 9999999999; /* make sure it shows on fullscreen */ +} + +.mejs__contextmenu-separator, +.mejs-contextmenu-separator { + background: #333; + font-size: 0; + height: 1px; + margin: 5px 6px; +} + +.mejs__contextmenu-item, +.mejs-contextmenu-item { + color: #333; + cursor: pointer; + font-size: 12px; + padding: 4px 6px; +} + +.mejs__contextmenu-item:hover, +.mejs-contextmenu-item:hover { + background: #2c7c91; + color: #fff; +} diff --git a/src/js/plugins/context-menu/context-menu.js b/src/js/plugins/context-menu/context-menu.js new file mode 100644 index 000000000..488e6cea5 --- /dev/null +++ b/src/js/plugins/context-menu/context-menu.js @@ -0,0 +1,222 @@ +'use strict'; + +// Translations (English required) +mejs.i18n.en['mejs.fullscreen-off'] = 'Turn off Fullscreen'; +mejs.i18n.en['mejs.fullscreen-on'] = 'Go Fullscreen'; +mejs.i18n.en['mejs.download-video'] = 'Download Video'; + +/* + * ContextMenu + * + */ +Object.assign(mejs.MepDefaults, { + contextMenuItems: [{ + // demo of a fullscreen option + render (player) { + + // check for fullscreen plugin + if (player.enterFullScreen === undefined) { + return null; + } + + if (player.isFullScreen) { + return mejs.i18n.t('mejs.fullscreen-off'); + } else { + return mejs.i18n.t('mejs.fullscreen-on'); + } + }, + click (player) { + if (player.isFullScreen) { + player.exitFullScreen(); + } else { + player.enterFullScreen(); + } + } + }, + // demo of a mute/unmute button + { + render (player) { + if (player.media.muted) { + return mejs.i18n.t('mejs.unmute'); + } else { + return mejs.i18n.t('mejs.mute'); + } + }, + click (player) { + if (player.media.muted) { + player.setMuted(false); + } else { + player.setMuted(true); + } + } + }, + // separator + { + isSeparator: true + }, + // demo of simple download video + { + render () { + return mejs.i18n.t('mejs.download-video'); + }, + click (player) { + window.location.href = player.media.currentSrc; + } + }] + } +); + + +Object.assign(MediaElementPlayer.prototype, { + + isContextMenuEnabled: true, + + contextMenuTimeout: null, + + buildcontextmenu (player) { + + if (!player.isVideo) { + return; + } + + // create context menu + if (!document.querySelector(`.${player.options.classPrefix}contextmenu`)) { + player.contextMenu = document.createElement('div'); + player.contextMenu.className = `${player.options.classPrefix}contextmenu`; + player.contextMenu.style.display = 'none'; + + document.body.appendChild(player.contextMenu); + } + + // create events for showing context menu + player.container.addEventListener('contextmenu', (e) => { + if (player.isContextMenuEnabled && (e.keyCode === 3 || e.which === 3)) { + player.renderContextMenu(e); + e.preventDefault(); + e.stopPropagation(); + } + }); + player.container.addEventListener('click', () => { + player.contextMenu.style.display = 'none'; + }); + player.contextMenu.addEventListener('mouseleave', () => { + player.startContextMenuTimer(); + + }); + }, + + cleancontextmenu (player) { + player.contextMenu.remove(); + }, + + enableContextMenu () { + this.isContextMenuEnabled = true; + }, + disableContextMenu () { + this.isContextMenuEnabled = false; + }, + + startContextMenuTimer () { + const t = this; + + t.killContextMenuTimer(); + + t.contextMenuTimer = setTimeout(() => { + t.hideContextMenu(); + t.killContextMenuTimer(); + }, 750); + }, + killContextMenuTimer () { + let timer = this.contextMenuTimer; + + if (timer !== null && timer !== undefined) { + clearTimeout(timer); + timer = null; + } + }, + + hideContextMenu () { + this.contextMenu.style.display = 'none'; + }, + + renderContextMenu (event) { + + // alway re-render the items so that things like "turn fullscreen on" and "turn fullscreen off" are always written correctly + let + t = this, + html = '', + items = t.options.contextMenuItems + ; + + for (let i = 0, total = items.length; i < total; i++) { + + const item = items[i]; + + if (item.isSeparator) { + html += `
`; + } else { + + const rendered = item.render(t); + + // render can return null if the item doesn't need to be used at the moment + if (rendered !== null && rendered !== undefined) { + html += `
${rendered}
`; + } + } + } + + + // position and show the context menu + t.contextMenu.innerHTML = html; + + const + width = t.contextMenu.offsetWidth, + height = t.contextMenu.offsetHeight, + x = event.pageX, + y = event.pageY, + doc = document.documentElement, + scrollLeft = (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0), + scrollTop = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0), + left = (x + width > window.innerWidth + scrollLeft) ? x - width : x, + top = (y + height > window.innerHeight + scrollTop) ? y - height : y + ; + + t.contextMenu.style.display = ''; + t.contextMenu.style.left = `${left}px`; + t.contextMenu.style.top = `${top}px`; + + // bind events + const contextItems = t.contextMenu.querySelectorAll(`.${t.options.classPrefix}contextmenu-item`); + for (let i = 0, total = contextItems.length; i < total; i++) { + + // which one is this? + const + menuItem = contextItems[i], + itemIndex = parseInt(menuItem.getAttribute('data-itemindex'), 10), + item = t.options.contextMenuItems[itemIndex] + ; + + // bind extra functionality? + if (typeof item.show !== 'undefined') { + item.show(menuItem, t); + } + + // bind click action + menuItem.addEventListener('click', () => { + // perform click action + if (typeof item.click !== 'undefined') { + item.click(t); + } + + // close + t.contextMenu.style.display = 'none'; + }); + } + + // stop the controls from hiding + setTimeout(() => { + t.killControlsTimer(); + }, 100); + + } +}); \ No newline at end of file diff --git a/src/js/plugins/loop/loop-i18n.js b/src/js/plugins/loop/loop-i18n.js new file mode 100644 index 000000000..2b63c8536 --- /dev/null +++ b/src/js/plugins/loop/loop-i18n.js @@ -0,0 +1,65 @@ +'use strict'; + +if (mejs.i18n.ca !== undefined) { + mejs.i18n.ca['mejs.loop'] = 'Commuta Bucle'; +} +if (mejs.i18n.cs !== undefined) { + mejs.i18n.cs['mejs.loop'] = 'Přepnout smyčku'; +} +if (mejs.i18n.de !== undefined) { + mejs.i18n.de['mejs.loop'] = 'Wiederholung (de-)aktivieren'; +} +if (mejs.i18n.es !== undefined) { + mejs.i18n.es['mejs.loop'] = 'Alternar Repetición'; +} +if (mejs.i18n.fa !== undefined) { + mejs.i18n.fa['mejs.loop'] = 'حلقه تعویض'; +} +if (mejs.i18n.fr !== undefined) { + mejs.i18n.fr['mejs.loop'] = 'Répéter'; +} +if (mejs.i18n.hr !== undefined) { + mejs.i18n.hr['mejs.loop'] = 'Uključi/isključi ponavljanje'; +} +if (mejs.i18n.hu !== undefined) { + mejs.i18n.hu['mejs.loop'] = 'Húzza át a kapcsolót'; +} +if (mejs.i18n.it !== undefined) { + mejs.i18n.it['mejs.loop'] = 'Passare il ciclo'; +} +if (mejs.i18n.ja !== undefined) { + mejs.i18n.ja['mejs.loop'] = 'トグルループ'; +} +if (mejs.i18n.ko !== undefined) { + mejs.i18n.ko['mejs.loop'] = '루프 토글'; +} +if (mejs.i18n.nl !== undefined) { + mejs.i18n.nl['mejs.loop'] = 'Schakellus'; +} +if (mejs.i18n.pl !== undefined) { + mejs.i18n.pl['mejs.loop'] = 'Zapętl'; +} +if (mejs.i18n.pt !== undefined) { + mejs.i18n.pt['mejs.loop'] = 'Loop alternativo'; +} +if (mejs.i18n.ro !== undefined) { + mejs.i18n.ro['mejs.loop'] = 'Comutați buclă'; +} +if (mejs.i18n.ru !== undefined) { + mejs.i18n.ru['mejs.loop'] = 'Зациклить воспроизведение'; +} +if (mejs.i18n.sk !== undefined) { + mejs.i18n.sk['mejs.loop'] = 'Prepínať slučku'; +} +if (mejs.i18n.sv !== undefined) { + mejs.i18n.sv['mejs.loop'] = 'Repetera'; +} +if (mejs.i18n.uk !== undefined) { + mejs.i18n.uk['mejs.loop'] = 'Повторювати'; +} +if (mejs.i18n.zh !== undefined) { + mejs.i18n.zh['mejs.loop'] = '切換循環'; +} +if (mejs.i18n['zh-CN'] !== undefined) { + mejs.i18n['zh-CN']['mejs.loop'] = '切换循环'; +} \ No newline at end of file diff --git a/src/js/plugins/loop/loop.css b/src/js/plugins/loop/loop.css new file mode 100644 index 000000000..2138664a6 --- /dev/null +++ b/src/js/plugins/loop/loop.css @@ -0,0 +1,13 @@ +.mejs__loop-button > button, +.mejs-loop-button > button { + background: url('loop.svg') no-repeat transparent; +} +.mejs__loop-off > button, +.mejs-loop-off > button { + background-position: -20px 1px; +} + +.mejs__loop-on > button, +.mejs-loop-on > button { + background-position: 0 1px; +} diff --git a/src/js/plugins/loop/loop.js b/src/js/plugins/loop/loop.js new file mode 100644 index 000000000..8f295587d --- /dev/null +++ b/src/js/plugins/loop/loop.js @@ -0,0 +1,54 @@ +'use strict'; + +/** + * Loop button + * + * This feature creates a loop button in the control bar to toggle its behavior. It will restart media once finished + * if activated + */ + +// Translations (English required) +mejs.i18n.en['mejs.loop'] = 'Toggle Loop'; + +// Feature configuration +Object.assign(mejs.MepDefaults, { + /** + * @type {?String} + */ + loopText: null +}); + +Object.assign(MediaElementPlayer.prototype, { + /** + * Feature constructor. + * + * Always has to be prefixed with `build` and the name that will be used in MepDefaults.features list + * @param {MediaElementPlayer} player + */ + buildloop (player) { + const + t = this, + loopTitle = mejs.Utils.isString(t.options.loopText) ? t.options.loopText : mejs.i18n.t('mejs.loop'), + loop = document.createElement('div') + ; + + loop.className = `${t.options.classPrefix}button ${t.options.classPrefix}loop-button ${((player.options.loop) ? `${t.options.classPrefix}loop-on` : `${t.options.classPrefix}loop-off`)}`; + loop.innerHTML = ``; + + t.addControlElement(loop, 'loop'); + + // add a click toggle event + loop.addEventListener('click', () => { + player.options.loop = !player.options.loop; + if (player.options.loop) { + mejs.Utils.removeClass(loop, `${t.options.classPrefix}loop-off`); + mejs.Utils.addClass(loop, `${t.options.classPrefix}loop-on`); + } else { + mejs.Utils.removeClass(loop, `${t.options.classPrefix}loop-on`); + mejs.Utils.addClass(loop, `${t.options.classPrefix}loop-off`); + } + }); + } +}); + + diff --git a/src/js/plugins/loop/loop.svg b/src/js/plugins/loop/loop.svg new file mode 100644 index 000000000..45ea4e9e1 --- /dev/null +++ b/src/js/plugins/loop/loop.svg @@ -0,0 +1 @@ +4 \ No newline at end of file diff --git a/src/js/plugins/preview/preview.js b/src/js/plugins/preview/preview.js new file mode 100644 index 000000000..ea3db2b1d --- /dev/null +++ b/src/js/plugins/preview/preview.js @@ -0,0 +1,250 @@ +'use strict'; + +/** + * Preview feature + * + * This feature allows to create a preview effect on videos (playing on hover and with possibility of mute/fade-in/fade-out audio) + */ + + +// Feature configuration +Object.assign(mejs.MepDefaults, { + /** + * Media starts playing when users mouse hovers on it, and resets when leaving player area + * @type {Boolean} + */ + previewMode: false, + /** + * When playing on preview mode, turn on/off audio completely + * @type {Boolean} + */ + muteOnPreviewMode: true, + /** + * If fade in set in, time when it starts fading in + * @type {Number} + */ + fadeInAudioStart: 0, + /** + * When playing media, time interval to fade in audio (must be greater than zero) + * @type {Number} + */ + fadeInAudioInterval: 0, + /** + * If fade out set in, time when it starts fading out + * @type {Number} + */ + fadeOutAudioStart: 0, + /** + * When playing media, time interval to fade out audio (must be greater than zero) + * @type {Number} + */ + fadeOutAudioInterval: 0, + /** + * Percentage in decimals in which media will fade in/out (i.e., 0.02 = 2%) + * @type {Number} + */ + fadePercent: 0.02, + /** + * Whether reset or not the media + * @type {Boolean} + */ + pauseOnlyOnPreview: false, + /** + * Delay in milliseconds to start previewing media + * @type {Number} + */ + delayPreview: 0 +}); + +Object.assign(MediaElementPlayer.prototype, { + + /** + * Feature constructor. + * + * Always has to be prefixed with `build` and the name that will be used in MepDefaults.features list + * @param {MediaElementPlayer} player + */ + buildpreview (player) { + let + initFadeIn = false, + initFadeOut = false, + timeout, + mouseOver = false + ; + + const + t = this, + fadeInCallback = () => { + if (t.options.fadeInAudioInterval) { + + if (Math.floor(t.media.currentTime) < t.options.fadeIntAudioStart) { + t.media.setVolume(0); + t.media.setMuted(true); + } + + if (Math.floor(t.media.currentTime) === t.options.fadeInAudioStart) { + + initFadeIn = true; + + let + volume = 0, + audioInterval = t.options.fadeInAudioInterval, + interval = setInterval(() => { + + // Increase volume by step as long as it is below 1 + if (volume < 1) { + volume += t.options.fadePercent; + if (volume > 1) { + volume = 1; + } + + // limit to 2 decimal places + t.media.setVolume(volume.toFixed(2)); + + } else { + // Stop firing interval when 1 is reached + clearInterval(interval); + interval = null; + t.media.setMuted(false); + setTimeout(() => { + initFadeIn = false; + }, 300); + } + + }, audioInterval) + ; + } + } + }, + fadeOutCallback = () => { + if (t.options.fadeOutAudioInterval) { + + if (Math.floor(t.media.currentTime) < t.options.fadeOutAudioStart) { + t.media.setVolume(1); + t.media.setMuted(false); + } + + if (Math.floor(t.media.currentTime) === t.options.fadeOutAudioStart) { + + initFadeOut = true; + + let + volume = 1, + audioInterval = t.options.fadeOutAudioInterval, + interval = setInterval(() => { + + // Increase volume by step as long as it is above 0 + + if (volume > 0) { + volume -= t.options.fadePercent; + if (volume < 0) { + volume = 0; + } + + // limit to 2 decimal places + t.media.setVolume(volume.toFixed(2)); + + } else { + // Stop firing interval when 0 is reached + clearInterval(interval); + interval = null; + t.media.setMuted(false); + setTimeout(() => { + initFadeOut = false; + }, 300); + } + }, audioInterval) + ; + } + } + } + ; + + if (t.options.muteOnPreviewMode || t.options.fadeInAudioInterval) { + t.media.setVolume(0); + t.media.setMuted(true); + } else if (t.options.fadeOutAudioInterval) { + t.media.setVolume(1); + t.media.setMuted(false); + } + + // fade-in/out should be available for both video/audio + t.media.addEventListener('timeupdate', () => { + + if (initFadeIn) { + t.media.removeEventListener('timeupdate', fadeInCallback); + return; + } + + if (initFadeOut) { + t.media.removeEventListener('timeupdate', fadeOutCallback); + return; + } + + fadeInCallback(); + fadeOutCallback(); + }); + + // preview is only for video + if (!player.isVideo) { + return; + } + + // show/hide controls + document.body.addEventListener('mouseover', (e) => { + + if (e.target === t.container || e.target.closest(`.${t.options.classPrefix}container`)) { + mouseOver = true; + t.container.querySelector(`.${t.options.classPrefix}overlay-loading`).parentNode.style.display = 'flex'; + t.container.querySelector(`.${t.options.classPrefix}overlay-play`).style.display = 'none'; + if (t.media.paused) { + timeout = setTimeout(() => { + if (mouseOver) { + t.media.play(); + } else { + clearTimeout(timeout); + timeout = null; + } + t.container.querySelector(`.${t.options.classPrefix}overlay-loading`).parentNode.style.display = 'none'; + + }, t.options.delayPreview); + } else { + t.container.querySelector(`.${t.options.classPrefix}overlay-loading`).parentNode.style.display = 'none'; + } + } else { + mouseOver = false; + clearTimeout(timeout); + timeout = null; + if (!t.media.paused) { + t.media.pause(); + } + t.container.querySelector(`.${t.options.classPrefix}overlay-loading`).parentNode.style.display = 'none'; + } + + }); + document.body.addEventListener('mouseout', (e) => { + if (!(e.target === t.container) && !(e.target.closest(`.${t.options.classPrefix}container`))) { + mouseOver = false; + t.container.querySelector(`.${t.options.classPrefix}overlay-loading`).parentNode.style.display = 'none'; + if (!t.media.paused) { + t.media.pause(); + + if (!t.options.pauseOnlyOnPreview) { + t.media.setCurrentTime(0); + } + } + + clearTimeout(timeout); + timeout = null; + } + }); + + window.addEventListener('scroll', () => { + mouseOver = false; + t.container.querySelector(`.${t.options.classPrefix}overlay-loading`).parentNode.style.display = 'none'; + if (!t.media.paused) { + t.media.pause(); + } + }); + } +}); \ No newline at end of file diff --git a/src/js/plugins/speed/speed-i18n.js b/src/js/plugins/speed/speed-i18n.js new file mode 100644 index 000000000..9e2167d65 --- /dev/null +++ b/src/js/plugins/speed/speed-i18n.js @@ -0,0 +1,65 @@ +'use strict'; + +if (mejs.i18n.ca !== undefined) { + mejs.i18n.ca['mejs.speed-rate'] = 'Velocitat'; +} +if (mejs.i18n.cs !== undefined) { + mejs.i18n.cs['mejs.speed-rate'] = 'Rychlost'; +} +if (mejs.i18n.de !== undefined) { + mejs.i18n.de['mejs.speed-rate'] = 'Geschwindigkeitsrate'; +} +if (mejs.i18n.es !== undefined) { + mejs.i18n.es['mejs.speed-rate'] = 'Velocidad'; +} +if (mejs.i18n.fa !== undefined) { + mejs.i18n.fa['mejs.speed-rate'] = 'نرخ سرعت'; +} +if (mejs.i18n.fr !== undefined) { + mejs.i18n.fr['mejs.speed-rate'] = 'Vitesse'; +} +if (mejs.i18n.hr !== undefined) { + mejs.i18n.hr['mejs.speed-rate'] = 'Brzina reprodukcije'; +} +if (mejs.i18n.hu !== undefined) { + mejs.i18n.hu['mejs.speed-rate'] = 'Sebesség'; +} +if (mejs.i18n.it !== undefined) { + mejs.i18n.it['mejs.speed-rate'] = 'Velocità'; +} +if (mejs.i18n.ja !== undefined) { + mejs.i18n.ja['mejs.speed-rate'] = '高速'; +} +if (mejs.i18n.ko !== undefined) { + mejs.i18n.ko['mejs.speed-rate'] = '속도 속도'; +} +if (mejs.i18n.nl !== undefined) { + mejs.i18n.nl['mejs.speed-rate'] = 'Snelheidsgraad'; +} +if (mejs.i18n.pl !== undefined) { + mejs.i18n.pl['mejs.speed-rate'] = 'Prędkość'; +} +if (mejs.i18n.pt !== undefined) { + mejs.i18n.pt['mejs.speed-rate'] = 'Taxa de velocidade'; +} +if (mejs.i18n.ro !== undefined) { + mejs.i18n.ro['mejs.speed-rate'] = 'Viteză de viteză'; +} +if (mejs.i18n.ru !== undefined) { + mejs.i18n.ru['mejs.speed-rate'] = 'Скорость воспроизведения'; +} +if (mejs.i18n.sk !== undefined) { + mejs.i18n.sk['mejs.speed-rate'] = 'Rýchlosť'; +} +if (mejs.i18n.sv !== undefined) { + mejs.i18n.sv['mejs.speed-rate'] = 'Hastighet'; +} +if (mejs.i18n.uk !== undefined) { + mejs.i18n.uk['mejs.speed-rate'] = 'Швидкість відтворення'; +} +if (mejs.i18n.zh !== undefined) { + mejs.i18n.zh['mejs.speed-rate'] = '速度'; +} +if (mejs.i18n['zh-CN'] !== undefined) { + mejs.i18n['zh-CN']['mejs.speed-rate'] = '速度'; +} \ No newline at end of file diff --git a/src/js/plugins/speed/speed.css b/src/js/plugins/speed/speed.css new file mode 100644 index 000000000..76579a889 --- /dev/null +++ b/src/js/plugins/speed/speed.css @@ -0,0 +1,94 @@ +.mejs__speed-button, +.mejs-speed-button { + position: relative; +} + +.mejs__speed-button > button, +.mejs-speed-button > button { + background: transparent; + color: #fff; + font-size: 11px; + line-height: normal; + margin: 11px 0 0; + width: 36px; +} + +.mejs__speed-selector, +.mejs-speed-selector { + background: rgba(50, 50, 50, 0.7); + border: solid 1px transparent; + border-radius: 0; + height: 150px; + left: -10px; + overflow: hidden; + padding: 0; + position: absolute; + top: -100px; + visibility: hidden; + width: 60px; +} + +.mejs__speed-selector, +.mejs-speed-selector { + visibility: visible; +} + +.mejs__speed-selector-list, +.mejs-speed-selector-list { + display: block; + list-style-type: none !important; + margin: 0; + overflow: hidden; + padding: 0; +} + +.mejs__speed-selector-list-item, +.mejs-speed-selector-list-item { + color: #fff; + display: block; + list-style-type: none !important; + margin: 0 0 6px; + overflow: hidden; + padding: 0 10px; +} + +.mejs__speed-selector-list-item:hover, +.mejs-speed-selector-list-item:hover { + background-color: rgb(200, 200, 200) !important; + background-color: rgba(255, 255, 255, 0.4) !important; +} + +.mejs__speed-selector-input, +.mejs-speed-selector-input { + clear: both; + float: left; + left: -1000px; + margin: 3px 3px 0 5px; + position: absolute; +} + +.mejs__speed-selector-label, +.mejs-speed-selector-label { + color: white; + cursor: pointer; + float: left; + font-size: 11px; + line-height: 15px; + margin-left: 5px; + padding: 4px 0 0; + width: 60px; +} + +.mejs__speed-selected, +.mejs-speed-selected { + color: rgba(33, 248, 248, 1); +} + +.mejs__speed-selector, +.mejs-speed-selector { + visibility: hidden; +} +.mejs__speed-button:hover .mejs__speed-selector, +.mejs-speed-button:hover .mejs-speed-selector { + visibility: visible; +} diff --git a/src/js/plugins/speed/speed.js b/src/js/plugins/speed/speed.js new file mode 100644 index 000000000..09658f9c4 --- /dev/null +++ b/src/js/plugins/speed/speed.js @@ -0,0 +1,221 @@ +'use strict'; + +/** + * Speed button + * + * This feature creates a button to speed media in different levels. + */ + +// Translations (English required) +mejs.i18n.en['mejs.speed-rate'] = 'Speed Rate'; + +// Feature configuration +Object.assign(mejs.MepDefaults, { + /** + * The speeds media can be accelerated + * + * Supports an array of float values or objects with format + * [{name: 'Slow', value: '0.75'}, {name: 'Normal', value: '1.00'}, ...] + * @type {{String[]|Object[]}} + */ + speeds: ['2.00', '1.50', '1.25', '1.00', '0.75'], + /** + * @type {String} + */ + defaultSpeed: '1.00', + /** + * @type {String} + */ + speedChar: 'x', + /** + * @type {?String} + */ + speedText: null +}); + +Object.assign(MediaElementPlayer.prototype, { + + /** + * Feature constructor. + * + * Always has to be prefixed with `build` and the name that will be used in MepDefaults.features list + * @param {MediaElementPlayer} player + * @param {HTMLElement} controls + * @param {HTMLElement} layers + * @param {HTMLElement} media + */ + buildspeed (player, controls, layers, media) { + const + t = this, + isNative = t.media.rendererName !== null && /(native|html5)/i.test(t.media.rendererName) + ; + + if (!isNative) { + return; + } + + const + speeds = [], + speedTitle = mejs.Utils.isString(t.options.speedText) ? t.options.speedText : mejs.i18n.t('mejs.speed-rate'), + getSpeedNameFromValue = (value) => { + for (let i = 0, total = speeds.length; i < total; i++) { + if (speeds[i].value === value) { + return speeds[i].name; + } + } + } + ; + + let + playbackSpeed, + defaultInArray = false + ; + + for (let i = 0, total = t.options.speeds.length; i < total; i++) { + const s = t.options.speeds[i]; + + if (typeof s === 'string') { + speeds.push({ + name: `${s}${t.options.speedChar}`, + value: s + }); + + if (s === t.options.defaultSpeed) { + defaultInArray = true; + } + } + else { + speeds.push(s); + if (s.value === t.options.defaultSpeed) { + defaultInArray = true; + } + } + } + + if (!defaultInArray) { + speeds.push({ + name: t.options.defaultSpeed + t.options.speedChar, + value: t.options.defaultSpeed + }); + } + + speeds.sort((a, b) => { + return parseFloat(b.value) - parseFloat(a.value); + }); + + t.cleanspeed(player); + + player.speedButton = document.createElement('div'); + player.speedButton.className = `${t.options.classPrefix}button ${t.options.classPrefix}speed-button`; + player.speedButton.innerHTML = `` + + `
` + + `` + + `
`; + + t.addControlElement(player.speedButton, 'speed'); + + for (let i = 0, total = speeds.length; i < total; i++) { + + const inputId = `${t.id}-speed-${speeds[i].value}`; + + player.speedButton.querySelector('ul').innerHTML += `
  • ` + + `` + + `` + + `
  • `; + } + + playbackSpeed = t.options.defaultSpeed; + + player.speedSelector = player.speedButton.querySelector(`.${t.options.classPrefix}speed-selector`); + + const + inEvents = ['mouseenter', 'focusin'], + outEvents = ['mouseleave', 'focusout'], + // Enable inputs after they have been appended to controls to avoid tab and up/down arrow focus issues + radios = player.speedButton.querySelectorAll('input[type="radio"]'), + labels = player.speedButton.querySelectorAll(`.${t.options.classPrefix}speed-selector-label`) + ; + + // hover or keyboard focus + for (let i = 0, total = inEvents.length; i < total; i++) { + player.speedButton.addEventListener(inEvents[i], () => { + mejs.Utils.removeClass(player.speedSelector, `${t.options.classPrefix}offscreen`); + player.speedSelector.style.height = player.speedSelector.querySelector('ul').offsetHeight; + player.speedSelector.style.top = `${(-1 * parseFloat(player.speedSelector.offsetHeight))}px`; + }); + } + + for (let i = 0, total = outEvents.length; i < total; i++) { + player.speedSelector.addEventListener(outEvents[i], function () { + mejs.Utils.addClass(this, `${t.options.classPrefix}offscreen`); + }); + } + + for (let i = 0, total = radios.length; i < total; i++) { + const radio = radios[i]; + radio.disabled = false; + radio.addEventListener('click', function() { + const + self = this, + newSpeed = self.value + ; + + playbackSpeed = newSpeed; + media.playbackRate = parseFloat(newSpeed); + player.speedButton.querySelector('button').innerHTML = (getSpeedNameFromValue(newSpeed)); + const selected = player.speedButton.querySelectorAll(`.${t.options.classPrefix}speed-selected`); + for (let i = 0, total = selected.length; i < total; i++) { + mejs.Utils.removeClass(selected[i], `${t.options.classPrefix}speed-selected`); + } + + self.checked = true; + const siblings = mejs.Utils.siblings(self, (el) => mejs.Utils.hasClass(el, `${t.options.classPrefix}speed-selector-label`)); + for (let j = 0, total = siblings.length; j < total; j++) { + mejs.Utils.addClass(siblings[j], `${t.options.classPrefix}speed-selected`); + } + }); + } + + for (let i = 0, total = labels.length; i < total; i++) { + labels[i].addEventListener('click', function () { + const + radio = mejs.Utils.siblings(this, (el) => el.tagName === 'INPUT')[0], + event = mejs.Utils.createEvent('click', radio) + ; + radio.dispatchEvent(event); + }); + } + + //Allow up/down arrow to change the selected radio without changing the volume. + player.speedSelector.addEventListener('keydown', (e) => { + e.stopPropagation(); + }); + + media.addEventListener('loadedmetadata', () => { + if (playbackSpeed) { + media.playbackRate = parseFloat(playbackSpeed); + } + }); + }, + /** + * Feature destructor. + * + * Always has to be prefixed with `clean` and the name that was used in MepDefaults.features list + * @param {MediaElementPlayer} player + */ + cleanspeed (player) { + if (player) { + if (player.speedButton) { + player.speedButton.parentNode.removeChild(player.speedButton); + } + if (player.speedSelector) { + player.speedSelector.parentNode.removeChild(player.speedSelector); + } + } + } +}); \ No newline at end of file diff --git a/src/js/templates.js b/src/js/templates.js index 5d347e827..a28f7fdfb 100644 --- a/src/js/templates.js +++ b/src/js/templates.js @@ -538,6 +538,7 @@ let playerView = new Vue({ firstLoad: true, publishedDate: '', videoUrl: '', + videoDash: '', videoId: '', channelId: '', channelIcon: '', diff --git a/src/js/videojs-dash.js b/src/js/videojs-dash.js new file mode 100644 index 000000000..bea3d25dd --- /dev/null +++ b/src/js/videojs-dash.js @@ -0,0 +1,708 @@ +/*! @name videojs-contrib-dash @version 2.11.0 @license Apache-2.0 */ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('dashjs'), require('video.js'), require('global/window'), require('global/document')) : + typeof define === 'function' && define.amd ? define(['dashjs', 'video.js', 'global/window', 'global/document'], factory) : + (global.videojsDash = factory(global.dashjs,global.videojs,global.window,global.document)); +}(this, (function (dashjs,videojs,window,document) { 'use strict'; + + dashjs = dashjs && dashjs.hasOwnProperty('default') ? dashjs['default'] : dashjs; + videojs = videojs && videojs.hasOwnProperty('default') ? videojs['default'] : videojs; + window = window && window.hasOwnProperty('default') ? window['default'] : window; + document = document && document.hasOwnProperty('default') ? document['default'] : document; + + /** + * Setup audio tracks. Take the tracks from dash and add the tracks to videojs. Listen for when + * videojs changes tracks and apply that to the dash player because videojs doesn't do this + * natively. + * + * @private + * @param {videojs} player the videojs player instance + * @param {videojs.tech} tech the videojs tech being used + */ + + function handlePlaybackMetadataLoaded(player, tech) { + var mediaPlayer = player.dash.mediaPlayer; + var dashAudioTracks = mediaPlayer.getTracksFor('audio'); + var videojsAudioTracks = player.audioTracks(); + + function generateIdFromTrackIndex(index) { + return "dash-audio-" + index; + } + + function findDashAudioTrack(subDashAudioTracks, videojsAudioTrack) { + return subDashAudioTracks.find(function (_ref) { + var index = _ref.index; + return generateIdFromTrackIndex(index) === videojsAudioTrack.id; + }); + } // Safari creates a single native `AudioTrack` (not `videojs.AudioTrack`) when loading. Clear all + // automatically generated audio tracks so we can create them all ourself. + + + if (videojsAudioTracks.length) { + tech.clearTracks(['audio']); + } + + var currentAudioTrack = mediaPlayer.getCurrentTrackFor('audio'); + dashAudioTracks.forEach(function (dashTrack) { + var localizedLabel; + + if (Array.isArray(dashTrack.labels)) { + for (var i = 0; i < dashTrack.labels.length; i++) { + if (dashTrack.labels[i].lang && player.language().indexOf(dashTrack.labels[i].lang.toLowerCase()) !== -1) { + localizedLabel = dashTrack.labels[i]; + break; + } + } + } + + var label; + + if (localizedLabel) { + label = localizedLabel.text; + } else if (Array.isArray(dashTrack.labels) && dashTrack.labels.length === 1) { + label = dashTrack.labels[0].text; + } else { + label = dashTrack.lang; + + if (dashTrack.roles && dashTrack.roles.length) { + label += ' (' + dashTrack.roles.join(', ') + ')'; + } + } // Add the track to the player's audio track list. + + + videojsAudioTracks.addTrack(new videojs.AudioTrack({ + enabled: dashTrack === currentAudioTrack, + id: generateIdFromTrackIndex(dashTrack.index), + kind: dashTrack.kind || 'main', + label: label, + language: dashTrack.lang + })); + }); + + var audioTracksChangeHandler = function audioTracksChangeHandler() { + for (var i = 0; i < videojsAudioTracks.length; i++) { + var track = videojsAudioTracks[i]; + + if (track.enabled) { + // Find the audio track we just selected by the id + var dashAudioTrack = findDashAudioTrack(dashAudioTracks, track); // Set is as the current track + + mediaPlayer.setCurrentTrack(dashAudioTrack); // Stop looping + + continue; + } + } + }; + + videojsAudioTracks.addEventListener('change', audioTracksChangeHandler); + player.dash.mediaPlayer.on(dashjs.MediaPlayer.events.STREAM_TEARDOWN_COMPLETE, function () { + videojsAudioTracks.removeEventListener('change', audioTracksChangeHandler); + }); + } + /* + * Call `handlePlaybackMetadataLoaded` when `mediaPlayer` emits + * `dashjs.MediaPlayer.events.PLAYBACK_METADATA_LOADED`. + */ + + + function setupAudioTracks(player, tech) { + // When `dashjs` finishes loading metadata, create audio tracks for `video.js`. + player.dash.mediaPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_METADATA_LOADED, handlePlaybackMetadataLoaded.bind(null, player, tech)); + } + + function find(l, f) { + for (var i = 0; i < l.length; i++) { + if (f(l[i])) { + return l[i]; + } + } + } + /* + * Attach text tracks from dash.js to videojs + * + * @param {videojs} player the videojs player instance + * @param {array} tracks the tracks loaded by dash.js to attach to videojs + * + * @private + */ + + + function attachDashTextTracksToVideojs(player, tech, tracks) { + var trackDictionary = []; // Add remote tracks + + var tracksAttached = tracks // Map input data to match HTMLTrackElement spec + // https://developer.mozilla.org/en-US/docs/Web/API/HTMLTrackElement + .map(function (track) { + var localizedLabel; + + if (Array.isArray(track.labels)) { + for (var i = 0; i < track.labels.length; i++) { + if (track.labels[i].lang && player.language().indexOf(track.labels[i].lang.toLowerCase()) !== -1) { + localizedLabel = track.labels[i]; + break; + } + } + } + + var label; + + if (localizedLabel) { + label = localizedLabel.text; + } else if (Array.isArray(track.labels) && track.labels.length === 1) { + label = track.labels[0].text; + } else { + label = track.lang || track.label; + } + + return { + dashTrack: track, + trackConfig: { + label: label, + language: track.lang, + srclang: track.lang, + kind: track.kind + } + }; + }) // Add track to videojs track list + .map(function (_ref) { + var trackConfig = _ref.trackConfig, + dashTrack = _ref.dashTrack; + var remoteTextTrack = player.addRemoteTextTrack(trackConfig, false); + trackDictionary.push({ + textTrack: remoteTextTrack.track, + dashTrack: dashTrack + }); // Don't add the cues becuase we're going to let dash handle it natively. This will ensure + // that dash handle external time text files and fragmented text tracks. + // + // Example file with external time text files: + // https://storage.googleapis.com/shaka-demo-assets/sintel-mp4-wvtt/dash.mpd + + return remoteTextTrack; + }); + /* + * Scan `videojs.textTracks()` to find one that is showing. Set the dash text track. + */ + + function updateActiveDashTextTrack() { + var dashMediaPlayer = player.dash.mediaPlayer; + var textTracks = player.textTracks(); + var activeTextTrackIndex = -1; // Iterate through the tracks and find the one marked as showing. If none are showing, + // `activeTextTrackIndex` will be set to `-1`, disabling text tracks. + + var _loop = function _loop(i) { + var textTrack = textTracks[i]; + + if (textTrack.mode === 'showing') { + // Find the dash track we want to use + + /* jshint loopfunc: true */ + var dictionaryLookupResult = find(trackDictionary, function (track) { + return track.textTrack === textTrack; + }); + /* jshint loopfunc: false */ + + var dashTrackToActivate = dictionaryLookupResult ? dictionaryLookupResult.dashTrack : null; // If we found a track, get it's index. + + if (dashTrackToActivate) { + activeTextTrackIndex = tracks.indexOf(dashTrackToActivate); + } + } + }; + + for (var i = 0; i < textTracks.length; i += 1) { + _loop(i); + } // If the text track has changed, then set it in dash + + + if (activeTextTrackIndex !== dashMediaPlayer.getCurrentTextTrackIndex()) { + dashMediaPlayer.setTextTrack(activeTextTrackIndex); + } + } // Update dash when videojs's selected text track changes. + + + player.textTracks().on('change', updateActiveDashTextTrack); // Cleanup event listeners whenever we start loading a new source + + player.dash.mediaPlayer.on(dashjs.MediaPlayer.events.STREAM_TEARDOWN_COMPLETE, function () { + player.textTracks().off('change', updateActiveDashTextTrack); + }); // Initialize the text track on our first run-through + + updateActiveDashTextTrack(); + return tracksAttached; + } + /* + * Wait for dash to emit `TEXT_TRACKS_ADDED` and then attach the text tracks loaded by dash if + * we're not using native text tracks. + * + * @param {videojs} player the videojs player instance + * @private + */ + + + function setupTextTracks(player, tech, options) { + // Clear VTTCue if it was shimmed by vttjs and let dash.js use TextTrackCue. + // This is necessary because dash.js creates text tracks + // using addTextTrack which is incompatible with vttjs.VTTCue in IE11 + if (window.VTTCue && !/\[native code\]/.test(window.VTTCue.toString())) { + window.VTTCue = false; + } // Store the tracks that we've added so we can remove them later. + + + var dashTracksAttachedToVideoJs = []; // We're relying on the user to disable native captions. Show an error if they didn't do so. + + if (tech.featuresNativeTextTracks) { + videojs.log.error('You must pass {html: {nativeCaptions: false}} in the videojs constructor ' + 'to use text tracks in videojs-contrib-dash'); + return; + } + + var mediaPlayer = player.dash.mediaPlayer; // Clear the tracks that we added. We don't clear them all because someone else can add tracks. + + function clearDashTracks() { + dashTracksAttachedToVideoJs.forEach(player.removeRemoteTextTrack.bind(player)); + dashTracksAttachedToVideoJs = []; + } + + function handleTextTracksAdded(_ref2) { + var index = _ref2.index, + tracks = _ref2.tracks; + // Stop listening for this event. We only want to hear it once. + mediaPlayer.off(dashjs.MediaPlayer.events.TEXT_TRACKS_ADDED, handleTextTracksAdded); // Cleanup old tracks + + clearDashTracks(); + + if (!tracks.length) { + // Don't try to add text tracks if there aren't any + return; + } // Save the tracks so we can remove them later + + + dashTracksAttachedToVideoJs = attachDashTextTracksToVideojs(player, tech, tracks, options); + } // Attach dash text tracks whenever we dash emits `TEXT_TRACKS_ADDED`. + + + mediaPlayer.on(dashjs.MediaPlayer.events.TEXT_TRACKS_ADDED, handleTextTracksAdded); // When the player can play, remove the initialization events. We might not have received + // TEXT_TRACKS_ADDED` so we have to stop listening for it or we'll get errors when we load new + // videos and are listening for the same event in multiple places, including cleaned up + // mediaPlayers. + + mediaPlayer.on(dashjs.MediaPlayer.events.CAN_PLAY, function () { + mediaPlayer.off(dashjs.MediaPlayer.events.TEXT_TRACKS_ADDED, handleTextTracksAdded); + }); + } + + /** + * videojs-contrib-dash + * + * Use Dash.js to playback DASH content inside of Video.js via a SourceHandler + */ + + var Html5DashJS = + /*#__PURE__*/ + function () { + function Html5DashJS(source, tech, options) { + var _this = this; + + // Get options from tech if not provided for backwards compatibility + options = options || tech.options_; + this.player = videojs(options.playerId); + this.player.dash = this.player.dash || {}; + this.tech_ = tech; + this.el_ = tech.el(); + this.elParent_ = this.el_.parentNode; + this.hasFiniteDuration_ = false; // Do nothing if the src is falsey + + if (!source.src) { + return; + } // While the manifest is loading and Dash.js has not finished initializing + // we must defer events and functions calls with isReady_ and then `triggerReady` + // again later once everything is setup + + + tech.isReady_ = false; + + if (Html5DashJS.updateSourceData) { + videojs.log.warn('updateSourceData has been deprecated.' + ' Please switch to using hook("updatesource", callback).'); + source = Html5DashJS.updateSourceData(source); + } // call updatesource hooks + + + Html5DashJS.hooks('updatesource').forEach(function (hook) { + source = hook(source); + }); + var manifestSource = source.src; + this.keySystemOptions_ = Html5DashJS.buildDashJSProtData(source.keySystemOptions); + this.player.dash.mediaPlayer = dashjs.MediaPlayer().create(); + this.mediaPlayer_ = this.player.dash.mediaPlayer; // For whatever reason, we need to call setTextDefaultEnabled(false) to get + // VTT captions to show, even though we're doing virtually the same thing + // in setup-text-tracks.js + + this.mediaPlayer_.setTextDefaultEnabled(false); // Log MedaPlayer messages through video.js + + if (Html5DashJS.useVideoJSDebug) { + videojs.log.warn('useVideoJSDebug has been deprecated.' + ' Please switch to using hook("beforeinitialize", callback).'); + Html5DashJS.useVideoJSDebug(this.mediaPlayer_); + } + + if (Html5DashJS.beforeInitialize) { + videojs.log.warn('beforeInitialize has been deprecated.' + ' Please switch to using hook("beforeinitialize", callback).'); + Html5DashJS.beforeInitialize(this.player, this.mediaPlayer_); + } + + Html5DashJS.hooks('beforeinitialize').forEach(function (hook) { + hook(_this.player, _this.mediaPlayer_); + }); // Must run controller before these two lines or else there is no + // element to bind to. + + this.mediaPlayer_.initialize(); // Retrigger a dash.js-specific error event as a player error + // See src/streaming/utils/ErrorHandler.js in dash.js code + // Handled with error (playback is stopped): + // - capabilityError + // - downloadError + // - manifestError + // - mediaSourceError + // - mediaKeySessionError + // Not handled: + // - timedTextError (video can still play) + // - mediaKeyMessageError (only fires under 'might not work' circumstances) + + this.retriggerError_ = function (event) { + if (event.error === 'capability' && event.event === 'mediasource') { + // No support for MSE + _this.player.error({ + code: 4, + message: 'The media cannot be played because it requires a feature ' + 'that your browser does not support.' + }); + } else if (event.error === 'manifestError' && ( // Manifest type not supported + event.event.id === 'createParser' || // Codec(s) not supported + event.event.id === 'codec' || // No streams available to stream + event.event.id === 'nostreams' || // Error creating Stream object + event.event.id === 'nostreamscomposed' || // syntax error parsing the manifest + event.event.id === 'parse' || // a stream has multiplexed audio+video + event.event.id === 'multiplexedrep')) { + // These errors have useful error messages, so we forward it on + _this.player.error({ + code: 4, + message: event.event.message + }); + } else if (event.error === 'mediasource') { + // This error happens when dash.js fails to allocate a SourceBuffer + // OR the underlying video element throws a `MediaError`. + // If it's a buffer allocation fail, the message states which buffer + // (audio/video/text) failed allocation. + // If it's a `MediaError`, dash.js inspects the error object for + // additional information to append to the error type. + if (event.event.match('MEDIA_ERR_ABORTED')) { + _this.player.error({ + code: 1, + message: event.event + }); + } else if (event.event.match('MEDIA_ERR_NETWORK')) { + _this.player.error({ + code: 2, + message: event.event + }); + } else if (event.event.match('MEDIA_ERR_DECODE')) { + _this.player.error({ + code: 3, + message: event.event + }); + } else if (event.event.match('MEDIA_ERR_SRC_NOT_SUPPORTED')) { + _this.player.error({ + code: 4, + message: event.event + }); + } else if (event.event.match('MEDIA_ERR_ENCRYPTED')) { + _this.player.error({ + code: 5, + message: event.event + }); + } else if (event.event.match('UNKNOWN')) { + // We shouldn't ever end up here, since this would mean a + // `MediaError` thrown by the video element that doesn't comply + // with the W3C spec. But, since we should handle the error, + // throwing a MEDIA_ERR_SRC_NOT_SUPPORTED is probably the + // most reasonable thing to do. + _this.player.error({ + code: 4, + message: event.event + }); + } else { + // Buffer allocation error + _this.player.error({ + code: 4, + message: event.event + }); + } + } else if (event.error === 'capability' && event.event === 'encryptedmedia') { + // Browser doesn't support EME + _this.player.error({ + code: 5, + message: 'The media cannot be played because it requires encryption ' + 'features that your browser does not support.' + }); + } else if (event.error === 'key_session') { + // This block handles pretty much all errors thrown by the + // encryption subsystem + _this.player.error({ + code: 5, + message: event.event + }); + } else if (event.error === 'download') { + _this.player.error({ + code: 2, + message: 'The media playback was aborted because too many consecutive ' + 'download errors occurred.' + }); + } else if (event.error === 'mssError') { + _this.player.error({ + code: 3, + message: event.event + }); + } else { + // ignore the error + return; + } // only reset the dash player in 10ms async, so that the rest of the + // calling function finishes + + + setTimeout(function () { + _this.mediaPlayer_.reset(); + }, 10); + }; + + this.mediaPlayer_.on(dashjs.MediaPlayer.events.ERROR, this.retriggerError_); + + this.getDuration_ = function (event) { + var periods = event.data.Period_asArray; + var oldHasFiniteDuration = _this.hasFiniteDuration_; + + if (event.data.mediaPresentationDuration || periods[periods.length - 1].duration) { + _this.hasFiniteDuration_ = true; + } else { + // in case we run into a weird situation where we're VOD but then + // switch to live + _this.hasFiniteDuration_ = false; + } + + if (_this.hasFiniteDuration_ !== oldHasFiniteDuration) { + _this.player.trigger('durationchange'); + } + }; + + this.mediaPlayer_.on(dashjs.MediaPlayer.events.MANIFEST_LOADED, this.getDuration_); // Apply all dash options that are set + + if (options.dash) { + Object.keys(options.dash).forEach(function (key) { + var _this$mediaPlayer_; + + var dashOptionsKey = 'set' + key.charAt(0).toUpperCase() + key.slice(1); + var value = options.dash[key]; + + if (_this.mediaPlayer_.hasOwnProperty(dashOptionsKey)) { + // Providing a key without `set` prefix is now deprecated. + videojs.log.warn('Using dash options in videojs-contrib-dash without the set prefix ' + ("has been deprecated. Change '" + key + "' to '" + dashOptionsKey + "'")); // Set key so it will still work + + key = dashOptionsKey; + } + + if (!_this.mediaPlayer_.hasOwnProperty(key)) { + videojs.log.warn("Warning: dash configuration option unrecognized: " + key); + return; + } // Guarantee `value` is an array + + + if (!Array.isArray(value)) { + value = [value]; + } + + (_this$mediaPlayer_ = _this.mediaPlayer_)[key].apply(_this$mediaPlayer_, value); + }); + } + + this.mediaPlayer_.attachView(this.el_); // Dash.js autoplays by default, video.js will handle autoplay + + this.mediaPlayer_.setAutoPlay(false); // Setup audio tracks + + setupAudioTracks.call(null, this.player, tech); // Setup text tracks + + setupTextTracks.call(null, this.player, tech, options); // Attach the source with any protection data + + this.mediaPlayer_.setProtectionData(this.keySystemOptions_); + this.mediaPlayer_.attachSource(manifestSource); + this.tech_.triggerReady(); + } + /* + * Iterate over the `keySystemOptions` array and convert each object into + * the type of object Dash.js expects in the `protData` argument. + * + * Also rename 'licenseUrl' property in the options to an 'serverURL' property + */ + + + Html5DashJS.buildDashJSProtData = function buildDashJSProtData(keySystemOptions) { + var output = {}; + + if (!keySystemOptions || !Array.isArray(keySystemOptions)) { + return null; + } + + for (var i = 0; i < keySystemOptions.length; i++) { + var keySystem = keySystemOptions[i]; + var options = videojs.mergeOptions({}, keySystem.options); + + if (options.licenseUrl) { + options.serverURL = options.licenseUrl; + delete options.licenseUrl; + } + + output[keySystem.name] = options; + } + + return output; + }; + + var _proto = Html5DashJS.prototype; + + _proto.dispose = function dispose() { + if (this.mediaPlayer_) { + this.mediaPlayer_.off(dashjs.MediaPlayer.events.ERROR, this.retriggerError_); + this.mediaPlayer_.off(dashjs.MediaPlayer.events.MANIFEST_LOADED, this.getDuration_); + this.mediaPlayer_.reset(); + } + + if (this.player.dash) { + delete this.player.dash; + } + }; + + _proto.duration = function duration() { + if (this.mediaPlayer_.isDynamic() && !this.hasFiniteDuration_) { + return Infinity; + } + + return this.mediaPlayer_.duration(); + }; + /** + * Get a list of hooks for a specific lifecycle + * + * @param {string} type the lifecycle to get hooks from + * @param {Function=|Function[]=} hook Optionally add a hook tothe lifecycle + * @return {Array} an array of hooks or epty if none + * @method hooks + */ + + + Html5DashJS.hooks = function hooks(type, hook) { + Html5DashJS.hooks_[type] = Html5DashJS.hooks_[type] || []; + + if (hook) { + Html5DashJS.hooks_[type] = Html5DashJS.hooks_[type].concat(hook); + } + + return Html5DashJS.hooks_[type]; + }; + /** + * Add a function hook to a specific dash lifecycle + * + * @param {string} type the lifecycle to hook the function to + * @param {Function|Function[]} hook the function or array of functions to attach + * @method hook + */ + + + Html5DashJS.hook = function hook(type, _hook) { + Html5DashJS.hooks(type, _hook); + }; + /** + * Remove a hook from a specific dash lifecycle. + * + * @param {string} type the lifecycle that the function hooked to + * @param {Function} hook The hooked function to remove + * @return {boolean} True if the function was removed, false if not found + * @method removeHook + */ + + + Html5DashJS.removeHook = function removeHook(type, hook) { + var index = Html5DashJS.hooks(type).indexOf(hook); + + if (index === -1) { + return false; + } + + Html5DashJS.hooks_[type] = Html5DashJS.hooks_[type].slice(); + Html5DashJS.hooks_[type].splice(index, 1); + return true; + }; + + return Html5DashJS; + }(); + + Html5DashJS.hooks_ = {}; + + var canHandleKeySystems = function canHandleKeySystems(source) { + // copy the source + source = JSON.parse(JSON.stringify(source)); + + if (Html5DashJS.updateSourceData) { + videojs.log.warn('updateSourceData has been deprecated.' + ' Please switch to using hook("updatesource", callback).'); + source = Html5DashJS.updateSourceData(source); + } // call updatesource hooks + + + Html5DashJS.hooks('updatesource').forEach(function (hook) { + source = hook(source); + }); + var videoEl = document.createElement('video'); + + if (source.keySystemOptions && !(window.navigator.requestMediaKeySystemAccess || // IE11 Win 8.1 + videoEl.msSetMediaKeys)) { + return false; + } + + return true; + }; + + videojs.DashSourceHandler = function () { + return { + canHandleSource: function canHandleSource(source) { + var dashExtRE = /\.mpd/i; + + if (!canHandleKeySystems(source)) { + return ''; + } + + if (videojs.DashSourceHandler.canPlayType(source.type)) { + return 'probably'; + } else if (dashExtRE.test(source.src)) { + return 'maybe'; + } + + return ''; + }, + handleSource: function handleSource(source, tech, options) { + return new Html5DashJS(source, tech, options); + }, + canPlayType: function canPlayType(type) { + return videojs.DashSourceHandler.canPlayType(type); + } + }; + }; + + videojs.DashSourceHandler.canPlayType = function (type) { + var dashTypeRE = /^application\/dash\+xml/i; + + if (dashTypeRE.test(type)) { + return 'probably'; + } + + return ''; + }; // Only add the SourceHandler if the browser supports MediaSourceExtensions + + + if (window.MediaSource) { + videojs.getTech('Html5').registerSourceHandler(videojs.DashSourceHandler(), 0); + } + + videojs.Html5DashJS = Html5DashJS; + + return Html5DashJS; + +}))); diff --git a/src/style/player.css b/src/style/player.css index c92d18946..1f00b6cdd 100644 --- a/src/style/player.css +++ b/src/style/player.css @@ -204,7 +204,11 @@ iframe { .videoPlayer { width: 100%; - max-height: 1100px; + height: 100%; +} + +#player { + min-height: 480px; } .statistics { diff --git a/src/templates/player.html b/src/templates/player.html index bf1bba5e6..3de1eca19 100644 --- a/src/templates/player.html +++ b/src/templates/player.html @@ -1,7 +1,10 @@
    -
    -