From 5ecca27c9fa86201bb514eb270f4e4c6d4722175 Mon Sep 17 00:00:00 2001 From: Xaekai Date: Sat, 12 Feb 2022 17:31:40 -0800 Subject: [PATCH] Add vjs plugin for audio track switching --- templates/channel.pug | 1 + www/js/vjs/videojs-audio-switcher.js | 587 +++++++++++++++++++++++++++ 2 files changed, 588 insertions(+) create mode 100644 www/js/vjs/videojs-audio-switcher.js diff --git a/templates/channel.pug b/templates/channel.pug index 1ca54908..750a95aa 100644 --- a/templates/channel.pug +++ b/templates/channel.pug @@ -254,6 +254,7 @@ html(lang="en") script(defer, src="/js/vjs/videojs-dash.js") script(defer, src="/js/vjs/videojs-hlsjs-plugin.js") script(defer, src="/js/vjs/videojs-resolution-switcher.js") + script(defer, src="/js/vjs/videojs-audio-switcher.js") script(defer, src="/js/playerjs-0.0.12.js") script(defer, src="/js/niconico.js") script(defer, src="/js/peertube.js") diff --git a/www/js/vjs/videojs-audio-switcher.js b/www/js/vjs/videojs-audio-switcher.js new file mode 100644 index 00000000..6e9f6263 --- /dev/null +++ b/www/js/vjs/videojs-audio-switcher.js @@ -0,0 +1,587 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(require('video.js')) : typeof define === 'function' && define.amd ? define(['video.js'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.videojs)); +}(this, function (videojs) { + 'use strict'; + function _interopDefaultLegacy(e) { + return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; + } + var videojs__default = /*#__PURE__*/ + _interopDefaultLegacy(videojs); + /** + * Checks if `value` is the + * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types) + * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an object, else `false`. + * @example + * + * _.isObject({}); + * // => true + * + * _.isObject([1, 2, 3]); + * // => true + * + * _.isObject(_.noop); + * // => true + * + * _.isObject(null); + * // => false + */ + function isObject(value) { + var type = typeof value; + return value != null && (type == 'object' || type == 'function'); + } + /** Detect free variable `global` from Node.js. */ + var freeGlobal = typeof global == 'object' && global && global.Object === Object && global; + /** Detect free variable `self`. */ + var freeSelf = typeof self == 'object' && self && self.Object === Object && self; + /** Used as a reference to the global object. */ + var root = freeGlobal || freeSelf || Function('return this')(); + /** + * Gets the timestamp of the number of milliseconds that have elapsed since + * the Unix epoch (1 January 1970 00:00:00 UTC). + * + * @static + * @memberOf _ + * @since 2.4.0 + * @category Date + * @returns {number} Returns the timestamp. + * @example + * + * _.defer(function(stamp) { + * console.log(_.now() - stamp); + * }, _.now()); + * // => Logs the number of milliseconds it took for the deferred invocation. + */ + var now = function () { + return root.Date.now(); + }; + /** Used to match a single whitespace character. */ + var reWhitespace = /\s/; + /** + * Used by `_.trim` and `_.trimEnd` to get the index of the last non-whitespace + * character of `string`. + * + * @private + * @param {string} string The string to inspect. + * @returns {number} Returns the index of the last non-whitespace character. + */ + function trimmedEndIndex(string) { + var index = string.length; + while (index-- && reWhitespace.test(string.charAt(index))) { + } + return index; + } + /** Used to match leading whitespace. */ + var reTrimStart = /^\s+/; + /** + * The base implementation of `_.trim`. + * + * @private + * @param {string} string The string to trim. + * @returns {string} Returns the trimmed string. + */ + function baseTrim(string) { + return string ? string.slice(0, trimmedEndIndex(string) + 1).replace(reTrimStart, '') : string; + } + /** Built-in value references. */ + var Symbol = root.Symbol; + /** Used for built-in method references. */ + var objectProto$1 = Object.prototype; + /** Used to check objects for own properties. */ + var hasOwnProperty = objectProto$1.hasOwnProperty; + /** + * Used to resolve the + * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) + * of values. + */ + var nativeObjectToString$1 = objectProto$1.toString; + /** Built-in value references. */ + var symToStringTag$1 = Symbol ? Symbol.toStringTag : undefined; + /** + * A specialized version of `baseGetTag` which ignores `Symbol.toStringTag` values. + * + * @private + * @param {*} value The value to query. + * @returns {string} Returns the raw `toStringTag`. + */ + function getRawTag(value) { + var isOwn = hasOwnProperty.call(value, symToStringTag$1), tag = value[symToStringTag$1]; + try { + value[symToStringTag$1] = undefined; + var unmasked = true; + } catch (e) { + } + var result = nativeObjectToString$1.call(value); + if (unmasked) { + if (isOwn) { + value[symToStringTag$1] = tag; + } else { + delete value[symToStringTag$1]; + } + } + return result; + } + /** Used for built-in method references. */ + var objectProto = Object.prototype; + /** + * Used to resolve the + * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) + * of values. + */ + var nativeObjectToString = objectProto.toString; + /** + * Converts `value` to a string using `Object.prototype.toString`. + * + * @private + * @param {*} value The value to convert. + * @returns {string} Returns the converted string. + */ + function objectToString(value) { + return nativeObjectToString.call(value); + } + /** `Object#toString` result references. */ + var nullTag = '[object Null]', undefinedTag = '[object Undefined]'; + /** Built-in value references. */ + var symToStringTag = Symbol ? Symbol.toStringTag : undefined; + /** + * The base implementation of `getTag` without fallbacks for buggy environments. + * + * @private + * @param {*} value The value to query. + * @returns {string} Returns the `toStringTag`. + */ + function baseGetTag(value) { + if (value == null) { + return value === undefined ? undefinedTag : nullTag; + } + return symToStringTag && symToStringTag in Object(value) ? getRawTag(value) : objectToString(value); + } + /** + * Checks if `value` is object-like. A value is object-like if it's not `null` + * and has a `typeof` result of "object". + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is object-like, else `false`. + * @example + * + * _.isObjectLike({}); + * // => true + * + * _.isObjectLike([1, 2, 3]); + * // => true + * + * _.isObjectLike(_.noop); + * // => false + * + * _.isObjectLike(null); + * // => false + */ + function isObjectLike(value) { + return value != null && typeof value == 'object'; + } + /** `Object#toString` result references. */ + var symbolTag = '[object Symbol]'; + /** + * Checks if `value` is classified as a `Symbol` primitive or object. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a symbol, else `false`. + * @example + * + * _.isSymbol(Symbol.iterator); + * // => true + * + * _.isSymbol('abc'); + * // => false + */ + function isSymbol(value) { + return typeof value == 'symbol' || isObjectLike(value) && baseGetTag(value) == symbolTag; + } + /** Used as references for various `Number` constants. */ + var NAN = 0 / 0; + /** Used to detect bad signed hexadecimal string values. */ + var reIsBadHex = /^[-+]0x[0-9a-f]+$/i; + /** Used to detect binary string values. */ + var reIsBinary = /^0b[01]+$/i; + /** Used to detect octal string values. */ + var reIsOctal = /^0o[0-7]+$/i; + /** Built-in method references without a dependency on `root`. */ + var freeParseInt = parseInt; + /** + * Converts `value` to a number. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to process. + * @returns {number} Returns the number. + * @example + * + * _.toNumber(3.2); + * // => 3.2 + * + * _.toNumber(Number.MIN_VALUE); + * // => 5e-324 + * + * _.toNumber(Infinity); + * // => Infinity + * + * _.toNumber('3.2'); + * // => 3.2 + */ + function toNumber(value) { + if (typeof value == 'number') { + return value; + } + if (isSymbol(value)) { + return NAN; + } + if (isObject(value)) { + var other = typeof value.valueOf == 'function' ? value.valueOf() : value; + value = isObject(other) ? other + '' : other; + } + if (typeof value != 'string') { + return value === 0 ? value : +value; + } + value = baseTrim(value); + var isBinary = reIsBinary.test(value); + return isBinary || reIsOctal.test(value) ? freeParseInt(value.slice(2), isBinary ? 2 : 8) : reIsBadHex.test(value) ? NAN : +value; + } + /** Error message constants. */ + var FUNC_ERROR_TEXT$1 = 'Expected a function'; + /* Built-in method references for those with the same name as other `lodash` methods. */ + var nativeMax = Math.max, nativeMin = Math.min; + /** + * Creates a debounced function that delays invoking `func` until after `wait` + * milliseconds have elapsed since the last time the debounced function was + * invoked. The debounced function comes with a `cancel` method to cancel + * delayed `func` invocations and a `flush` method to immediately invoke them. + * Provide `options` to indicate whether `func` should be invoked on the + * leading and/or trailing edge of the `wait` timeout. The `func` is invoked + * with the last arguments provided to the debounced function. Subsequent + * calls to the debounced function return the result of the last `func` + * invocation. + * + * **Note:** If `leading` and `trailing` options are `true`, `func` is + * invoked on the trailing edge of the timeout only if the debounced function + * is invoked more than once during the `wait` timeout. + * + * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred + * until to the next tick, similar to `setTimeout` with a timeout of `0`. + * + * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) + * for details over the differences between `_.debounce` and `_.throttle`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Function + * @param {Function} func The function to debounce. + * @param {number} [wait=0] The number of milliseconds to delay. + * @param {Object} [options={}] The options object. + * @param {boolean} [options.leading=false] + * Specify invoking on the leading edge of the timeout. + * @param {number} [options.maxWait] + * The maximum time `func` is allowed to be delayed before it's invoked. + * @param {boolean} [options.trailing=true] + * Specify invoking on the trailing edge of the timeout. + * @returns {Function} Returns the new debounced function. + * @example + * + * // Avoid costly calculations while the window size is in flux. + * jQuery(window).on('resize', _.debounce(calculateLayout, 150)); + * + * // Invoke `sendMail` when clicked, debouncing subsequent calls. + * jQuery(element).on('click', _.debounce(sendMail, 300, { + * 'leading': true, + * 'trailing': false + * })); + * + * // Ensure `batchLog` is invoked once after 1 second of debounced calls. + * var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 }); + * var source = new EventSource('/stream'); + * jQuery(source).on('message', debounced); + * + * // Cancel the trailing debounced invocation. + * jQuery(window).on('popstate', debounced.cancel); + */ + function debounce(func, wait, options) { + var lastArgs, lastThis, maxWait, result, timerId, lastCallTime, lastInvokeTime = 0, leading = false, maxing = false, trailing = true; + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT$1); + } + wait = toNumber(wait) || 0; + if (isObject(options)) { + leading = !!options.leading; + maxing = 'maxWait' in options; + maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait; + trailing = 'trailing' in options ? !!options.trailing : trailing; + } + function invokeFunc(time) { + var args = lastArgs, thisArg = lastThis; + lastArgs = lastThis = undefined; + lastInvokeTime = time; + result = func.apply(thisArg, args); + return result; + } + function leadingEdge(time) { + // Reset any `maxWait` timer. + lastInvokeTime = time; + // Start the timer for the trailing edge. + timerId = setTimeout(timerExpired, wait); + // Invoke the leading edge. + return leading ? invokeFunc(time) : result; + } + function remainingWait(time) { + var timeSinceLastCall = time - lastCallTime, timeSinceLastInvoke = time - lastInvokeTime, timeWaiting = wait - timeSinceLastCall; + return maxing ? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke) : timeWaiting; + } + function shouldInvoke(time) { + var timeSinceLastCall = time - lastCallTime, timeSinceLastInvoke = time - lastInvokeTime; + // Either this is the first call, activity has stopped and we're at the + // trailing edge, the system time has gone backwards and we're treating + // it as the trailing edge, or we've hit the `maxWait` limit. + return lastCallTime === undefined || timeSinceLastCall >= wait || timeSinceLastCall < 0 || maxing && timeSinceLastInvoke >= maxWait; + } + function timerExpired() { + var time = now(); + if (shouldInvoke(time)) { + return trailingEdge(time); + } + // Restart the timer. + timerId = setTimeout(timerExpired, remainingWait(time)); + } + function trailingEdge(time) { + timerId = undefined; + // Only invoke if we have `lastArgs` which means `func` has been + // debounced at least once. + if (trailing && lastArgs) { + return invokeFunc(time); + } + lastArgs = lastThis = undefined; + return result; + } + function cancel() { + if (timerId !== undefined) { + clearTimeout(timerId); + } + lastInvokeTime = 0; + lastArgs = lastCallTime = lastThis = timerId = undefined; + } + function flush() { + return timerId === undefined ? result : trailingEdge(now()); + } + function debounced() { + var time = now(), isInvoking = shouldInvoke(time); + lastArgs = arguments; + lastThis = this; + lastCallTime = time; + if (isInvoking) { + if (timerId === undefined) { + return leadingEdge(lastCallTime); + } + if (maxing) { + // Handle invocations in a tight loop. + clearTimeout(timerId); + timerId = setTimeout(timerExpired, wait); + return invokeFunc(lastCallTime); + } + } + if (timerId === undefined) { + timerId = setTimeout(timerExpired, wait); + } + return result; + } + debounced.cancel = cancel; + debounced.flush = flush; + return debounced; + } + /** Error message constants. */ + var FUNC_ERROR_TEXT = 'Expected a function'; + /** + * Creates a throttled function that only invokes `func` at most once per + * every `wait` milliseconds. The throttled function comes with a `cancel` + * method to cancel delayed `func` invocations and a `flush` method to + * immediately invoke them. Provide `options` to indicate whether `func` + * should be invoked on the leading and/or trailing edge of the `wait` + * timeout. The `func` is invoked with the last arguments provided to the + * throttled function. Subsequent calls to the throttled function return the + * result of the last `func` invocation. + * + * **Note:** If `leading` and `trailing` options are `true`, `func` is + * invoked on the trailing edge of the timeout only if the throttled function + * is invoked more than once during the `wait` timeout. + * + * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred + * until to the next tick, similar to `setTimeout` with a timeout of `0`. + * + * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) + * for details over the differences between `_.throttle` and `_.debounce`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Function + * @param {Function} func The function to throttle. + * @param {number} [wait=0] The number of milliseconds to throttle invocations to. + * @param {Object} [options={}] The options object. + * @param {boolean} [options.leading=true] + * Specify invoking on the leading edge of the timeout. + * @param {boolean} [options.trailing=true] + * Specify invoking on the trailing edge of the timeout. + * @returns {Function} Returns the new throttled function. + * @example + * + * // Avoid excessively updating the position while scrolling. + * jQuery(window).on('scroll', _.throttle(updatePosition, 100)); + * + * // Invoke `renewToken` when the click event is fired, but not more than once every 5 minutes. + * var throttled = _.throttle(renewToken, 300000, { 'trailing': false }); + * jQuery(element).on('click', throttled); + * + * // Cancel the trailing throttled invocation. + * jQuery(window).on('popstate', throttled.cancel); + */ + function throttle(func, wait, options) { + var leading = true, trailing = true; + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + if (isObject(options)) { + leading = 'leading' in options ? !!options.leading : leading; + trailing = 'trailing' in options ? !!options.trailing : trailing; + } + return debounce(func, wait, { + 'leading': leading, + 'maxWait': wait, + 'trailing': trailing + }); + } + const syncTime = (player, audio) => { + const time = player.currentTime(); + audio.currentTime = time; + }; + function audioSwitchPlugin(options) { + const {audioElement, audioTracks, debugInterval, syncInterval, volume} = options; + const player = this; + const checkAudioElement = () => { + const videoElement = player.el_; + let newAudio; + if (typeof audioElement !== 'undefined' && audioElement instanceof HTMLElement) { + newAudio = audioElement; + this.audio = newAudio; + } else { + newAudio = document.createElement('audio'); + this.audio = newAudio; + this.isOurAudio = true; + this.audio.className = 'audioSwitch-audio'; + this.audioParent = document.createElement('div'); + this.audioParent.className = 'audioSwitch-parent'; + this.audioParent.appendChild(this.audio); + if (videoElement.nextSibling) { + videoElement.parentNode.insertBefore(this.audioParent, videoElement.nextSibling); + } else { + videoElement.parentNode.appendChild(this.audioParent); + } + } + return newAudio; + }; + const audio = checkAudioElement(); + audio.currentTime = 0; + audio.volume = volume; + const onAudioTracksChange = (player, audio, event) => { + var audioTrackList = player.audioTracks(); + let enabledTrack; + for (let i = 0; i < audioTrackList.length; i++) { + let track = audioTrackList[i]; + if (track.enabled) { + enabledTrack = track; + break; + } + } + enabledTrack = enabledTrack || audioTrackList[0]; + if (enabledTrack) { + const isPlaying = !player.paused(); + if (isPlaying) + player.pause(); + audio.setAttribute('src', audioTracks.find(audioTrack => audioTrack.label === enabledTrack.label)?.url); + syncTime(player, audio); + if (isPlaying) + player.play(); + } + }; + var audioTrackList = player.audioTracks(); + audioTrackList.addEventListener('change', onAudioTracksChange.bind(null, player, audio)); + if (audioTracks.length > 0) { + audioTracks[0].kind = 'main'; + audioTracks[0].enabled = true; + audioTracks.forEach(track => audioTrackList.addTrack(new videojs.AudioTrack(track))); + audio.setAttribute('src', audioTracks[0].url); + } + player.on('play', () => { + syncTime(player, audio); + if (audio.paused) + audio.play(); + }); + player.on('pause', () => { + syncTime(player, audio); + if (!audio.paused) + audio.pause(); + }); + player.on('seeked', () => { + if (!player.paused()) + player.pause(); + player.one('canplay', () => { + const sync = () => { + syncTime(player, audio); + audio.removeEventListener('canplay', sync); + if (player.paused()) + player.play(); + }; + audio.addEventListener('canplay', sync); + }); + }); + player.on('volumechange', () => { + if (player.muted()) { + audio.muted = true; + } else { + audio.muted = false; + audio.volume = player.volume(); + } + }); + if (syncInterval) { + const syncOnInterval = throttle(() => { + syncTime(player, audio); + }, syncInterval); + player.on('timeupdate', syncOnInterval); + } + if (debugInterval) { + const debugOnInterval = throttle(() => { + const _audioBefore = audio.currentTime; + const _videoBefore = player.currentTime(); + console.log('debug', { + audio: _audioBefore, + video: _videoBefore, + diff: _videoBefore - _audioBefore + }); + }, debugInterval); + player.on('timeupdate', debugOnInterval); + } + } + videojs__default['default'].registerPlugin('audioSwitch', audioSwitchPlugin); +})); \ No newline at end of file