'use strict'; // Contains only auxiliary methods // May be included and executed unlimited number of times without any consequences // Polyfills for IE11 Array.prototype.find = Array.prototype.find || function (condition) { return this.filter(condition)[0]; }; Array.from = Array.from || function (source) { return Array.prototype.slice.call(source); }; NodeList.prototype.forEach = NodeList.prototype.forEach || function (callback) { Array.from(this).forEach(callback); }; String.prototype.includes = String.prototype.includes || function (searchString) { return this.indexOf(searchString) >= 0; }; String.prototype.startsWith = String.prototype.startsWith || function (prefix) { return this.substr(0, prefix.length) === prefix; }; Math.sign = Math.sign || function(x) { x = +x; if (!x) return x; // 0 and NaN return x > 0 ? 1 : -1; }; if (!window.hasOwnProperty('HTMLDetailsElement') && !window.hasOwnProperty('mockHTMLDetailsElement')) { window.mockHTMLDetailsElement = true; const style = 'details:not([open]) > :not(summary) {display: none}'; document.head.appendChild(document.createElement('style')).textContent = style; addEventListener('click', function (e) { if (e.target.nodeName !== 'SUMMARY') return; const details = e.target.parentElement; if (details.hasAttribute('open')) details.removeAttribute('open'); else details.setAttribute('open', ''); }); } // Monstrous global variable for handy code // Includes: clamp, xhr, storage.{get,set,remove} window.helpers = window.helpers || { /** * https://en.wikipedia.org/wiki/Clamping_(graphics) * @param {Number} num Source number * @param {Number} min Low border * @param {Number} max High border * @returns {Number} Clamped value */ clamp: function (num, min, max) { if (max < min) { var t = max; max = min; min = t; // swap max and min } if (max < num) return max; if (min > num) return min; return num; }, /** @private */ _xhr: function (method, url, options, callbacks) { const xhr = new XMLHttpRequest(); xhr.open(method, url); // Default options xhr.responseType = 'json'; xhr.timeout = 10000; // Default options redefining if (options.responseType) xhr.responseType = options.responseType; if (options.timeout) xhr.timeout = options.timeout; if (method === 'POST') xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); // better than onreadystatechange because of 404 codes https://stackoverflow.com/a/36182963 xhr.onloadend = function () { if (xhr.status === 200) { if (callbacks.on200) { // fix for IE11. It doesn't convert response to JSON if (xhr.responseType === '' && typeof(xhr.response) === 'string') callbacks.on200(JSON.parse(xhr.response)); else callbacks.on200(xhr.response); } } else { // handled by onerror if (xhr.status === 0) return; if (callbacks.onNon200) callbacks.onNon200(xhr); } }; xhr.ontimeout = function () { if (callbacks.onTimeout) callbacks.onTimeout(xhr); }; xhr.onerror = function () { if (callbacks.onError) callbacks.onError(xhr); }; if (options.payload) xhr.send(options.payload); else xhr.send(); }, /** @private */ _xhrRetry: function(method, url, options, callbacks) { if (options.retries <= 0) { console.warn('Failed to pull', options.entity_name); if (callbacks.onTotalFail) callbacks.onTotalFail(); return; } helpers._xhr(method, url, options, callbacks); }, /** * @callback callbackXhrOn200 * @param {Object} response - xhr.response */ /** * @callback callbackXhrError * @param {XMLHttpRequest} xhr */ /** * @param {'GET'|'POST'} method - 'GET' or 'POST' * @param {String} url - URL to send request to * @param {Object} options - other XHR options * @param {XMLHttpRequestBodyInit} [options.payload=null] - payload for POST-requests * @param {'arraybuffer'|'blob'|'document'|'json'|'text'} [options.responseType=json] * @param {Number} [options.timeout=10000] * @param {Number} [options.retries=1] * @param {String} [options.entity_name='unknown'] - string to log * @param {Number} [options.retry_timeout=1000] * @param {Object} callbacks - functions to execute on events fired * @param {callbackXhrOn200} [callbacks.on200] * @param {callbackXhrError} [callbacks.onNon200] * @param {callbackXhrError} [callbacks.onTimeout] * @param {callbackXhrError} [callbacks.onError] * @param {callbackXhrError} [callbacks.onTotalFail] - if failed after all retries */ xhr: function(method, url, options, callbacks) { if (!options.retries || options.retries <= 1) { helpers._xhr(method, url, options, callbacks); return; } if (!options.entity_name) options.entity_name = 'unknown'; if (!options.retry_timeout) options.retry_timeout = 1000; const retries_total = options.retries; let currentTry = 1; const retry = function () { console.warn('Pulling ' + options.entity_name + ' failed... ' + (currentTry++) + '/' + retries_total); setTimeout(function () { options.retries--; helpers._xhrRetry(method, url, options, callbacks); }, options.retry_timeout); }; // Pack retry() call into error handlers callbacks._onError = callbacks.onError; callbacks.onError = function (xhr) { if (callbacks._onError) callbacks._onError(xhr); retry(); }; callbacks._onTimeout = callbacks.onTimeout; callbacks.onTimeout = function (xhr) { if (callbacks._onTimeout) callbacks._onTimeout(xhr); retry(); }; helpers._xhrRetry(method, url, options, callbacks); }, /** * @typedef {Object} invidiousStorage * @property {(key:String) => Object} get * @property {(key:String, value:Object)} set * @property {(key:String)} remove */ /** * Universal storage, stores and returns JS objects. Uses inside localStorage or cookies * @type {invidiousStorage} */ storage: (function () { // access to localStorage throws exception in Tor Browser, so try is needed let localStorageIsUsable = false; try{localStorageIsUsable = !!localStorage.setItem;}catch(e){} if (localStorageIsUsable) { return { get: function (key) { let storageItem = localStorage.getItem(key) if (!storageItem) return; try { return JSON.parse(decodeURIComponent(storageItem)); } catch(e) { // Erase non parsable value helpers.storage.remove(key); } }, set: function (key, value) { let encoded_value = encodeURIComponent(JSON.stringify(value)) localStorage.setItem(key, encoded_value); }, remove: function (key) { localStorage.removeItem(key); } }; } // TODO: fire 'storage' event for cookies console.info('Storage: localStorage is disabled or unaccessible. Cookies used as fallback'); return { get: function (key) { const cookiePrefix = key + '='; function findCallback(cookie) {return cookie.startsWith(cookiePrefix);} const matchedCookie = document.cookie.split('; ').find(findCallback); if (matchedCookie) { const cookieBody = matchedCookie.replace(cookiePrefix, ''); if (cookieBody.length === 0) return; try { return JSON.parse(decodeURIComponent(cookieBody)); } catch(e) { // Erase non parsable value helpers.storage.remove(key); } } }, set: function (key, value) { const cookie_data = encodeURIComponent(JSON.stringify(value)); // Set expiration in 2 year const date = new Date(); date.setFullYear(date.getFullYear()+2); document.cookie = key + '=' + cookie_data + '; expires=' + date.toGMTString(); }, remove: function (key) { document.cookie = key + '=; Max-Age=0'; } }; })() };'use strict'; (function () { var video_player = document.getElementById('player_html5_api'); if (video_player) { video_player.onmouseenter = function () { video_player['data-title'] = video_player['title']; video_player['title'] = ''; }; video_player.onmouseleave = function () { video_player['title'] = video_player['data-title']; video_player['data-title'] = ''; }; video_player.oncontextmenu = function () { video_player['title'] = video_player['data-title']; }; } // For dynamically inserted elements addEventListener('click', function (e) { if (!e || !e.target) return; var t = e.target; var handler_name = t.getAttribute('data-onclick'); switch (handler_name) { case 'jump_to_time': e.preventDefault(); var time = t.getAttribute('data-jump-time'); player.currentTime(time); break; case 'get_youtube_replies': var load_more = t.getAttribute('data-load-more') !== null; var load_replies = t.getAttribute('data-load-replies') !== null; get_youtube_replies(t, load_more, load_replies); break; case 'toggle_parent': e.preventDefault(); toggle_parent(t); break; default: break; } }); document.querySelectorAll('[data-mouse="switch_classes"]').forEach(function (el) { var classes = el.getAttribute('data-switch-classes').split(','); var classOnEnter = classes[0]; var classOnLeave = classes[1]; function toggle_classes(toAdd, toRemove) { el.classList.add(toAdd); el.classList.remove(toRemove); } el.onmouseenter = function () { toggle_classes(classOnEnter, classOnLeave); }; el.onmouseleave = function () { toggle_classes(classOnLeave, classOnEnter); }; }); document.querySelectorAll('[data-onsubmit="return_false"]').forEach(function (el) { el.onsubmit = function () { return false; }; }); document.querySelectorAll('[data-onclick="mark_watched"]').forEach(function (el) { el.onclick = function () { mark_watched(el); }; }); document.querySelectorAll('[data-onclick="mark_unwatched"]').forEach(function (el) { el.onclick = function () { mark_unwatched(el); }; }); document.querySelectorAll('[data-onclick="add_playlist_video"]').forEach(function (el) { el.onclick = function () { add_playlist_video(el); }; }); document.querySelectorAll('[data-onclick="add_playlist_item"]').forEach(function (el) { el.onclick = function () { add_playlist_item(el); }; }); document.querySelectorAll('[data-onclick="remove_playlist_item"]').forEach(function (el) { el.onclick = function () { remove_playlist_item(el); }; }); document.querySelectorAll('[data-onclick="revoke_token"]').forEach(function (el) { el.onclick = function () { revoke_token(el); }; }); document.querySelectorAll('[data-onclick="remove_subscription"]').forEach(function (el) { el.onclick = function () { remove_subscription(el); }; }); document.querySelectorAll('[data-onclick="notification_requestPermission"]').forEach(function (el) { el.onclick = function () { Notification.requestPermission(); }; }); document.querySelectorAll('[data-onrange="update_volume_value"]').forEach(function (el) { function update_volume_value() { document.getElementById('volume-value').textContent = el.value; } el.oninput = update_volume_value; el.onchange = update_volume_value; }); function revoke_token(target) { var row = target.parentNode.parentNode.parentNode.parentNode.parentNode; row.style.display = 'none'; var count = document.getElementById('count'); count.textContent--; var url = '/token_ajax?action_revoke_token=1&redirect=false' + '&referer=' + encodeURIComponent(location.href) + '&session=' + target.getAttribute('data-session'); var payload = 'csrf_token=' + target.parentNode.querySelector('input[name="csrf_token"]').value; helpers.xhr('POST', url, {payload: payload}, { onNon200: function (xhr) { count.textContent++; row.style.display = ''; } }); } function remove_subscription(target) { var row = target.parentNode.parentNode.parentNode.parentNode.parentNode; row.style.display = 'none'; var count = document.getElementById('count'); count.textContent--; var url = '/subscription_ajax?action_remove_subscriptions=1&redirect=false' + '&referer=' + encodeURIComponent(location.href) + '&c=' + target.getAttribute('data-ucid'); var payload = 'csrf_token=' + target.parentNode.querySelector('input[name="csrf_token"]').value; helpers.xhr('POST', url, {payload: payload}, { onNon200: function (xhr) { count.textContent++; row.style.display = ''; } }); } // Handle keypresses addEventListener('keydown', function (event) { // Ignore modifier keys if (event.ctrlKey || event.metaKey) return; // Ignore shortcuts if any text input is focused let focused_tag = document.activeElement.tagName.toLowerCase(); const allowed = /^(button|checkbox|file|radio|submit)$/; if (focused_tag === 'textarea') return; if (focused_tag === 'input') { let focused_type = document.activeElement.type.toLowerCase(); if (!allowed.test(focused_type)) return; } // Focus search bar on '/' if (event.key === '/') { document.getElementById('searchbox').focus(); event.preventDefault(); } }); })();'use strict'; var community_data = JSON.parse(document.getElementById('community_data').textContent); function hide_youtube_replies(event) { var target = event.target; var sub_text = target.getAttribute('data-inner-text'); var inner_text = target.getAttribute('data-sub-text'); var body = target.parentNode.parentNode.children[1]; body.style.display = 'none'; target.innerHTML = sub_text; target.onclick = show_youtube_replies; target.setAttribute('data-inner-text', inner_text); target.setAttribute('data-sub-text', sub_text); } function show_youtube_replies(event) { var target = event.target; var sub_text = target.getAttribute('data-inner-text'); var inner_text = target.getAttribute('data-sub-text'); var body = target.parentNode.parentNode.children[1]; body.style.display = ''; target.innerHTML = sub_text; target.onclick = hide_youtube_replies; target.setAttribute('data-inner-text', inner_text); target.setAttribute('data-sub-text', sub_text); } function get_youtube_replies(target, load_more) { var continuation = target.getAttribute('data-continuation'); var body = target.parentNode.parentNode; var fallback = body.innerHTML; body.innerHTML = '

'; var url = '/api/v1/channels/comments/' + community_data.ucid + '?format=html' + '&hl=' + community_data.preferences.locale + '&thin_mode=' + community_data.preferences.thin_mode + '&continuation=' + continuation; helpers.xhr('GET', url, {}, { on200: function (response) { if (load_more) { body = body.parentNode.parentNode; body.removeChild(body.lastElementChild); body.innerHTML += response.contentHtml; } else { body.removeChild(body.lastElementChild); var p = document.createElement('p'); var a = document.createElement('a'); p.appendChild(a); a.href = 'javascript:void(0)'; a.onclick = hide_youtube_replies; a.setAttribute('data-sub-text', community_data.hide_replies_text); a.setAttribute('data-inner-text', community_data.show_replies_text); a.textContent = community_data.hide_replies_text; var div = document.createElement('div'); div.innerHTML = response.contentHtml; body.appendChild(p); body.appendChild(div); } }, onNon200: function (xhr) { body.innerHTML = fallback; }, onTimeout: function (xhr) { console.warn('Pulling comments failed'); body.innerHTML = fallback; } }); }'use strict'; var video_data = JSON.parse(document.getElementById('video_data').textContent); function get_playlist(plid) { var plid_url; if (plid.startsWith('RD')) { plid_url = '/api/v1/mixes/' + plid + '?continuation=' + video_data.id + '&format=html&hl=' + video_data.preferences.locale; } else { plid_url = '/api/v1/playlists/' + plid + '?index=' + video_data.index + '&continuation' + video_data.id + '&format=html&hl=' + video_data.preferences.locale; } helpers.xhr('GET', plid_url, {retries: 5, entity_name: 'playlist'}, { on200: function (response) { if (!response.nextVideo) return; player.on('ended', function () { var url = new URL('https://example.com/embed/' + response.nextVideo); url.searchParams.set('list', plid); if (!plid.startsWith('RD')) url.searchParams.set('index', response.index); if (video_data.params.autoplay || video_data.params.continue_autoplay) url.searchParams.set('autoplay', '1'); if (video_data.params.listen !== video_data.preferences.listen) url.searchParams.set('listen', video_data.params.listen); if (video_data.params.speed !== video_data.preferences.speed) url.searchParams.set('speed', video_data.params.speed); if (video_data.params.local !== video_data.preferences.local) url.searchParams.set('local', video_data.params.local); location.assign(url.pathname + url.search); }); } }); } addEventListener('load', function (e) { if (video_data.plid) { get_playlist(video_data.plid); } else if (video_data.video_series) { player.on('ended', function () { var url = new URL('https://example.com/embed/' + video_data.video_series.shift()); if (video_data.params.autoplay || video_data.params.continue_autoplay) url.searchParams.set('autoplay', '1'); if (video_data.params.listen !== video_data.preferences.listen) url.searchParams.set('listen', video_data.params.listen); if (video_data.params.speed !== video_data.preferences.speed) url.searchParams.set('speed', video_data.params.speed); if (video_data.params.local !== video_data.preferences.local) url.searchParams.set('local', video_data.params.local); if (video_data.video_series.length !== 0) url.searchParams.set('playlist', video_data.video_series.join(',')); location.assign(url.pathname + url.search); }); } });'use strict'; var notification_data = JSON.parse(document.getElementById('notification_data').textContent); /** Boolean meaning 'some tab have stream' */ const STORAGE_KEY_STREAM = 'stream'; /** Number of notifications. May be increased or reset */ const STORAGE_KEY_NOTIF_COUNT = 'notification_count'; var notifications, delivered; var notifications_mock = { close: function () { } }; function get_subscriptions() { helpers.xhr('GET', '/api/v1/auth/subscriptions?fields=authorId', { retries: 5, entity_name: 'subscriptions' }, { on200: create_notification_stream }); } function create_notification_stream(subscriptions) { // sse.js can't be replaced to EventSource in place as it lack support of payload and headers // see https://developer.mozilla.org/en-US/docs/Web/API/EventSource/EventSource notifications = new SSE( '/api/v1/auth/notifications?fields=videoId,title,author,authorId,publishedText,published,authorThumbnails,liveNow', { withCredentials: true, payload: 'topics=' + subscriptions.map(function (subscription) { return subscription.authorId; }).join(','), headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }); delivered = []; var start_time = Math.round(new Date() / 1000); notifications.onmessage = function (event) { if (!event.id) return; var notification = JSON.parse(event.data); console.info('Got notification:', notification); // Ignore not actual and delivered notifications if (start_time > notification.published || delivered.includes(notification.videoId)) return; delivered.push(notification.videoId); let notification_count = helpers.storage.get(STORAGE_KEY_NOTIF_COUNT) || 0; notification_count++; helpers.storage.set(STORAGE_KEY_NOTIF_COUNT, notification_count); update_ticker_count(); // permission for notifications handled on settings page. JS handler is in handlers.js if (window.Notification && Notification.permission === 'granted') { var notification_text = notification.liveNow ? notification_data.live_now_text : notification_data.upload_text; notification_text = notification_text.replace('`x`', notification.author); var system_notification = new Notification(notification_text, { body: notification.title, icon: '/ggpht' + new URL(notification.authorThumbnails[2].url).pathname, img: '/ggpht' + new URL(notification.authorThumbnails[4].url).pathname }); system_notification.onclick = function (e) { open('/watch?v=' + notification.videoId, '_blank'); }; } }; notifications.addEventListener('error', function (e) { console.warn('Something went wrong with notifications, trying to reconnect...'); notifications = notifications_mock; setTimeout(get_subscriptions, 1000); }); notifications.stream(); } function update_ticker_count() { var notification_ticker = document.getElementById('notification_ticker'); const notification_count = helpers.storage.get(STORAGE_KEY_STREAM); if (notification_count > 0) { notification_ticker.innerHTML = '' + notification_count + ' '; } else { notification_ticker.innerHTML = ''; } } function start_stream_if_needed() { // random wait for other tabs set 'stream' flag setTimeout(function () { if (!helpers.storage.get(STORAGE_KEY_STREAM)) { // if no one set 'stream', set it by yourself and start stream helpers.storage.set(STORAGE_KEY_STREAM, true); notifications = notifications_mock; get_subscriptions(); } }, Math.random() * 1000 + 50); // [0.050 .. 1.050) second } addEventListener('storage', function (e) { if (e.key === STORAGE_KEY_NOTIF_COUNT) update_ticker_count(); // if 'stream' key was removed if (e.key === STORAGE_KEY_STREAM && !helpers.storage.get(STORAGE_KEY_STREAM)) { if (notifications) { // restore it if we have active stream helpers.storage.set(STORAGE_KEY_STREAM, true); } else { start_stream_if_needed(); } } }); addEventListener('load', function () { var notification_count_el = document.getElementById('notification_count'); var notification_count = notification_count_el ? parseInt(notification_count_el.textContent) : 0; helpers.storage.set(STORAGE_KEY_NOTIF_COUNT, notification_count); if (helpers.storage.get(STORAGE_KEY_STREAM)) helpers.storage.remove(STORAGE_KEY_STREAM); start_stream_if_needed(); }); addEventListener('unload', function () { // let chance to other tabs to be a streamer via firing 'storage' event if (notifications) helpers.storage.remove(STORAGE_KEY_STREAM); });'use strict'; var player_data = JSON.parse(document.getElementById('player_data').textContent); var video_data = JSON.parse(document.getElementById('video_data').textContent); var options = { preload: 'auto', liveui: true, playbackRates: [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0], controlBar: { children: [ 'playToggle', 'volumePanel', 'currentTimeDisplay', 'timeDivider', 'durationDisplay', 'progressControl', 'remainingTimeDisplay', 'Spacer', 'captionsButton', 'audioTrackButton', 'qualitySelector', 'playbackRateMenuButton', 'fullscreenToggle' ] }, html5: { preloadTextTracks: false, vhs: { overrideNative: true } } }; if (player_data.aspect_ratio) { options.aspectRatio = player_data.aspect_ratio; } var embed_url = new URL(location); embed_url.searchParams.delete('v'); var short_url = location.origin + '/' + video_data.id + embed_url.search; embed_url = location.origin + '/embed/' + video_data.id + embed_url.search; var save_player_pos_key = 'save_player_pos'; videojs.Vhs.xhr.beforeRequest = function(options) { // set local if requested not videoplayback if (!options.uri.includes('videoplayback')) { if (!options.uri.includes('local=true')) options.uri += '?local=true'; } return options; }; var player = videojs('player', options); player.on('error', function () { if (video_data.params.quality === 'dash') return; var localNotDisabled = ( !player.currentSrc().includes('local=true') && !video_data.local_disabled ); var reloadMakesSense = ( player.error().code === MediaError.MEDIA_ERR_NETWORK || player.error().code === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED ); if (localNotDisabled) { // add local=true to all current sources player.src(player.currentSources().map(function (source) { source.src += '&local=true'; return source; })); } else if (reloadMakesSense) { setTimeout(function () { console.warn('An error occurred in the player, reloading...'); // After load() all parameters are reset. Save them var currentTime = player.currentTime(); var playbackRate = player.playbackRate(); var paused = player.paused(); player.load(); if (currentTime > 0.5) currentTime -= 0.5; player.currentTime(currentTime); player.playbackRate(playbackRate); if (!paused) player.play(); }, 5000); } }); if (video_data.params.quality === 'dash') { player.reloadSourceOnError({ errorInterval: 10 }); } /** * Function for add time argument to url * * @param {String} url * @param {String} [base] * @returns {URL} urlWithTimeArg */ function addCurrentTimeToURL(url, base) { var urlUsed = new URL(url, base); urlUsed.searchParams.delete('start'); var currentTime = Math.ceil(player.currentTime()); if (currentTime > 0) urlUsed.searchParams.set('t', currentTime); else if (urlUsed.searchParams.has('t')) urlUsed.searchParams.delete('t'); return urlUsed; } /** * Global variable to save the last timestamp (in full seconds) at which the external * links were updated by the 'timeupdate' callback below. * * It is initialized to 5s so that the video will always restart from the beginning * if the user hasn't really started watching before switching to the other website. */ var timeupdate_last_ts = 5; /** * Callback that updates the timestamp on all external links */ player.on('timeupdate', function () { // Only update once every second let current_ts = Math.floor(player.currentTime()); if (current_ts > timeupdate_last_ts) timeupdate_last_ts = current_ts; else return; // YouTube links let elem_yt_watch = document.getElementById('link-yt-watch'); let elem_yt_embed = document.getElementById('link-yt-embed'); let base_url_yt_watch = elem_yt_watch.getAttribute('data-base-url'); let base_url_yt_embed = elem_yt_embed.getAttribute('data-base-url'); elem_yt_watch.href = addCurrentTimeToURL(base_url_yt_watch); elem_yt_embed.href = addCurrentTimeToURL(base_url_yt_embed); // Invidious links let domain = window.location.origin; let elem_iv_embed = document.getElementById('link-iv-embed'); let elem_iv_other = document.getElementById('link-iv-other'); let base_url_iv_embed = elem_iv_embed.getAttribute('data-base-url'); let base_url_iv_other = elem_iv_other.getAttribute('data-base-url'); elem_iv_embed.href = addCurrentTimeToURL(base_url_iv_embed, domain); elem_iv_other.href = addCurrentTimeToURL(base_url_iv_other, domain); }); var shareOptions = { socials: ['fbFeed', 'tw', 'reddit', 'email'], get url() { return addCurrentTimeToURL(short_url); }, title: player_data.title, description: player_data.description, image: player_data.thumbnail, get embedCode() { // Single quotes inside here required. HTML inserted as is into value attribute of input return ""; } }; if (location.pathname.startsWith('/embed/')) { var overlay_content = '

' + player_data.title + '

'; player.overlay({ overlays: [ { start: 'loadstart', content: overlay_content, end: 'playing', align: 'top'}, { start: 'pause', content: overlay_content, end: 'playing', align: 'top'} ] }); } // Detect mobile users and initialize mobileUi for better UX // Detection code taken from https://stackoverflow.com/a/20293441 function isMobile() { try{ document.createEvent('TouchEvent'); return true; } catch(e){ return false; } } if (isMobile()) { player.mobileUi({ touchControls: { seekSeconds: 5 * player.playbackRate() } }); var buttons = ['playToggle', 'volumePanel', 'captionsButton']; if (!video_data.params.listen && video_data.params.quality === 'dash') buttons.push('audioTrackButton'); if (video_data.params.listen || video_data.params.quality !== 'dash') buttons.push('qualitySelector'); // Create new control bar object for operation buttons const ControlBar = videojs.getComponent('controlBar'); let operations_bar = new ControlBar(player, { children: [], playbackRates: [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0] }); buttons.slice(1).forEach(function (child) {operations_bar.addChild(child);}); // Remove operation buttons from primary control bar var primary_control_bar = player.getChild('controlBar'); buttons.forEach(function (child) {primary_control_bar.removeChild(child);}); var operations_bar_element = operations_bar.el(); operations_bar_element.classList.add('mobile-operations-bar'); player.addChild(operations_bar); // Playback menu doesn't work when it's initialized outside of the primary control bar var playback_element = document.getElementsByClassName('vjs-playback-rate')[0]; operations_bar_element.append(playback_element); // The share and http source selector element can't be fetched till the players ready. player.one('playing', function () { var share_element = document.getElementsByClassName('vjs-share-control')[0]; operations_bar_element.append(share_element); if (!video_data.params.listen && video_data.params.quality === 'dash') { var http_source_selector = document.getElementsByClassName('vjs-http-source-selector vjs-menu-button')[0]; operations_bar_element.append(http_source_selector); } }); } // Enable VR video support if (!video_data.params.listen && video_data.vr && video_data.params.vr_mode) { player.crossOrigin('anonymous'); switch (video_data.projection_type) { case 'EQUIRECTANGULAR': player.vr({projection: 'equirectangular'}); default: // Should only be 'MESH' but we'll use this as a fallback. player.vr({projection: 'EAC'}); } } // Add markers if (video_data.params.video_start > 0 || video_data.params.video_end > 0) { var markers = [{ time: video_data.params.video_start, text: 'Start' }]; if (video_data.params.video_end < 0) { markers.push({ time: video_data.length_seconds - 0.5, text: 'End' }); } else { markers.push({ time: video_data.params.video_end, text: 'End' }); } player.markers({ onMarkerReached: function (marker) { if (marker.text === 'End') player.loop() ? player.markers.prev('Start') : player.pause(); }, markers: markers }); player.currentTime(video_data.params.video_start); } player.volume(video_data.params.volume / 100); player.playbackRate(video_data.params.speed); /** * Method for getting the contents of a cookie * * @param {String} name Name of cookie * @returns {String|null} cookieValue */ function getCookieValue(name) { var cookiePrefix = name + '='; var matchedCookie = document.cookie.split(';').find(function (item) {return item.includes(cookiePrefix);}); if (matchedCookie) return matchedCookie.replace(cookiePrefix, ''); return null; } /** * Method for updating the 'PREFS' cookie (or creating it if missing) * * @param {number} newVolume New volume defined (null if unchanged) * @param {number} newSpeed New speed defined (null if unchanged) */ function updateCookie(newVolume, newSpeed) { var volumeValue = newVolume !== null ? newVolume : video_data.params.volume; var speedValue = newSpeed !== null ? newSpeed : video_data.params.speed; var cookieValue = getCookieValue('PREFS'); var cookieData; if (cookieValue !== null) { var cookieJson = JSON.parse(decodeURIComponent(cookieValue)); cookieJson.volume = volumeValue; cookieJson.speed = speedValue; cookieData = encodeURIComponent(JSON.stringify(cookieJson)); } else { cookieData = encodeURIComponent(JSON.stringify({ 'volume': volumeValue, 'speed': speedValue })); } // Set expiration in 2 year var date = new Date(); date.setFullYear(date.getFullYear() + 2); var ipRegex = /^((\d+\.){3}\d+|[\dA-Fa-f]*:[\d:A-Fa-f]*:[\d:A-Fa-f]+)$/; var domainUsed = location.hostname; // Fix for a bug in FF where the leading dot in the FQDN is not ignored if (domainUsed.charAt(0) !== '.' && !ipRegex.test(domainUsed) && domainUsed !== 'localhost') domainUsed = '.' + location.hostname; var secure = location.protocol.startsWith("https") ? " Secure;" : ""; document.cookie = 'PREFS=' + cookieData + '; SameSite=Lax; path=/; domain=' + domainUsed + '; expires=' + date.toGMTString() + ';' + secure; video_data.params.volume = volumeValue; video_data.params.speed = speedValue; } player.on('ratechange', function () { updateCookie(null, player.playbackRate()); if (isMobile()) { player.mobileUi({ touchControls: { seekSeconds: 5 * player.playbackRate() } }); } }); player.on('volumechange', function () { updateCookie(Math.ceil(player.volume() * 100), null); }); player.on('waiting', function () { if (player.playbackRate() > 1 && player.liveTracker.isLive() && player.liveTracker.atLiveEdge()) { console.info('Player has caught up to source, resetting playbackRate'); player.playbackRate(1); } }); if (video_data.premiere_timestamp && Math.round(new Date() / 1000) < video_data.premiere_timestamp) { player.getChild('bigPlayButton').hide(); } if (video_data.params.save_player_pos) { const url = new URL(location); const hasTimeParam = url.searchParams.has('t'); const rememberedTime = get_video_time(); let lastUpdated = 0; if(!hasTimeParam) set_seconds_after_start(rememberedTime); player.on('timeupdate', function () { const raw = player.currentTime(); const time = Math.floor(raw); if(lastUpdated !== time && raw <= video_data.length_seconds - 15) { save_video_time(time); lastUpdated = time; } }); } else remove_all_video_times(); if (video_data.params.autoplay) { var bpb = player.getChild('bigPlayButton'); bpb.hide(); player.ready(function () { new Promise(function (resolve, reject) { setTimeout(function () {resolve(1);}, 1); }).then(function (result) { var promise = player.play(); if (promise !== undefined) { promise.then(function () { }).catch(function (error) { bpb.show(); }); } }); }); } if (!video_data.params.listen && video_data.params.quality === 'dash') { player.httpSourceSelector(); if (video_data.params.quality_dash !== 'auto') { player.ready(function () { player.on('loadedmetadata', function () { const qualityLevels = Array.from(player.qualityLevels()).sort(function (a, b) {return a.height - b.height;}); let targetQualityLevel; switch (video_data.params.quality_dash) { case 'best': targetQualityLevel = qualityLevels.length - 1; break; case 'worst': targetQualityLevel = 0; break; default: const targetHeight = parseInt(video_data.params.quality_dash); for (let i = 0; i < qualityLevels.length; i++) { if (qualityLevels[i].height <= targetHeight) targetQualityLevel = i; else break; } } qualityLevels.forEach(function (level, index) { level.enabled = (index === targetQualityLevel); }); }); }); } } player.vttThumbnails({ src: '/api/v1/storyboards/' + video_data.id + '?height=90', showTimestamp: true }); // Enable annotations if (!video_data.params.listen && video_data.params.annotations) { addEventListener('load', function (e) { addEventListener('__ar_annotation_click', function (e) { const url = e.detail.url, target = e.detail.target, seconds = e.detail.seconds; var path = new URL(url); if (path.href.startsWith('https://www.youtube.com/watch?') && seconds) { path.search += '&t=' + seconds; } path = path.pathname + path.search; if (target === 'current') { location.href = path; } else if (target === 'new') { open(path, '_blank'); } }); helpers.xhr('GET', '/api/v1/annotations/' + video_data.id, { responseType: 'text', timeout: 60000 }, { on200: function (response) { var video_container = document.getElementById('player'); videojs.registerPlugin('youtubeAnnotationsPlugin', youtubeAnnotationsPlugin); if (player.paused()) { player.one('play', function (event) { player.youtubeAnnotationsPlugin({ annotationXml: response, videoContainer: video_container }); }); } else { player.youtubeAnnotationsPlugin({ annotationXml: response, videoContainer: video_container }); } } }); }); } function change_volume(delta) { const curVolume = player.volume(); let newVolume = curVolume + delta; newVolume = helpers.clamp(newVolume, 0, 1); player.volume(newVolume); } function toggle_muted() { player.muted(!player.muted()); } function skip_seconds(delta) { const duration = player.duration(); const curTime = player.currentTime(); let newTime = curTime + delta; newTime = helpers.clamp(newTime, 0, duration); player.currentTime(newTime); } function set_seconds_after_start(delta) { const start = video_data.params.video_start; player.currentTime(start + delta); } function save_video_time(seconds) { const all_video_times = get_all_video_times(); all_video_times[video_data.id] = seconds; helpers.storage.set(save_player_pos_key, all_video_times); } function get_video_time() { return get_all_video_times()[video_data.id] || 0; } function get_all_video_times() { return helpers.storage.get(save_player_pos_key) || {}; } function remove_all_video_times() { helpers.storage.remove(save_player_pos_key); } function set_time_percent(percent) { const duration = player.duration(); const newTime = duration * (percent / 100); player.currentTime(newTime); } function play() { player.play(); } function pause() { player.pause(); } function stop() { player.pause(); player.currentTime(0); } function toggle_play() { player.paused() ? play() : pause(); } const toggle_captions = (function () { let toggledTrack = null; function bindChange(onOrOff) { player.textTracks()[onOrOff]('change', function (e) { toggledTrack = null; }); } // Wrapper function to ignore our own emitted events and only listen // to events emitted by Video.js on click on the captions menu items. function setMode(track, mode) { bindChange('off'); track.mode = mode; setTimeout(function () { bindChange('on'); }, 0); } bindChange('on'); return function () { if (toggledTrack !== null) { if (toggledTrack.mode !== 'showing') { setMode(toggledTrack, 'showing'); } else { setMode(toggledTrack, 'disabled'); } toggledTrack = null; return; } // Used as a fallback if no captions are currently active. // TODO: Make this more intelligent by e.g. relying on browser language. let fallbackCaptionsTrack = null; const tracks = player.textTracks(); for (let i = 0; i < tracks.length; i++) { const track = tracks[i]; if (track.kind !== 'captions') continue; if (fallbackCaptionsTrack === null) { fallbackCaptionsTrack = track; } if (track.mode === 'showing') { setMode(track, 'disabled'); toggledTrack = track; return; } } // Fallback if no captions are currently active. if (fallbackCaptionsTrack !== null) { setMode(fallbackCaptionsTrack, 'showing'); toggledTrack = fallbackCaptionsTrack; } }; })(); function toggle_fullscreen() { player.isFullscreen() ? player.exitFullscreen() : player.requestFullscreen(); } function increase_playback_rate(steps) { const maxIndex = options.playbackRates.length - 1; const curIndex = options.playbackRates.indexOf(player.playbackRate()); let newIndex = curIndex + steps; newIndex = helpers.clamp(newIndex, 0, maxIndex); player.playbackRate(options.playbackRates[newIndex]); } addEventListener('keydown', function (e) { if (e.target.tagName.toLowerCase() === 'input') { // Ignore input when focus is on certain elements, e.g. form fields. return; } // See https://github.com/ctd1500/videojs-hotkeys/blob/bb4a158b2e214ccab87c2e7b95f42bc45c6bfd87/videojs.hotkeys.js#L310-L313 const isPlayerFocused = false || e.target === document.querySelector('.video-js') || e.target === document.querySelector('.vjs-tech') || e.target === document.querySelector('.iframeblocker') || e.target === document.querySelector('.vjs-control-bar') ; let action = null; const code = e.keyCode; const decoratedKey = e.key + (e.altKey ? '+alt' : '') + (e.ctrlKey ? '+ctrl' : '') + (e.metaKey ? '+meta' : '') ; switch (decoratedKey) { case ' ': case 'k': case 'MediaPlayPause': action = toggle_play; break; case 'MediaPlay': action = play; break; case 'MediaPause': action = pause; break; case 'MediaStop': action = stop; break; case 'ArrowUp': if (isPlayerFocused) action = change_volume.bind(this, 0.1); break; case 'ArrowDown': if (isPlayerFocused) action = change_volume.bind(this, -0.1); break; case 'm': action = toggle_muted; break; case 'ArrowRight': case 'MediaFastForward': action = skip_seconds.bind(this, 5 * player.playbackRate()); break; case 'ArrowLeft': case 'MediaTrackPrevious': action = skip_seconds.bind(this, -5 * player.playbackRate()); break; case 'l': action = skip_seconds.bind(this, 10 * player.playbackRate()); break; case 'j': action = skip_seconds.bind(this, -10 * player.playbackRate()); break; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': // Ignore numpad numbers if (code > 57) break; const percent = (code - 48) * 10; action = set_time_percent.bind(this, percent); break; case 'c': action = toggle_captions; break; case 'f': action = toggle_fullscreen; break; case 'N': case 'MediaTrackNext': action = next_video; break; case 'P': case 'MediaTrackPrevious': // TODO: Add support to play back previous video. break; // TODO: More precise step. Now FPS is taken equal to 29.97 // Common FPS: https://forum.videohelp.com/threads/81868#post323588 // Possible solution is new HTMLVideoElement.requestVideoFrameCallback() https://wicg.github.io/video-rvfc/ case ',': action = function () { pause(); skip_seconds(-1/29.97); }; break; case '.': action = function () { pause(); skip_seconds( 1/29.97); }; break; case '>': action = increase_playback_rate.bind(this, 1); break; case '<': action = increase_playback_rate.bind(this, -1); break; default: console.info('Unhandled key down event: %s:', decoratedKey, e); break; } if (action) { e.preventDefault(); action(); } }, false); // Add support for controlling the player volume by scrolling over it. Adapted from // https://github.com/ctd1500/videojs-hotkeys/blob/bb4a158b2e214ccab87c2e7b95f42bc45c6bfd87/videojs.hotkeys.js#L292-L328 (function () { const pEl = document.getElementById('player'); var volumeHover = false; var volumeSelector = pEl.querySelector('.vjs-volume-menu-button') || pEl.querySelector('.vjs-volume-panel'); if (volumeSelector !== null) { volumeSelector.onmouseover = function () { volumeHover = true; }; volumeSelector.onmouseout = function () { volumeHover = false; }; } function mouseScroll(event) { // When controls are disabled, hotkeys will be disabled as well if (!player.controls() || !volumeHover) return; event.preventDefault(); var wheelMove = event.wheelDelta || -event.detail; var volumeSign = Math.sign(wheelMove); change_volume(volumeSign * 0.05); // decrease/increase by 5% } player.on('mousewheel', mouseScroll); player.on('DOMMouseScroll', mouseScroll); }()); // Since videojs-share can sometimes be blocked, we defer it until last if (player.share) player.share(shareOptions); // show the preferred caption by default if (player_data.preferred_caption_found) { player.ready(function () { if (!video_data.params.listen && video_data.params.quality === 'dash') { // play.textTracks()[0] on DASH mode is showing some debug messages player.textTracks()[1].mode = 'showing'; } else { player.textTracks()[0].mode = 'showing'; } }); } // Safari audio double duration fix if (navigator.vendor === 'Apple Computer, Inc.' && video_data.params.listen) { player.on('loadedmetadata', function () { player.on('timeupdate', function () { if (player.remainingTime() < player.duration() / 2 && player.remainingTime() >= 2) { player.currentTime(player.duration() - 1); } }); }); } // Safari screen timeout on looped video playback fix if (navigator.vendor === 'Apple Computer, Inc.' && !video_data.params.listen && video_data.params.video_loop) { player.loop(false); player.ready(function () { player.on('ended', function () { player.currentTime(0); player.play(); }); }); } // Watch on Invidious link if (location.pathname.startsWith('/embed/')) { const Button = videojs.getComponent('Button'); let watch_on_invidious_button = new Button(player); // Create hyperlink for current instance var redirect_element = document.createElement('a'); redirect_element.setAttribute('href', location.pathname.replace('/embed/', '/watch?v=')); redirect_element.appendChild(document.createTextNode('Invidious')); watch_on_invidious_button.el().appendChild(redirect_element); watch_on_invidious_button.addClass('watch-on-invidious'); var cb = player.getChild('ControlBar'); cb.addChild(watch_on_invidious_button); } addEventListener('DOMContentLoaded', function () { // Save time during redirection on another instance const changeInstanceLink = document.querySelector('#watch-on-another-invidious-instance > a'); if (changeInstanceLink) changeInstanceLink.addEventListener('click', function () { changeInstanceLink.href = addCurrentTimeToURL(changeInstanceLink.href); }); });/** * Copyright (C) 2016 Maxime Petazzoni . * All rights reserved. */ var SSE = function (url, options) { if (!(this instanceof SSE)) { return new SSE(url, options); } this.INITIALIZING = -1; this.CONNECTING = 0; this.OPEN = 1; this.CLOSED = 2; this.url = url; options = options || {}; this.headers = options.headers || {}; this.payload = options.payload !== undefined ? options.payload : ''; this.method = options.method || (this.payload && 'POST' || 'GET'); this.FIELD_SEPARATOR = ':'; this.listeners = {}; this.xhr = null; this.readyState = this.INITIALIZING; this.progress = 0; this.chunk = ''; this.addEventListener = function(type, listener) { if (this.listeners[type] === undefined) { this.listeners[type] = []; } if (this.listeners[type].indexOf(listener) === -1) { this.listeners[type].push(listener); } }; this.removeEventListener = function(type, listener) { if (this.listeners[type] === undefined) { return; } var filtered = []; this.listeners[type].forEach(function(element) { if (element !== listener) { filtered.push(element); } }); if (filtered.length === 0) { delete this.listeners[type]; } else { this.listeners[type] = filtered; } }; this.dispatchEvent = function(e) { if (!e) { return true; } e.source = this; var onHandler = 'on' + e.type; if (this.hasOwnProperty(onHandler)) { this[onHandler].call(this, e); if (e.defaultPrevented) { return false; } } if (this.listeners[e.type]) { return this.listeners[e.type].every(function(callback) { callback(e); return !e.defaultPrevented; }); } return true; }; this._setReadyState = function (state) { var event = new CustomEvent('readystatechange'); event.readyState = state; this.readyState = state; this.dispatchEvent(event); }; this._onStreamFailure = function(e) { this.dispatchEvent(new CustomEvent('error')); this.close(); } this._onStreamProgress = function(e) { if (this.xhr.status !== 200 && this.readyState !== this.CLOSED) { this._onStreamFailure(e); return; } if (this.readyState == this.CONNECTING) { this.dispatchEvent(new CustomEvent('open')); this._setReadyState(this.OPEN); } var data = this.xhr.responseText.substring(this.progress); this.progress += data.length; data.split(/(\r\n|\r|\n){2}/g).forEach(function(part) { if (part.trim().length === 0) { this.dispatchEvent(this._parseEventChunk(this.chunk.trim())); this.chunk = ''; } else { this.chunk += part; } }.bind(this)); }; this._onStreamLoaded = function(e) { this._onStreamProgress(e); // Parse the last chunk. this.dispatchEvent(this._parseEventChunk(this.chunk)); this.chunk = ''; }; /** * Parse a received SSE event chunk into a constructed event object. */ this._parseEventChunk = function(chunk) { if (!chunk || chunk.length === 0) { return null; } var e = {'id': null, 'retry': null, 'data': '', 'event': 'message'}; chunk.split(/\n|\r\n|\r/).forEach(function(line) { line = line.trimRight(); var index = line.indexOf(this.FIELD_SEPARATOR); if (index <= 0) { // Line was either empty, or started with a separator and is a comment. // Either way, ignore. return; } var field = line.substring(0, index); if (!(field in e)) { return; } var value = line.substring(index + 1).trimLeft(); if (field === 'data') { e[field] += value; } else { e[field] = value; } }.bind(this)); var event = new CustomEvent(e.event); event.data = e.data; event.id = e.id; return event; }; this._checkStreamClosed = function() { if (this.xhr.readyState === XMLHttpRequest.DONE) { this._setReadyState(this.CLOSED); } }; this.stream = function() { this._setReadyState(this.CONNECTING); this.xhr = new XMLHttpRequest(); this.xhr.addEventListener('progress', this._onStreamProgress.bind(this)); this.xhr.addEventListener('load', this._onStreamLoaded.bind(this)); this.xhr.addEventListener('readystatechange', this._checkStreamClosed.bind(this)); this.xhr.addEventListener('error', this._onStreamFailure.bind(this)); this.xhr.addEventListener('abort', this._onStreamFailure.bind(this)); this.xhr.open(this.method, this.url); for (var header in this.headers) { this.xhr.setRequestHeader(header, this.headers[header]); } this.xhr.send(this.payload); }; this.close = function() { if (this.readyState === this.CLOSED) { return; } this.xhr.abort(); this.xhr = null; this._setReadyState(this.CLOSED); }; }; // Export our SSE module for npm.js if (typeof exports !== 'undefined') { exports.SSE = SSE; }'use strict'; var subscribe_data = JSON.parse(document.getElementById('subscribe_data').textContent); var payload = 'csrf_token=' + subscribe_data.csrf_token; var subscribe_button = document.getElementById('subscribe'); subscribe_button.parentNode.action = 'javascript:void(0)'; if (subscribe_button.getAttribute('data-type') === 'subscribe') { subscribe_button.onclick = subscribe; } else { subscribe_button.onclick = unsubscribe; } function subscribe() { var fallback = subscribe_button.innerHTML; subscribe_button.onclick = unsubscribe; subscribe_button.innerHTML = '' + subscribe_data.unsubscribe_text + ' | ' + subscribe_data.sub_count_text + ''; var url = '/subscription_ajax?action_create_subscription_to_channel=1&redirect=false' + '&c=' + subscribe_data.ucid; helpers.xhr('POST', url, {payload: payload, retries: 5, entity_name: 'subscribe request'}, { onNon200: function (xhr) { subscribe_button.onclick = subscribe; subscribe_button.innerHTML = fallback; } }); } function unsubscribe() { var fallback = subscribe_button.innerHTML; subscribe_button.onclick = subscribe; subscribe_button.innerHTML = '' + subscribe_data.subscribe_text + ' | ' + subscribe_data.sub_count_text + ''; var url = '/subscription_ajax?action_remove_subscriptions=1&redirect=false' + '&c=' + subscribe_data.ucid; helpers.xhr('POST', url, {payload: payload, retries: 5, entity_name: 'unsubscribe request'}, { onNon200: function (xhr) { subscribe_button.onclick = unsubscribe; subscribe_button.innerHTML = fallback; } }); }'use strict'; var toggle_theme = document.getElementById('toggle_theme'); toggle_theme.href = 'javascript:void(0)'; const STORAGE_KEY_THEME = 'dark_mode'; const THEME_DARK = 'dark'; const THEME_LIGHT = 'light'; // TODO: theme state controlled by system toggle_theme.addEventListener('click', function () { const isDarkTheme = helpers.storage.get(STORAGE_KEY_THEME) === THEME_DARK; const newTheme = isDarkTheme ? THEME_LIGHT : THEME_DARK; setTheme(newTheme); helpers.storage.set(STORAGE_KEY_THEME, newTheme); helpers.xhr('GET', '/toggle_theme?redirect=false', {}, {}); }); /** @param {THEME_DARK|THEME_LIGHT} theme */ function setTheme(theme) { // By default body element has .no-theme class that uses OS theme via CSS @media rules // It rewrites using hard className below if (theme === THEME_DARK) { toggle_theme.children[0].className = 'icon ion-ios-sunny'; document.body.className = 'dark-theme'; } else if (theme === THEME_LIGHT) { toggle_theme.children[0].className = 'icon ion-ios-moon'; document.body.className = 'light-theme'; } else { document.body.className = 'no-theme'; } } // Handles theme change event caused by other tab addEventListener('storage', function (e) { if (e.key === STORAGE_KEY_THEME) setTheme(helpers.storage.get(STORAGE_KEY_THEME)); }); // Set theme from preferences on page load addEventListener('DOMContentLoaded', function () { const prefTheme = document.getElementById('dark_mode_pref').textContent; if (prefTheme) { setTheme(prefTheme); helpers.storage.set(STORAGE_KEY_THEME, prefTheme); } });/*! @name videojs-contrib-quality-levels @version 2.1.0 @license Apache-2.0 */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('video.js'), require('global/document')) : typeof define === 'function' && define.amd ? define(['video.js', 'global/document'], factory) : (global.videojsContribQualityLevels = factory(global.videojs,global.document)); }(this, (function (videojs,document) { 'use strict'; videojs = videojs && videojs.hasOwnProperty('default') ? videojs['default'] : videojs; document = document && document.hasOwnProperty('default') ? document['default'] : document; function _inheritsLoose(subClass, superClass) { subClass.prototype = Object.create(superClass.prototype); subClass.prototype.constructor = subClass; subClass.__proto__ = superClass; } function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } /** * A single QualityLevel. * * interface QualityLevel { * readonly attribute DOMString id; * attribute DOMString label; * readonly attribute long width; * readonly attribute long height; * readonly attribute long bitrate; * attribute boolean enabled; * }; * * @class QualityLevel */ var QualityLevel = /** * Creates a QualityLevel * * @param {Representation|Object} representation The representation of the quality level * @param {string} representation.id Unique id of the QualityLevel * @param {number=} representation.width Resolution width of the QualityLevel * @param {number=} representation.height Resolution height of the QualityLevel * @param {number} representation.bandwidth Bitrate of the QualityLevel * @param {Function} representation.enabled Callback to enable/disable QualityLevel */ function QualityLevel(representation) { var level = this; // eslint-disable-line if (videojs.browser.IS_IE8) { level = document.createElement('custom'); for (var prop in QualityLevel.prototype) { if (prop !== 'constructor') { level[prop] = QualityLevel.prototype[prop]; } } } level.id = representation.id; level.label = level.id; level.width = representation.width; level.height = representation.height; level.bitrate = representation.bandwidth; level.enabled_ = representation.enabled; Object.defineProperty(level, 'enabled', { /** * Get whether the QualityLevel is enabled. * * @return {boolean} True if the QualityLevel is enabled. */ get: function get() { return level.enabled_(); }, /** * Enable or disable the QualityLevel. * * @param {boolean} enable true to enable QualityLevel, false to disable. */ set: function set(enable) { level.enabled_(enable); } }); return level; }; /** * A list of QualityLevels. * * interface QualityLevelList : EventTarget { * getter QualityLevel (unsigned long index); * readonly attribute unsigned long length; * readonly attribute long selectedIndex; * * void addQualityLevel(QualityLevel qualityLevel) * void removeQualityLevel(QualityLevel remove) * QualityLevel? getQualityLevelById(DOMString id); * * attribute EventHandler onchange; * attribute EventHandler onaddqualitylevel; * attribute EventHandler onremovequalitylevel; * }; * * @extends videojs.EventTarget * @class QualityLevelList */ var QualityLevelList = /*#__PURE__*/ function (_videojs$EventTarget) { _inheritsLoose(QualityLevelList, _videojs$EventTarget); function QualityLevelList() { var _this; _this = _videojs$EventTarget.call(this) || this; var list = _assertThisInitialized(_assertThisInitialized(_this)); // eslint-disable-line if (videojs.browser.IS_IE8) { list = document.createElement('custom'); for (var prop in QualityLevelList.prototype) { if (prop !== 'constructor') { list[prop] = QualityLevelList.prototype[prop]; } } } list.levels_ = []; list.selectedIndex_ = -1; /** * Get the index of the currently selected QualityLevel. * * @returns {number} The index of the selected QualityLevel. -1 if none selected. * @readonly */ Object.defineProperty(list, 'selectedIndex', { get: function get() { return list.selectedIndex_; } }); /** * Get the length of the list of QualityLevels. * * @returns {number} The length of the list. * @readonly */ Object.defineProperty(list, 'length', { get: function get() { return list.levels_.length; } }); return list || _assertThisInitialized(_this); } /** * Adds a quality level to the list. * * @param {Representation|Object} representation The representation of the quality level * @param {string} representation.id Unique id of the QualityLevel * @param {number=} representation.width Resolution width of the QualityLevel * @param {number=} representation.height Resolution height of the QualityLevel * @param {number} representation.bandwidth Bitrate of the QualityLevel * @param {Function} representation.enabled Callback to enable/disable QualityLevel * @return {QualityLevel} the QualityLevel added to the list * @method addQualityLevel */ var _proto = QualityLevelList.prototype; _proto.addQualityLevel = function addQualityLevel(representation) { var qualityLevel = this.getQualityLevelById(representation.id); // Do not add duplicate quality levels if (qualityLevel) { return qualityLevel; } var index = this.levels_.length; qualityLevel = new QualityLevel(representation); if (!('' + index in this)) { Object.defineProperty(this, index, { get: function get() { return this.levels_[index]; } }); } this.levels_.push(qualityLevel); this.trigger({ qualityLevel: qualityLevel, type: 'addqualitylevel' }); return qualityLevel; }; /** * Removes a quality level from the list. * * @param {QualityLevel} remove QualityLevel to remove to the list. * @return {QualityLevel|null} the QualityLevel removed or null if nothing removed * @method removeQualityLevel */ _proto.removeQualityLevel = function removeQualityLevel(qualityLevel) { var removed = null; for (var i = 0, l = this.length; i < l; i++) { if (this[i] === qualityLevel) { removed = this.levels_.splice(i, 1)[0]; if (this.selectedIndex_ === i) { this.selectedIndex_ = -1; } else if (this.selectedIndex_ > i) { this.selectedIndex_--; } break; } } if (removed) { this.trigger({ qualityLevel: qualityLevel, type: 'removequalitylevel' }); } return removed; }; /** * Searches for a QualityLevel with the given id. * * @param {string} id The id of the QualityLevel to find. * @return {QualityLevel|null} The QualityLevel with id, or null if not found. * @method getQualityLevelById */ _proto.getQualityLevelById = function getQualityLevelById(id) { for (var i = 0, l = this.length; i < l; i++) { var level = this[i]; if (level.id === id) { return level; } } return null; }; /** * Resets the list of QualityLevels to empty * * @method dispose */ _proto.dispose = function dispose() { this.selectedIndex_ = -1; this.levels_.length = 0; }; return QualityLevelList; }(videojs.EventTarget); /** * change - The selected QualityLevel has changed. * addqualitylevel - A QualityLevel has been added to the QualityLevelList. * removequalitylevel - A QualityLevel has been removed from the QualityLevelList. */ QualityLevelList.prototype.allowedEvents_ = { change: 'change', addqualitylevel: 'addqualitylevel', removequalitylevel: 'removequalitylevel' }; // emulate attribute EventHandler support to allow for feature detection for (var event in QualityLevelList.prototype.allowedEvents_) { QualityLevelList.prototype['on' + event] = null; } var version = "2.1.0"; var registerPlugin = videojs.registerPlugin || videojs.plugin; /** * Initialization function for the qualityLevels plugin. Sets up the QualityLevelList and * event handlers. * * @param {Player} player Player object. * @param {Object} options Plugin options object. * @function initPlugin */ var initPlugin = function initPlugin(player, options) { var originalPluginFn = player.qualityLevels; var qualityLevelList = new QualityLevelList(); var disposeHandler = function disposeHandler() { qualityLevelList.dispose(); player.qualityLevels = originalPluginFn; player.off('dispose', disposeHandler); }; player.on('dispose', disposeHandler); player.qualityLevels = function () { return qualityLevelList; }; player.qualityLevels.VERSION = version; return qualityLevelList; }; /** * A video.js plugin. * * In the plugin function, the value of `this` is a video.js `Player` * instance. You cannot rely on the player being in a "ready" state here, * depending on how the plugin is invoked. This may or may not be important * to you; if not, remove the wait for "ready"! * * @param {Object} options Plugin options object * @function qualityLevels */ var qualityLevels = function qualityLevels(options) { return initPlugin(this, videojs.mergeOptions({}, options)); }; // Register the plugin with video.js. registerPlugin('qualityLevels', qualityLevels); // Include the version number. qualityLevels.VERSION = version; return qualityLevels; })));}))); /*! @name videojs-mobile-ui @version 0.6.1 @license MIT */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('video.js'), require('global/window')) : typeof define === 'function' && define.amd ? define(['video.js', 'global/window'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.videojsMobileUi = factory(global.videojs, global.window)); }(this, (function (videojs, window) { 'use strict'; function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var videojs__default = /*#__PURE__*/_interopDefaultLegacy(videojs); var window__default = /*#__PURE__*/_interopDefaultLegacy(window); var version = "0.6.1"; function createCommonjsModule(fn, basedir, module) { return module = { path: basedir, exports: {}, require: function (path, base) { return commonjsRequire(path, (base === undefined || base === null) ? module.path : base); } }, fn(module, module.exports), module.exports; } function commonjsRequire () { throw new Error('Dynamic requires are not currently supported by @rollup/plugin-commonjs'); } var setPrototypeOf = createCommonjsModule(function (module) { function _setPrototypeOf(o, p) { module.exports = _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; module.exports["default"] = module.exports, module.exports.__esModule = true; return _setPrototypeOf(o, p); } module.exports = _setPrototypeOf; module.exports["default"] = module.exports, module.exports.__esModule = true; }); var inheritsLoose = createCommonjsModule(function (module) { function _inheritsLoose(subClass, superClass) { subClass.prototype = Object.create(superClass.prototype); subClass.prototype.constructor = subClass; setPrototypeOf(subClass, superClass); } module.exports = _inheritsLoose; module.exports["default"] = module.exports, module.exports.__esModule = true; }); var Component = videojs__default['default'].getComponent('Component'); var dom = videojs__default['default'].dom || videojs__default['default']; /** * The `TouchOverlay` is an overlay to capture tap events. * * @extends Component */ var TouchOverlay = /*#__PURE__*/function (_Component) { inheritsLoose(TouchOverlay, _Component); /** * Creates an instance of the this class. * * @param {Player} player * The `Player` that this class should be attached to. * * @param {Object} [options] * The key/value store of player options. */ function TouchOverlay(player, options) { var _this; _this = _Component.call(this, player, options) || this; _this.seekSeconds = options.seekSeconds; _this.tapTimeout = options.tapTimeout; // Add play toggle overlay _this.addChild('playToggle', {}); // Clear overlay when playback starts or with control fade player.on(['playing', 'userinactive'], function (e) { _this.removeClass('show-play-toggle'); }); // A 0 inactivity timeout won't work here if (_this.player_.options_.inactivityTimeout === 0) { _this.player_.options_.inactivityTimeout = 5000; } _this.enable(); return _this; } /** * Builds the DOM element. * * @return {Element} * The DOM element. */ var _proto = TouchOverlay.prototype; _proto.createEl = function createEl() { var el = dom.createEl('div', { className: 'vjs-touch-overlay', // Touch overlay is not tabbable. tabIndex: -1 }); return el; } /** * Debounces to either handle a delayed single tap, or a double tap * * @param {Event} event * The touch event * */ ; _proto.handleTap = function handleTap(event) { var _this2 = this; // Don't handle taps on the play button if (event.target !== this.el_) { return; } event.preventDefault(); if (this.firstTapCaptured) { this.firstTapCaptured = false; if (this.timeout) { window__default['default'].clearTimeout(this.timeout); } this.handleDoubleTap(event); } else { this.firstTapCaptured = true; this.timeout = window__default['default'].setTimeout(function () { _this2.firstTapCaptured = false; _this2.handleSingleTap(event); }, this.tapTimeout); } } /** * Toggles display of play toggle * * @param {Event} event * The touch event * */ ; _proto.handleSingleTap = function handleSingleTap(event) { this.removeClass('skip'); this.toggleClass('show-play-toggle'); } /** * Seeks by configured number of seconds if left or right part of video double tapped * * @param {Event} event * The touch event * */ ; _proto.handleDoubleTap = function handleDoubleTap(event) { var _this3 = this; var rect = this.el_.getBoundingClientRect(); var x = event.changedTouches[0].clientX - rect.left; // Check if double tap is in left or right area if (x < rect.width * 0.4) { this.player_.currentTime(Math.max(0, this.player_.currentTime() - this.seekSeconds)); this.addClass('reverse'); } else if (x > rect.width - rect.width * 0.4) { this.player_.currentTime(Math.min(this.player_.duration(), this.player_.currentTime() + this.seekSeconds)); this.removeClass('reverse'); } else { return; } // Remove play toggle if showing this.removeClass('show-play-toggle'); // Remove and readd class to trigger animation this.removeClass('skip'); window__default['default'].requestAnimationFrame(function () { _this3.addClass('skip'); }); } /** * Enables touch handler */ ; _proto.enable = function enable() { this.firstTapCaptured = false; this.on('touchend', this.handleTap); } /** * Disables touch handler */ ; _proto.disable = function disable() { this.off('touchend', this.handleTap); }; return TouchOverlay; }(Component); Component.registerComponent('TouchOverlay', TouchOverlay); var defaults = { fullscreen: { enterOnRotate: true, exitOnRotate: true, lockOnRotate: true, iOS: false }, touchControls: { seekSeconds: 10, tapTimeout: 300, disableOnEnd: false } }; var screen = window__default['default'].screen; /** * Gets 'portrait' or 'lanscape' from the two orientation APIs * * @return {string} orientation */ var getOrientation = function getOrientation() { if (screen) { // Prefer the string over angle, as 0° can be landscape on some tablets var orientationString = (screen.orientation || {}).type || screen.mozOrientation || screen.msOrientation || ''.split('-')[0]; if (orientationString === 'landscape' || orientationString === 'portrait') { return orientationString; } } // iOS only supports window.orientation if (typeof window__default['default'].orientation === 'number') { if (window__default['default'].orientation === 0 || window__default['default'].orientation === 180) { return 'portrait'; } return 'landscape'; } return 'portrait'; }; // Cross-compatibility for Video.js 5 and 6. var registerPlugin = videojs__default['default'].registerPlugin || videojs__default['default'].plugin; /** * Add UI and event listeners * * @function onPlayerReady * @param {Player} player * A Video.js player object. * * @param {Object} [options={}] * A plain object containing options for the plugin. */ var onPlayerReady = function onPlayerReady(player, options) { player.addClass('vjs-mobile-ui'); if (options.touchControls.disableOnEnd || typeof player.endscreen === 'function') { player.addClass('vjs-mobile-ui-disable-end'); } if (options.fullscreen.iOS && videojs__default['default'].browser.IS_IOS && videojs__default['default'].browser.IOS_VERSION > 9 && !player.el_.ownerDocument.querySelector('.bc-iframe')) { player.tech_.el_.setAttribute('playsinline', 'playsinline'); player.tech_.supportsFullScreen = function () { return false; }; } // Insert before the control bar var controlBarIdx; var versionParts = videojs__default['default'].VERSION.split('.'); var major = parseInt(versionParts[0], 10); var minor = parseInt(versionParts[1], 10); // Video.js < 7.7.0 doesn't account for precedding components that don't have elements if (major < 7 || major === 7 && minor < 7) { controlBarIdx = Array.prototype.indexOf.call(player.el_.children, player.getChild('ControlBar').el_); } else { controlBarIdx = player.children_.indexOf(player.getChild('ControlBar')); } player.addChild('TouchOverlay', options.touchControls, controlBarIdx); var locked = false; var rotationHandler = function rotationHandler() { var currentOrientation = getOrientation(); if (currentOrientation === 'landscape' && options.fullscreen.enterOnRotate) { if (player.paused() === false) { player.requestFullscreen(); if (options.fullscreen.lockOnRotate && screen.orientation && screen.orientation.lock) { screen.orientation.lock('landscape').then(function () { locked = true; }).catch(function (e) { videojs__default['default'].log('Browser refused orientation lock:', e); }); } } } else if (currentOrientation === 'portrait' && options.fullscreen.exitOnRotate && !locked) { if (player.isFullscreen()) { player.exitFullscreen(); } } }; if (options.fullscreen.enterOnRotate || options.fullscreen.exitOnRotate) { if (videojs__default['default'].browser.IS_IOS) { window__default['default'].addEventListener('orientationchange', rotationHandler); player.on('dispose', function () { window__default['default'].removeEventListener('orientationchange', rotationHandler); }); } else if (screen.orientation) { // addEventListener('orientationchange') is not a user interaction on Android screen.orientation.onchange = rotationHandler; player.on('dispose', function () { screen.orientation.onchange = null; }); } } player.on('ended', function (_) { if (locked === true) { screen.orientation.unlock(); locked = false; } }); player.on('fullscreenchange', function (_) { if (!player.isFullscreen() && locked) { screen.orientation.unlock(); locked = false; } }); }; /** * A video.js plugin. * * Adds a monile UI for player control, and fullscreen orientation control * * @function mobileUi * @param {Object} [options={}] * Plugin options. * @param {boolean} [options.forceForTesting=false] * Enables the display regardless of user agent, for testing purposes * @param {Object} [options.fullscreen={}] * Fullscreen options. * @param {boolean} [options.fullscreen.enterOnRotate=true] * Whether to go fullscreen when rotating to landscape * @param {boolean} [options.fullscreen.exitOnRotate=true] * Whether to leave fullscreen when rotating to portrait (if not locked) * @param {boolean} [options.fullscreen.lockOnRotate=true] * Whether to lock orientation when rotating to landscape * Unlocked when exiting fullscreen or on 'ended' * @param {boolean} [options.fullscreen.iOS=false] * Whether to disable iOS's native fullscreen so controls can work * @param {Object} [options.touchControls={}] * Touch UI options. * @param {int} [options.touchControls.seekSeconds=10] * Number of seconds to seek on double-tap * @param {int} [options.touchControls.tapTimeout=300] * Interval in ms to be considered a doubletap * @param {boolean} [options.touchControls.disableOnEnd=false] * Whether to disable when the video ends (e.g., if there is an endscreen) * Never shows if the endscreen plugin is present */ var mobileUi = function mobileUi(options) { var _this = this; if (options === void 0) { options = {}; } if (options.forceForTesting || videojs__default['default'].browser.IS_ANDROID || videojs__default['default'].browser.IS_IOS) { this.ready(function () { onPlayerReady(_this, videojs__default['default'].mergeOptions(defaults, options)); }); } }; // Register the plugin with video.js. registerPlugin('mobileUi', mobileUi); // Include the version number. mobileUi.VERSION = version; return mobileUi; })));(function (global, factory) { if (typeof define === "function" && define.amd) { define(['video.js'], factory); } else if (typeof exports !== "undefined") { factory(require('video.js')); } else { var mod = { exports: {} }; factory(global.videojs); global.videojsMarkers = mod.exports; } })(this, function (_video) { /*! videojs-markers - v1.0.1 - 2018-02-03 * Copyright (c) 2018 ; Licensed */ 'use strict'; var _video2 = _interopRequireDefault(_video); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; // default setting var defaultSetting = { markerStyle: { 'width': '7px', 'border-radius': '30%', 'background-color': 'red' }, markerTip: { display: true, text: function text(marker) { return "Break: " + marker.text; }, time: function time(marker) { return marker.time; } }, breakOverlay: { display: false, displayTime: 3, text: function text(marker) { return "Break overlay: " + marker.overlayText; }, style: { 'width': '100%', 'height': '20%', 'background-color': 'rgba(0,0,0,0.7)', 'color': 'white', 'font-size': '17px' } }, onMarkerClick: function onMarkerClick(marker) {}, onMarkerReached: function onMarkerReached(marker, index) {}, markers: [] }; // create a non-colliding random number function generateUUID() { var d = new Date().getTime(); var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { var r = (d + Math.random() * 16) % 16 | 0; d = Math.floor(d / 16); return (c == 'x' ? r : r & 0x3 | 0x8).toString(16); }); return uuid; }; /** * Returns the size of an element and its position * a default Object with 0 on each of its properties * its return in case there's an error * @param {Element} element el to get the size and position * @return {DOMRect|Object} size and position of an element */ function getElementBounding(element) { var elementBounding; var defaultBoundingRect = { top: 0, bottom: 0, left: 0, width: 0, height: 0, right: 0 }; try { elementBounding = element.getBoundingClientRect(); } catch (e) { elementBounding = defaultBoundingRect; } return elementBounding; } var NULL_INDEX = -1; function registerVideoJsMarkersPlugin(options) { // copied from video.js/src/js/utils/merge-options.js since // videojs 4 doens't support it by defualt. if (!_video2.default.mergeOptions) { var isPlain = function isPlain(value) { return !!value && (typeof value === 'undefined' ? 'undefined' : _typeof(value)) === 'object' && toString.call(value) === '[object Object]' && value.constructor === Object; }; var mergeOptions = function mergeOptions(source1, source2) { var result = {}; var sources = [source1, source2]; sources.forEach(function (source) { if (!source) { return; } Object.keys(source).forEach(function (key) { var value = source[key]; if (!isPlain(value)) { result[key] = value; return; } if (!isPlain(result[key])) { result[key] = {}; } result[key] = mergeOptions(result[key], value); }); }); return result; }; _video2.default.mergeOptions = mergeOptions; } if (!_video2.default.createEl) { _video2.default.createEl = function (tagName, props, attrs) { var el = _video2.default.Player.prototype.createEl(tagName, props); if (!!attrs) { Object.keys(attrs).forEach(function (key) { el.setAttribute(key, attrs[key]); }); } return el; }; } /** * register the markers plugin (dependent on jquery) */ var setting = _video2.default.mergeOptions(defaultSetting, options), markersMap = {}, markersList = [], // list of markers sorted by time currentMarkerIndex = NULL_INDEX, player = this, markerTip = null, breakOverlay = null, overlayIndex = NULL_INDEX; function sortMarkersList() { // sort the list by time in asc order markersList.sort(function (a, b) { return setting.markerTip.time(a) - setting.markerTip.time(b); }); } function addMarkers(newMarkers) { newMarkers.forEach(function (marker) { marker.key = generateUUID(); player.el().querySelector('.vjs-progress-holder').appendChild(createMarkerDiv(marker)); // store marker in an internal hash map markersMap[marker.key] = marker; markersList.push(marker); }); sortMarkersList(); } function getPosition(marker) { return setting.markerTip.time(marker) / player.duration() * 100; } function setMarkderDivStyle(marker, markerDiv) { markerDiv.className = 'vjs-marker ' + (marker.class || ""); Object.keys(setting.markerStyle).forEach(function (key) { markerDiv.style[key] = setting.markerStyle[key]; }); // hide out-of-bound markers var ratio = marker.time / player.duration(); if (ratio < 0 || ratio > 1) { markerDiv.style.display = 'none'; } // set position markerDiv.style.left = getPosition(marker) + '%'; if (marker.duration) { markerDiv.style.width = marker.duration / player.duration() * 100 + '%'; markerDiv.style.marginLeft = '0px'; } else { var markerDivBounding = getElementBounding(markerDiv); markerDiv.style.marginLeft = markerDivBounding.width / 2 + 'px'; } } function createMarkerDiv(marker) { var markerDiv = _video2.default.createEl('div', {}, { 'data-marker-key': marker.key, 'data-marker-time': setting.markerTip.time(marker) }); setMarkderDivStyle(marker, markerDiv); // bind click event to seek to marker time markerDiv.addEventListener('click', function (e) { var preventDefault = false; if (typeof setting.onMarkerClick === "function") { // if return false, prevent default behavior preventDefault = setting.onMarkerClick(marker) === false; } if (!preventDefault) { var key = this.getAttribute('data-marker-key'); player.currentTime(setting.markerTip.time(markersMap[key])); } }); if (setting.markerTip.display) { registerMarkerTipHandler(markerDiv); } return markerDiv; } function updateMarkers(force) { // update UI for markers whose time changed markersList.forEach(function (marker) { var markerDiv = player.el().querySelector(".vjs-marker[data-marker-key='" + marker.key + "']"); var markerTime = setting.markerTip.time(marker); if (force || markerDiv.getAttribute('data-marker-time') !== markerTime) { setMarkderDivStyle(marker, markerDiv); markerDiv.setAttribute('data-marker-time', markerTime); } }); sortMarkersList(); } function removeMarkers(indexArray) { // reset overlay if (!!breakOverlay) { overlayIndex = NULL_INDEX; breakOverlay.style.visibility = "hidden"; } currentMarkerIndex = NULL_INDEX; var deleteIndexList = []; indexArray.forEach(function (index) { var marker = markersList[index]; if (marker) { // delete from memory delete markersMap[marker.key]; deleteIndexList.push(index); // delete from dom var el = player.el().querySelector(".vjs-marker[data-marker-key='" + marker.key + "']"); el && el.parentNode.removeChild(el); } }); // clean up markers array deleteIndexList.reverse(); deleteIndexList.forEach(function (deleteIndex) { markersList.splice(deleteIndex, 1); }); // sort again sortMarkersList(); } // attach hover event handler function registerMarkerTipHandler(markerDiv) { markerDiv.addEventListener('mouseover', function () { var marker = markersMap[markerDiv.getAttribute('data-marker-key')]; if (!!markerTip) { markerTip.querySelector('.vjs-tip-inner').innerText = setting.markerTip.text(marker); // margin-left needs to minus the padding length to align correctly with the marker markerTip.style.left = getPosition(marker) + '%'; var markerTipBounding = getElementBounding(markerTip); var markerDivBounding = getElementBounding(markerDiv); markerTip.style.marginLeft = -parseFloat(markerTipBounding.width / 2) + parseFloat(markerDivBounding.width / 4) + 'px'; markerTip.style.visibility = 'visible'; } }); markerDiv.addEventListener('mouseout', function () { if (!!markerTip) { markerTip.style.visibility = "hidden"; } }); } function initializeMarkerTip() { markerTip = _video2.default.createEl('div', { className: 'vjs-tip', innerHTML: "
" }); player.el().querySelector('.vjs-progress-holder').appendChild(markerTip); } // show or hide break overlays function updateBreakOverlay() { if (!setting.breakOverlay.display || currentMarkerIndex < 0) { return; } var currentTime = player.currentTime(); var marker = markersList[currentMarkerIndex]; var markerTime = setting.markerTip.time(marker); if (currentTime >= markerTime && currentTime <= markerTime + setting.breakOverlay.displayTime) { if (overlayIndex !== currentMarkerIndex) { overlayIndex = currentMarkerIndex; if (breakOverlay) { breakOverlay.querySelector('.vjs-break-overlay-text').innerHTML = setting.breakOverlay.text(marker); } } if (breakOverlay) { breakOverlay.style.visibility = "visible"; } } else { overlayIndex = NULL_INDEX; if (breakOverlay) { breakOverlay.style.visibility = "hidden"; } } } // problem when the next marker is within the overlay display time from the previous marker function initializeOverlay() { breakOverlay = _video2.default.createEl('div', { className: 'vjs-break-overlay', innerHTML: "
" }); Object.keys(setting.breakOverlay.style).forEach(function (key) { if (breakOverlay) { breakOverlay.style[key] = setting.breakOverlay.style[key]; } }); player.el().appendChild(breakOverlay); overlayIndex = NULL_INDEX; } function onTimeUpdate() { onUpdateMarker(); updateBreakOverlay(); options.onTimeUpdateAfterMarkerUpdate && options.onTimeUpdateAfterMarkerUpdate(); } function onUpdateMarker() { /* check marker reached in between markers the logic here is that it triggers a new marker reached event only if the player enters a new marker range (e.g. from marker 1 to marker 2). Thus, if player is on marker 1 and user clicked on marker 1 again, no new reached event is triggered) */ if (!markersList.length) { return; } var getNextMarkerTime = function getNextMarkerTime(index) { if (index < markersList.length - 1) { return setting.markerTip.time(markersList[index + 1]); } // next marker time of last marker would be end of video time return player.duration(); }; var currentTime = player.currentTime(); var newMarkerIndex = NULL_INDEX; if (currentMarkerIndex !== NULL_INDEX) { // check if staying at same marker var nextMarkerTime = getNextMarkerTime(currentMarkerIndex); if (currentTime >= setting.markerTip.time(markersList[currentMarkerIndex]) && currentTime < nextMarkerTime) { return; } // check for ending (at the end current time equals player duration) if (currentMarkerIndex === markersList.length - 1 && currentTime === player.duration()) { return; } } // check first marker, no marker is selected if (currentTime < setting.markerTip.time(markersList[0])) { newMarkerIndex = NULL_INDEX; } else { // look for new index for (var i = 0; i < markersList.length; i++) { nextMarkerTime = getNextMarkerTime(i); if (currentTime >= setting.markerTip.time(markersList[i]) && currentTime < nextMarkerTime) { newMarkerIndex = i; break; } } } // set new marker index if (newMarkerIndex !== currentMarkerIndex) { // trigger event if index is not null if (newMarkerIndex !== NULL_INDEX && options.onMarkerReached) { options.onMarkerReached(markersList[newMarkerIndex], newMarkerIndex); } currentMarkerIndex = newMarkerIndex; } } // setup the whole thing function initialize() { if (setting.markerTip.display) { initializeMarkerTip(); } // remove existing markers if already initialized player.markers.removeAll(); addMarkers(setting.markers); if (setting.breakOverlay.display) { initializeOverlay(); } onTimeUpdate(); player.on("timeupdate", onTimeUpdate); player.off("loadedmetadata"); } // setup the plugin after we loaded video's meta data player.on("loadedmetadata", function () { initialize(); }); // exposed plugin API player.markers = { getMarkers: function getMarkers() { return markersList; }, next: function next() { // go to the next marker from current timestamp var currentTime = player.currentTime(); for (var i = 0; i < markersList.length; i++) { var markerTime = setting.markerTip.time(markersList[i]); if (markerTime > currentTime) { player.currentTime(markerTime); break; } } }, prev: function prev() { // go to previous marker var currentTime = player.currentTime(); for (var i = markersList.length - 1; i >= 0; i--) { var markerTime = setting.markerTip.time(markersList[i]); // add a threshold if (markerTime + 0.5 < currentTime) { player.currentTime(markerTime); return; } } }, add: function add(newMarkers) { // add new markers given an array of index addMarkers(newMarkers); }, remove: function remove(indexArray) { // remove markers given an array of index removeMarkers(indexArray); }, removeAll: function removeAll() { var indexArray = []; for (var i = 0; i < markersList.length; i++) { indexArray.push(i); } removeMarkers(indexArray); }, // force - force all markers to be updated, regardless of if they have changed or not. updateTime: function updateTime(force) { // notify the plugin to update the UI for changes in marker times updateMarkers(force); }, reset: function reset(newMarkers) { // remove all the existing markers and add new ones player.markers.removeAll(); addMarkers(newMarkers); }, destroy: function destroy() { // unregister the plugins and clean up even handlers player.markers.removeAll(); breakOverlay && breakOverlay.remove(); markerTip && markerTip.remove(); player.off("timeupdate", updateBreakOverlay); delete player.markers; } }; } _video2.default.plugin('markers', registerVideoJsMarkersPlugin); }); //# sourceMappingURL=videojs-markers.js.map