From 8506cb1f02ee117d88d0fc4bfab413b7c19b3b03 Mon Sep 17 00:00:00 2001 From: Maurice Oegerli Date: Mon, 2 Oct 2023 17:01:21 +0200 Subject: [PATCH 001/121] =?UTF-8?q?=F0=9F=9A=A7=20Work=20on=20videoplayer?= =?UTF-8?q?=20rework?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/components/video/ControlBar.js | 1089 +++++++++++++++++ client/components/video/Player2.vue | 56 + client/composables/videoplayer/videoState.ts | 23 + client/pages/watch.vue | 6 +- client/utils/videoplayer/adapters/adapter.ts | 22 + .../utils/videoplayer/adapters/dashAdapter.ts | 62 + client/utils/videoplayer/helpers/dash.ts | 9 - 7 files changed, 1256 insertions(+), 11 deletions(-) create mode 100644 client/components/video/ControlBar.js create mode 100644 client/components/video/Player2.vue create mode 100644 client/composables/videoplayer/videoState.ts create mode 100644 client/utils/videoplayer/adapters/adapter.ts create mode 100644 client/utils/videoplayer/adapters/dashAdapter.ts diff --git a/client/components/video/ControlBar.js b/client/components/video/ControlBar.js new file mode 100644 index 000000000..3864dbdee --- /dev/null +++ b/client/components/video/ControlBar.js @@ -0,0 +1,1089 @@ +/** + * 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, + 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); + }; + + 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); + }; + + 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.time()); + updateDuration(); + togglePlayPauseBtnState(); + if (seekbarBufferInterval) { + clearInterval(seekbarBufferInterval); + } + }; + + var _onPlayTimeUpdate = function (/*e*/) { + updateDuration(); + if (!seeking) { + setTime(displayUTCTimeCodes ? player.timeAsUTC() : player.time()); + if (seekbarPlay) { + seekbarPlay.style.width = (player.time() / player.duration() * 100) + '%'; + } + + if (seekbar.getAttribute('type') === 'range') { + seekbar.value = player.time(); + } + + } + }; + + 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)) { + 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 = self.player.duration() - value; + 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.durationAsUTC() : 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.getBitrateInfoListFor && self.player.getBitrateInfoListFor('audio') || []; + availableBitrates.video = self.player.getBitrateInfoListFor && self.player.getBitrateInfoListFor('video') || []; + availableBitrates.images = self.player.getBitrateInfoListFor && self.player.getBitrateInfoListFor('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.bitrate / 1000) + ' kbps'; + result += element && element.width && element.height ? ' (' + element.width + 'x' + element.height + ')' : ''; + 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] + ' '; + } + + if (element.codec) { + info += '- Codec: ' + element.codec + ' '; + } + + return label || info + }; + trackSwitchMenu = createMenu(availableTracks, contentFunc); + var func = function () { + onMenuClick(trackSwitchMenu, trackSwitchBtn); + }; + menuHandlersList.track = func; + trackSwitchBtn.addEventListener('click', func); + trackSwitchBtn.classList.remove('hide'); + } + } + }; + + 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 'OFF'; + } + + var label = getLabelForLocale(element.labels); + if (label) { + return label + ' : ' + element.type; + } + + return element.lang + ' : ' + element.kind; + }; + 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 _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); + createCaptionSwitchMenu(e.streamId); + }; + + var _onBufferLevelUpdated = function () { + if (seekbarBuffer) { + seekbarBuffer.style.width = ((player.time() + 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; + 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'); + } + + 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.setQualityFor(item.mediaType, item.index - 1, forceQuality); + } else { + cfg.streaming.abr.autoSwitchBitrate[item.mediaType] = true; + self.player.updateSettings(cfg); + } + break; + case 'image-bitrate-list': + player.setQualityFor(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\./); + }; + + //************************************************************************************ + // 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); + } + 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); + } + }; +}; diff --git a/client/components/video/Player2.vue b/client/components/video/Player2.vue new file mode 100644 index 000000000..ae49fc6f9 --- /dev/null +++ b/client/components/video/Player2.vue @@ -0,0 +1,56 @@ + + + + + diff --git a/client/composables/videoplayer/videoState.ts b/client/composables/videoplayer/videoState.ts new file mode 100644 index 000000000..d577e3bd3 --- /dev/null +++ b/client/composables/videoplayer/videoState.ts @@ -0,0 +1,23 @@ +import { VideoplaybackAdapter } from '~/utils/videoplayer/adapters/adapter'; + +export const useVideoState = ( + videoRef: Ref, + adapter: VideoplaybackAdapter, + source: Ref +) => { + const videoState = reactive({ + playing: false, + buffering: false, + currentTime: 0, + duration: 0, + volume: 1 + }); + + onMounted(async () => { + const adapterInstance = await adapter(videoRef, source); + }); + + return { + videoState + }; +}; diff --git a/client/pages/watch.vue b/client/pages/watch.vue index f2da62a72..c3146287b 100644 --- a/client/pages/watch.vue +++ b/client/pages/watch.vue @@ -301,7 +301,7 @@ const watchPageTitle = computed(() => { - { :autoplay="isAutoplaying" class="video-player-p" @video-ended="onVideoEnded" - /> + /> --> + +