From 6ed6d1c28b41065d342742bc16d7eaca6eb25c3f Mon Sep 17 00:00:00 2001 From: Trek H Date: Fri, 24 Jan 2020 14:10:42 +1030 Subject: [PATCH] mjpeg-player: removed files from branch that are changed in existing PRs --- cmd/mjpeg-player/eventemitter3/index.js | 331 --------------- cmd/mjpeg-player/hlsjs/config.js | 101 ----- .../hlsjs/controller/stream-controller.js | 109 ----- cmd/mjpeg-player/hlsjs/event-handler.js | 105 ----- cmd/mjpeg-player/hlsjs/events.js | 55 --- cmd/mjpeg-player/hlsjs/hls.js | 50 --- .../hlsjs/loader/fragment-loader.js | 137 ------- cmd/mjpeg-player/hlsjs/loader/fragment.js | 222 ---------- cmd/mjpeg-player/hlsjs/loader/level-key.js | 47 --- cmd/mjpeg-player/hlsjs/loader/m3u8-parser.js | 388 ------------------ .../hlsjs/loader/playlist-loader.js | 343 ---------------- cmd/mjpeg-player/hlsjs/mts-demuxer.js | 369 ----------------- cmd/mjpeg-player/hlsjs/observer.js | 38 -- cmd/mjpeg-player/hlsjs/types/loader.js | 42 -- cmd/mjpeg-player/hlsjs/utils/codecs.js | 98 ----- cmd/mjpeg-player/hlsjs/utils/xhr-loader.js | 189 --------- cmd/mjpeg-player/index.html | 59 --- cmd/mjpeg-player/main.js | 143 ------- cmd/mjpeg-player/player.js | 134 ------ cmd/mjpeg-player/url-toolkit/url-toolkit.js | 149 ------- 20 files changed, 3109 deletions(-) delete mode 100644 cmd/mjpeg-player/eventemitter3/index.js delete mode 100644 cmd/mjpeg-player/hlsjs/config.js delete mode 100644 cmd/mjpeg-player/hlsjs/controller/stream-controller.js delete mode 100644 cmd/mjpeg-player/hlsjs/event-handler.js delete mode 100644 cmd/mjpeg-player/hlsjs/events.js delete mode 100644 cmd/mjpeg-player/hlsjs/hls.js delete mode 100644 cmd/mjpeg-player/hlsjs/loader/fragment-loader.js delete mode 100644 cmd/mjpeg-player/hlsjs/loader/fragment.js delete mode 100644 cmd/mjpeg-player/hlsjs/loader/level-key.js delete mode 100644 cmd/mjpeg-player/hlsjs/loader/m3u8-parser.js delete mode 100644 cmd/mjpeg-player/hlsjs/loader/playlist-loader.js delete mode 100644 cmd/mjpeg-player/hlsjs/mts-demuxer.js delete mode 100644 cmd/mjpeg-player/hlsjs/observer.js delete mode 100644 cmd/mjpeg-player/hlsjs/types/loader.js delete mode 100644 cmd/mjpeg-player/hlsjs/utils/codecs.js delete mode 100644 cmd/mjpeg-player/hlsjs/utils/xhr-loader.js delete mode 100644 cmd/mjpeg-player/index.html delete mode 100644 cmd/mjpeg-player/main.js delete mode 100644 cmd/mjpeg-player/player.js delete mode 100644 cmd/mjpeg-player/url-toolkit/url-toolkit.js diff --git a/cmd/mjpeg-player/eventemitter3/index.js b/cmd/mjpeg-player/eventemitter3/index.js deleted file mode 100644 index 7d65945d..00000000 --- a/cmd/mjpeg-player/eventemitter3/index.js +++ /dev/null @@ -1,331 +0,0 @@ -'use strict'; - -var has = Object.prototype.hasOwnProperty - , prefix = '~'; - -/** - * Constructor to create a storage for our `EE` objects. - * An `Events` instance is a plain object whose properties are event names. - * - * @constructor - * @private - */ -function Events() { } - -// -// We try to not inherit from `Object.prototype`. In some engines creating an -// instance in this way is faster than calling `Object.create(null)` directly. -// If `Object.create(null)` is not supported we prefix the event names with a -// character to make sure that the built-in object properties are not -// overridden or used as an attack vector. -// -if (Object.create) { - Events.prototype = Object.create(null); - - // - // This hack is needed because the `__proto__` property is still inherited in - // some old browsers like Android 4, iPhone 5.1, Opera 11 and Safari 5. - // - if (!new Events().__proto__) prefix = false; -} - -/** - * Representation of a single event listener. - * - * @param {Function} fn The listener function. - * @param {*} context The context to invoke the listener with. - * @param {Boolean} [once=false] Specify if the listener is a one-time listener. - * @constructor - * @private - */ -function EE(fn, context, once) { - this.fn = fn; - this.context = context; - this.once = once || false; -} - -/** - * Add a listener for a given event. - * - * @param {EventEmitter} emitter Reference to the `EventEmitter` instance. - * @param {(String|Symbol)} event The event name. - * @param {Function} fn The listener function. - * @param {*} context The context to invoke the listener with. - * @param {Boolean} once Specify if the listener is a one-time listener. - * @returns {EventEmitter} - * @private - */ -function addListener(emitter, event, fn, context, once) { - if (typeof fn !== 'function') { - throw new TypeError('The listener must be a function'); - } - - var listener = new EE(fn, context || emitter, once) - , evt = prefix ? prefix + event : event; - - if (!emitter._events[evt]) emitter._events[evt] = listener, emitter._eventsCount++; - else if (!emitter._events[evt].fn) emitter._events[evt].push(listener); - else emitter._events[evt] = [emitter._events[evt], listener]; - - return emitter; -} - -/** - * Clear event by name. - * - * @param {EventEmitter} emitter Reference to the `EventEmitter` instance. - * @param {(String|Symbol)} evt The Event name. - * @private - */ -function clearEvent(emitter, evt) { - if (--emitter._eventsCount === 0) emitter._events = new Events(); - else delete emitter._events[evt]; -} - -/** - * Minimal `EventEmitter` interface that is molded against the Node.js - * `EventEmitter` interface. - * - * @constructor - * @public - */ -function EventEmitter() { - this._events = new Events(); - this._eventsCount = 0; -} - -/** - * Return an array listing the events for which the emitter has registered - * listeners. - * - * @returns {Array} - * @public - */ -EventEmitter.prototype.eventNames = function eventNames() { - var names = [] - , events - , name; - - if (this._eventsCount === 0) return names; - - for (name in (events = this._events)) { - if (has.call(events, name)) names.push(prefix ? name.slice(1) : name); - } - - if (Object.getOwnPropertySymbols) { - return names.concat(Object.getOwnPropertySymbols(events)); - } - - return names; -}; - -/** - * Return the listeners registered for a given event. - * - * @param {(String|Symbol)} event The event name. - * @returns {Array} The registered listeners. - * @public - */ -EventEmitter.prototype.listeners = function listeners(event) { - var evt = prefix ? prefix + event : event - , handlers = this._events[evt]; - - if (!handlers) return []; - if (handlers.fn) return [handlers.fn]; - - for (var i = 0, l = handlers.length, ee = new Array(l); i < l; i++) { - ee[i] = handlers[i].fn; - } - - return ee; -}; - -/** - * Return the number of listeners listening to a given event. - * - * @param {(String|Symbol)} event The event name. - * @returns {Number} The number of listeners. - * @public - */ -EventEmitter.prototype.listenerCount = function listenerCount(event) { - var evt = prefix ? prefix + event : event - , listeners = this._events[evt]; - - if (!listeners) return 0; - if (listeners.fn) return 1; - return listeners.length; -}; - -/** - * Calls each of the listeners registered for a given event. - * - * @param {(String|Symbol)} event The event name. - * @returns {Boolean} `true` if the event had listeners, else `false`. - * @public - */ -EventEmitter.prototype.emit = function emit(event, a1, a2, a3, a4, a5) { - var evt = prefix ? prefix + event : event; - - if (!this._events[evt]) return false; - - var listeners = this._events[evt] - , len = arguments.length - , args - , i; - - if (listeners.fn) { - if (listeners.once) this.removeListener(event, listeners.fn, undefined, true); - - switch (len) { - case 1: return listeners.fn.call(listeners.context), true; - case 2: return listeners.fn.call(listeners.context, a1), true; - case 3: return listeners.fn.call(listeners.context, a1, a2), true; - case 4: return listeners.fn.call(listeners.context, a1, a2, a3), true; - case 5: return listeners.fn.call(listeners.context, a1, a2, a3, a4), true; - case 6: return listeners.fn.call(listeners.context, a1, a2, a3, a4, a5), true; - } - - for (i = 1, args = new Array(len - 1); i < len; i++) { - args[i - 1] = arguments[i]; - } - - listeners.fn.apply(listeners.context, args); - } else { - var length = listeners.length - , j; - - for (i = 0; i < length; i++) { - if (listeners[i].once) this.removeListener(event, listeners[i].fn, undefined, true); - - switch (len) { - case 1: listeners[i].fn.call(listeners[i].context); break; - case 2: listeners[i].fn.call(listeners[i].context, a1); break; - case 3: listeners[i].fn.call(listeners[i].context, a1, a2); break; - case 4: listeners[i].fn.call(listeners[i].context, a1, a2, a3); break; - default: - if (!args) for (j = 1, args = new Array(len - 1); j < len; j++) { - args[j - 1] = arguments[j]; - } - - listeners[i].fn.apply(listeners[i].context, args); - } - } - } - - return true; -}; - -/** - * Add a listener for a given event. - * - * @param {(String|Symbol)} event The event name. - * @param {Function} fn The listener function. - * @param {*} [context=this] The context to invoke the listener with. - * @returns {EventEmitter} `this`. - * @public - */ -EventEmitter.prototype.on = function on(event, fn, context) { - return addListener(this, event, fn, context, false); -}; - -/** - * Add a one-time listener for a given event. - * - * @param {(String|Symbol)} event The event name. - * @param {Function} fn The listener function. - * @param {*} [context=this] The context to invoke the listener with. - * @returns {EventEmitter} `this`. - * @public - */ -EventEmitter.prototype.once = function once(event, fn, context) { - return addListener(this, event, fn, context, true); -}; - -/** - * Remove the listeners of a given event. - * - * @param {(String|Symbol)} event The event name. - * @param {Function} fn Only remove the listeners that match this function. - * @param {*} context Only remove the listeners that have this context. - * @param {Boolean} once Only remove one-time listeners. - * @returns {EventEmitter} `this`. - * @public - */ -EventEmitter.prototype.removeListener = function removeListener(event, fn, context, once) { - var evt = prefix ? prefix + event : event; - - if (!this._events[evt]) return this; - if (!fn) { - clearEvent(this, evt); - return this; - } - - var listeners = this._events[evt]; - - if (listeners.fn) { - if ( - listeners.fn === fn && - (!once || listeners.once) && - (!context || listeners.context === context) - ) { - clearEvent(this, evt); - } - } else { - for (var i = 0, events = [], length = listeners.length; i < length; i++) { - if ( - listeners[i].fn !== fn || - (once && !listeners[i].once) || - (context && listeners[i].context !== context) - ) { - events.push(listeners[i]); - } - } - - // - // Reset the array, or remove it completely if we have no more listeners. - // - if (events.length) this._events[evt] = events.length === 1 ? events[0] : events; - else clearEvent(this, evt); - } - - return this; -}; - -/** - * Remove all listeners, or those of the specified event. - * - * @param {(String|Symbol)} [event] The event name. - * @returns {EventEmitter} `this`. - * @public - */ -EventEmitter.prototype.removeAllListeners = function removeAllListeners(event) { - var evt; - - if (event) { - evt = prefix ? prefix + event : event; - if (this._events[evt]) clearEvent(this, evt); - } else { - this._events = new Events(); - this._eventsCount = 0; - } - - return this; -}; - -// -// Alias methods names because people roll like that. -// -EventEmitter.prototype.off = EventEmitter.prototype.removeListener; -EventEmitter.prototype.addListener = EventEmitter.prototype.on; - -// -// Expose the prefix. -// -EventEmitter.prefixed = prefix; - -// -// Allow `EventEmitter` to be imported as module namespace. -// -EventEmitter.EventEmitter = EventEmitter; - -export default EventEmitter; diff --git a/cmd/mjpeg-player/hlsjs/config.js b/cmd/mjpeg-player/hlsjs/config.js deleted file mode 100644 index ed548ff9..00000000 --- a/cmd/mjpeg-player/hlsjs/config.js +++ /dev/null @@ -1,101 +0,0 @@ -/* -AUTHOR - Trek Hopton - -LICENSE - This file is Copyright (C) 2020 the Australian Ocean Lab (AusOcean) - - It is free software: you can redistribute it and/or modify them - under the terms of the GNU General Public License as published by the - Free Software Foundation, either version 3 of the License, or (at your - option) any later version. - - It is distributed in the hope that it will be useful, but WITHOUT - ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - for more details. - - You should have received a copy of the GNU General Public License in gpl.txt. - If not, see http://www.gnu.org/licenses. - - For hls.js Copyright notice and license, see LICENSE file. -*/ - -import XhrLoader from './utils/xhr-loader.js'; - -// If possible, keep hlsDefaultConfig shallow -// It is cloned whenever a new Hls instance is created, by keeping the config -// shallow the properties are cloned, and we don't end up manipulating the default -export const hlsDefaultConfig = { - autoStartLoad: true, // used by stream-controller - startPosition: -1, // used by stream-controller - defaultAudioCodec: void 0, // used by stream-controller - debug: false, // used by logger - capLevelOnFPSDrop: false, // used by fps-controller - capLevelToPlayerSize: false, // used by cap-level-controller - initialLiveManifestSize: 1, // used by stream-controller - maxBufferLength: 30, // used by stream-controller - maxBufferSize: 60 * 1000 * 1000, // used by stream-controller - maxBufferHole: 0.5, // used by stream-controller - - lowBufferWatchdogPeriod: 0.5, // used by stream-controller - highBufferWatchdogPeriod: 3, // used by stream-controller - nudgeOffset: 0.1, // used by stream-controller - nudgeMaxRetry: 3, // used by stream-controller - maxFragLookUpTolerance: 0.25, // used by stream-controller - liveSyncDurationCount: 3, // used by stream-controller - liveMaxLatencyDurationCount: Infinity, // used by stream-controller - liveSyncDuration: void 0, // used by stream-controller - liveMaxLatencyDuration: void 0, // used by stream-controller - liveDurationInfinity: false, // used by buffer-controller - liveBackBufferLength: Infinity, // used by buffer-controller - maxMaxBufferLength: 600, // used by stream-controller - enableWorker: true, // used by demuxer - enableSoftwareAES: true, // used by decrypter - manifestLoadingTimeOut: 10000, // used by playlist-loader - manifestLoadingMaxRetry: 1, // used by playlist-loader - manifestLoadingRetryDelay: 1000, // used by playlist-loader - manifestLoadingMaxRetryTimeout: 64000, // used by playlist-loader - startLevel: void 0, // used by level-controller - levelLoadingTimeOut: 10000, // used by playlist-loader - levelLoadingMaxRetry: 4, // used by playlist-loader - levelLoadingRetryDelay: 1000, // used by playlist-loader - levelLoadingMaxRetryTimeout: 64000, // used by playlist-loader - fragLoadingTimeOut: 20000, // used by fragment-loader - fragLoadingMaxRetry: 6, // used by fragment-loader - fragLoadingRetryDelay: 1000, // used by fragment-loader - fragLoadingMaxRetryTimeout: 64000, // used by fragment-loader - startFragPrefetch: false, // used by stream-controller - fpsDroppedMonitoringPeriod: 5000, // used by fps-controller - fpsDroppedMonitoringThreshold: 0.2, // used by fps-controller - appendErrorMaxRetry: 3, // used by buffer-controller - loader: XhrLoader, - // loader: FetchLoader, - fLoader: void 0, // used by fragment-loader - pLoader: void 0, // used by playlist-loader - xhrSetup: void 0, // used by xhr-loader - licenseXhrSetup: void 0, // used by eme-controller - fetchSetup: void 0, - // abrController: AbrController, - // bufferController: BufferController, - // capLevelController: CapLevelController, - // fpsController: FPSController, - stretchShortVideoTrack: false, // used by mp4-remuxer - maxAudioFramesDrift: 1, // used by mp4-remuxer - forceKeyFrameOnDiscontinuity: true, // used by ts-demuxer - abrEwmaFastLive: 3, // used by abr-controller - abrEwmaSlowLive: 9, // used by abr-controller - abrEwmaFastVoD: 3, // used by abr-controller - abrEwmaSlowVoD: 9, // used by abr-controller - abrEwmaDefaultEstimate: 5e5, // 500 kbps // used by abr-controller - abrBandWidthFactor: 0.95, // used by abr-controller - abrBandWidthUpFactor: 0.7, // used by abr-controller - abrMaxWithRealBitrate: false, // used by abr-controller - maxStarvationDelay: 4, // used by abr-controller - maxLoadingDelay: 4, // used by abr-controller - minAutoBitrate: 0, // used by hls - emeEnabled: false, // used by eme-controller - widevineLicenseUrl: void 0, // used by eme-controller - // requestMediaKeySystemAccessFunc: requestMediaKeySystemAccess, // used by eme-controller - -}; \ No newline at end of file diff --git a/cmd/mjpeg-player/hlsjs/controller/stream-controller.js b/cmd/mjpeg-player/hlsjs/controller/stream-controller.js deleted file mode 100644 index 31fd0828..00000000 --- a/cmd/mjpeg-player/hlsjs/controller/stream-controller.js +++ /dev/null @@ -1,109 +0,0 @@ -/* -AUTHOR - Trek Hopton - -LICENSE - This file is Copyright (C) 2020 the Australian Ocean Lab (AusOcean) - - It is free software: you can redistribute it and/or modify them - under the terms of the GNU General Public License as published by the - Free Software Foundation, either version 3 of the License, or (at your - option) any later version. - - It is distributed in the hope that it will be useful, but WITHOUT - ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - for more details. - - You should have received a copy of the GNU General Public License in gpl.txt. - If not, see http://www.gnu.org/licenses. - - For hls.js Copyright notice and license, see LICENSE file. -*/ - -/* - * Stream Controller -*/ - -import Event from '../events.js'; -import EventHandler from '../event-handler.js'; - -class StreamController extends EventHandler { - constructor(hls) { - super(hls, - Event.LEVEL_LOADED, - Event.FRAG_LOADED); - this.hls = hls; - this.config = hls.config; - this.audioCodecSwap = false; - this.stallReported = false; - this.gapController = null; - this.currentFrag = 0; - } - - _fetchPayloadOrEos(levelDetails) { - this.fragments = levelDetails.fragments; - this._loadFragment(); - } - - _loadFragment() { - let fragLen = this.fragments.length; - if (this.currentFrag >= fragLen) { - return; - } - this.hls.trigger(Event.FRAG_LOADING, { frag: this.fragments[this.currentFrag++] }); - } - - onLevelLoaded(data) { - const newDetails = data.details; - const newLevelId = data.level; - const levelDetails = data.details; - const duration = newDetails.totalduration; - let sliding = 0; - - console.log(`level ${newLevelId} loaded [${newDetails.startSN},${newDetails.endSN}],duration:${duration}`); - - if (newDetails.live) { - console.log("handling of this case is not implemented"); - } else { - newDetails.PTSKnown = false; - } - // override level info - this.levelLastLoaded = newLevelId; - this.hls.trigger(Event.LEVEL_UPDATED, { details: newDetails, level: newLevelId }); - - if (this.startFragRequested === false) { - // compute start position if set to -1. use it straight away if value is defined - if (this.startPosition === -1 || this.lastCurrentTime === -1) { - // first, check if start time offset has been set in playlist, if yes, use this value - let startTimeOffset = newDetails.startTimeOffset; - if (Number.isFinite(startTimeOffset)) { - if (startTimeOffset < 0) { - console.log(`negative start time offset ${startTimeOffset}, count from end of last fragment`); - startTimeOffset = sliding + duration + startTimeOffset; - } - console.log(`start time offset found in playlist, adjust startPosition to ${startTimeOffset}`); - this.startPosition = startTimeOffset; - } else { - // if live playlist, set start position to be fragment N-this.config.liveSyncDurationCount (usually 3) - if (newDetails.live) { - console.log("handling of this case is not implemented"); - } else { - this.startPosition = 0; - } - } - this.lastCurrentTime = this.startPosition; - } - this.nextLoadPosition = this.startPosition; - } - - this._fetchPayloadOrEos(levelDetails); - } - - onFragLoaded(data) { - this.hls.loadSuccess(data.payload); - this._loadFragment(); - - } -} -export default StreamController; diff --git a/cmd/mjpeg-player/hlsjs/event-handler.js b/cmd/mjpeg-player/hlsjs/event-handler.js deleted file mode 100644 index 24735225..00000000 --- a/cmd/mjpeg-player/hlsjs/event-handler.js +++ /dev/null @@ -1,105 +0,0 @@ -/* -AUTHOR - Trek Hopton - -LICENSE - This file is Copyright (C) 2020 the Australian Ocean Lab (AusOcean) - - It is free software: you can redistribute it and/or modify them - under the terms of the GNU General Public License as published by the - Free Software Foundation, either version 3 of the License, or (at your - option) any later version. - - It is distributed in the hope that it will be useful, but WITHOUT - ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - for more details. - - You should have received a copy of the GNU General Public License in gpl.txt. - If not, see http://www.gnu.org/licenses. - - For hls.js Copyright notice and license, see LICENSE file. -*/ - -/* -* -* All objects in the event handling chain should inherit from this class -* -*/ -import Event from './events.js'; - -const FORBIDDEN_EVENT_NAMES = { - 'hlsEventGeneric': true, - 'hlsHandlerDestroying': true, - 'hlsHandlerDestroyed': true -}; - -class EventHandler { - constructor(hls, ...events) { - this.hls = hls; - this.onEvent = this.onEvent.bind(this); - this.handledEvents = events; - this.useGenericHandler = true; - - this.registerListeners(); - } - - destroy() { - this.onHandlerDestroying(); - this.unregisterListeners(); - this.onHandlerDestroyed(); - } - - onHandlerDestroying() { } - onHandlerDestroyed() { } - - isEventHandler() { - return typeof this.handledEvents === 'object' && this.handledEvents.length && typeof this.onEvent === 'function'; - } - - registerListeners() { - if (this.isEventHandler()) { - this.handledEvents.forEach(function (event) { - if (FORBIDDEN_EVENT_NAMES[event]) { - throw new Error('Forbidden event-name: ' + event); - } - - this.hls.on(event, this.onEvent); - }, this); - } - } - - unregisterListeners() { - if (this.isEventHandler()) { - this.handledEvents.forEach(function (event) { - this.hls.off(event, this.onEvent); - }, this); - } - } - - /** - * arguments: event (string), data (any) - */ - onEvent(event, data) { - this.onEventGeneric(event, data); - } - - onEventGeneric(event, data) { - let eventToFunction = function (event, data) { - let funcName = 'on' + event.replace('hls', ''); - if (typeof this[funcName] !== 'function') { - throw new Error(`Event ${event} has no generic handler in this ${this.constructor.name} class (tried ${funcName})`); - } - - return this[funcName].bind(this, data); - }; - try { - eventToFunction.call(this, event, data).call(); - } catch (err) { - console.error(`An internal error happened while handling event ${event}. Error message: "${err.message}". Here is a stacktrace:`, err); - this.hls.trigger(Event.ERROR, { type: ErrorTypes.OTHER_ERROR, details: ErrorDetails.INTERNAL_EXCEPTION, fatal: false, event: event, err: err }); - } - } -} - -export default EventHandler; diff --git a/cmd/mjpeg-player/hlsjs/events.js b/cmd/mjpeg-player/hlsjs/events.js deleted file mode 100644 index e71d71c5..00000000 --- a/cmd/mjpeg-player/hlsjs/events.js +++ /dev/null @@ -1,55 +0,0 @@ -/* -AUTHOR - Trek Hopton - -LICENSE - This file is Copyright (C) 2020 the Australian Ocean Lab (AusOcean) - - It is free software: you can redistribute it and/or modify them - under the terms of the GNU General Public License as published by the - Free Software Foundation, either version 3 of the License, or (at your - option) any later version. - - It is distributed in the hope that it will be useful, but WITHOUT - ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - for more details. - - You should have received a copy of the GNU General Public License in gpl.txt. - If not, see http://www.gnu.org/licenses. - - For hls.js Copyright notice and license, see LICENSE file. -*/ - -/** - * @readonly - * @enum {string} - */ -const HlsEvents = { - // fired to signal that a manifest loading starts - data: { url : manifestURL} - MANIFEST_LOADING: 'hlsManifestLoading', - // fired after manifest has been loaded - data: { levels : [available quality levels], audioTracks : [ available audio tracks], url : manifestURL, stats : { trequest, tfirst, tload, mtime}} - MANIFEST_LOADED: 'hlsManifestLoaded', - // fired when a level playlist loading starts - data: { url : level URL, level : id of level being loaded} - LEVEL_LOADING: 'hlsLevelLoading', - // fired when a level playlist loading finishes - data: { details : levelDetails object, level : id of loaded level, stats : { trequest, tfirst, tload, mtime} } - LEVEL_LOADED: 'hlsLevelLoaded', - // fired when a level's details have been updated based on previous details, after it has been loaded - data: { details : levelDetails object, level : id of updated level } - LEVEL_UPDATED: 'hlsLevelUpdated', - // fired when an audio track loading starts - data: { url : audio track URL, id : audio track id } - AUDIO_TRACK_LOADING: 'hlsAudioTrackLoading', - // fired when an audio track loading finishes - data: { details : levelDetails object, id : audio track id, stats : { trequest, tfirst, tload, mtime } } - AUDIO_TRACK_LOADED: 'hlsAudioTrackLoaded', - // fired when a subtitle track loading starts - data: { url : subtitle track URL, id : subtitle track id } - SUBTITLE_TRACK_LOADING: 'hlsSubtitleTrackLoading', - // fired when a subtitle track loading finishes - data: { details : levelDetails object, id : subtitle track id, stats : { trequest, tfirst, tload, mtime } } - SUBTITLE_TRACK_LOADED: 'hlsSubtitleTrackLoaded', - // fired when a fragment loading starts - data: { frag : fragment object } - FRAG_LOADING: 'hlsFragLoading', - // fired when a fragment loading is progressing - data: { frag : fragment object, { trequest, tfirst, loaded } } - FRAG_LOAD_PROGRESS: 'hlsFragLoadProgress', - // fired when a fragment loading is completed - data: { frag : fragment object, payload : fragment payload, stats : { trequest, tfirst, tload, length } } - FRAG_LOADED: 'hlsFragLoaded' -}; - -export default HlsEvents; diff --git a/cmd/mjpeg-player/hlsjs/hls.js b/cmd/mjpeg-player/hlsjs/hls.js deleted file mode 100644 index a344d330..00000000 --- a/cmd/mjpeg-player/hlsjs/hls.js +++ /dev/null @@ -1,50 +0,0 @@ -/* -AUTHOR - Trek Hopton - -LICENSE - This file is Copyright (C) 2020 the Australian Ocean Lab (AusOcean) - - It is free software: you can redistribute it and/or modify them - under the terms of the GNU General Public License as published by the - Free Software Foundation, either version 3 of the License, or (at your - option) any later version. - - It is distributed in the hope that it will be useful, but WITHOUT - ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - for more details. - - You should have received a copy of the GNU General Public License in gpl.txt. - If not, see http://www.gnu.org/licenses. - - For hls.js Copyright notice and license, see LICENSE file. -*/ - -import URLToolkit from '../url-toolkit/url-toolkit.js'; -import HlsEvents from './events.js'; -import PlaylistLoader from './loader/playlist-loader.js'; -import FragmentLoader from './loader/fragment-loader.js'; -import StreamController from './controller/stream-controller.js'; -import { hlsDefaultConfig } from './config.js'; -import { Observer } from './observer.js'; - -class Hls extends Observer { - constructor() { - super(); - this.pLoader = new PlaylistLoader(this); - this.streamController = new StreamController(this); - this.fragmentLoader = new FragmentLoader(this); - - this.config = hlsDefaultConfig; - } - - // url is the source URL. Can be relative or absolute. - loadSource(url, callback) { - this.loadSuccess = callback; - url = URLToolkit.buildAbsoluteURL(window.location.href, url, { alwaysNormalize: true }); - this.trigger(HlsEvents.MANIFEST_LOADING, { url: url }); - } -} - -export default Hls \ No newline at end of file diff --git a/cmd/mjpeg-player/hlsjs/loader/fragment-loader.js b/cmd/mjpeg-player/hlsjs/loader/fragment-loader.js deleted file mode 100644 index b7e1f9e1..00000000 --- a/cmd/mjpeg-player/hlsjs/loader/fragment-loader.js +++ /dev/null @@ -1,137 +0,0 @@ -/* -AUTHOR - Trek Hopton - -LICENSE - This file is Copyright (C) 2020 the Australian Ocean Lab (AusOcean) - - It is free software: you can redistribute it and/or modify them - under the terms of the GNU General Public License as published by the - Free Software Foundation, either version 3 of the License, or (at your - option) any later version. - - It is distributed in the hope that it will be useful, but WITHOUT - ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - for more details. - - You should have received a copy of the GNU General Public License in gpl.txt. - If not, see http://www.gnu.org/licenses. - - For hls.js Copyright notice and license, see LICENSE file. -*/ - -/* - * Fragment Loader -*/ - -import Event from '../events.js'; -import EventHandler from '../event-handler.js'; - -class FragmentLoader extends EventHandler { - constructor(hls) { - super(hls, Event.FRAG_LOADING); - this.loaders = {}; - } - - destroy() { - let loaders = this.loaders; - for (let loaderName in loaders) { - let loader = loaders[loaderName]; - if (loader) { - loader.destroy(); - } - } - this.loaders = {}; - - super.destroy(); - } - - onFragLoading(data) { - const frag = data.frag, - type = frag.type, - loaders = this.loaders, - config = this.hls.config, - FragmentILoader = config.fLoader, - DefaultILoader = config.loader; - - // reset fragment state - frag.loaded = 0; - - let loader = loaders[type]; - if (loader) { - console.warn(`abort previous fragment loader for type: ${type}`); - loader.abort(); - } - - loader = loaders[type] = frag.loader = - config.fLoader ? new FragmentILoader(config) : new DefaultILoader(config); - - let loaderContext, loaderConfig, loaderCallbacks; - - loaderContext = { url: frag.url, frag: frag, responseType: 'arraybuffer', progressData: false }; - - let start = frag.byteRangeStartOffset, - end = frag.byteRangeEndOffset; - - if (Number.isFinite(start) && Number.isFinite(end)) { - loaderContext.rangeStart = start; - loaderContext.rangeEnd = end; - } - - loaderConfig = { - timeout: config.fragLoadingTimeOut, - maxRetry: 0, - retryDelay: 0, - maxRetryDelay: config.fragLoadingMaxRetryTimeout - }; - - loaderCallbacks = { - onSuccess: this.loadsuccess.bind(this), - onError: this.loaderror.bind(this), - onTimeout: this.loadtimeout.bind(this), - onProgress: this.loadprogress.bind(this) - }; - - loader.load(loaderContext, loaderConfig, loaderCallbacks); - } - - loadsuccess(response, stats, context, networkDetails = null) { - let payload = response.data, frag = context.frag; - // detach fragment loader on load success - frag.loader = undefined; - this.loaders[frag.type] = undefined; - this.hls.trigger(Event.FRAG_LOADED, { payload: payload, frag: frag, stats: stats, networkDetails: networkDetails }); - } - - loaderror(response, context, networkDetails = null) { - const frag = context.frag; - let loader = frag.loader; - if (loader) { - loader.abort(); - } - - this.loaders[frag.type] = undefined; - this.hls.trigger(Event.ERROR, { type: ErrorTypes.NETWORK_ERROR, details: ErrorDetails.FRAG_LOAD_ERROR, fatal: false, frag: context.frag, response: response, networkDetails: networkDetails }); - } - - loadtimeout(stats, context, networkDetails = null) { - const frag = context.frag; - let loader = frag.loader; - if (loader) { - loader.abort(); - } - - this.loaders[frag.type] = undefined; - this.hls.trigger(Event.ERROR, { type: ErrorTypes.NETWORK_ERROR, details: ErrorDetails.FRAG_LOAD_TIMEOUT, fatal: false, frag: context.frag, networkDetails: networkDetails }); - } - - // data will be used for progressive parsing - loadprogress(stats, context, data, networkDetails = null) { // jshint ignore:line - let frag = context.frag; - frag.loaded = stats.loaded; - this.hls.trigger(Event.FRAG_LOAD_PROGRESS, { frag: frag, stats: stats, networkDetails: networkDetails }); - } -} - -export default FragmentLoader; diff --git a/cmd/mjpeg-player/hlsjs/loader/fragment.js b/cmd/mjpeg-player/hlsjs/loader/fragment.js deleted file mode 100644 index 01fb0509..00000000 --- a/cmd/mjpeg-player/hlsjs/loader/fragment.js +++ /dev/null @@ -1,222 +0,0 @@ -/* -AUTHOR - Trek Hopton - -LICENSE - This file is Copyright (C) 2020 the Australian Ocean Lab (AusOcean) - - It is free software: you can redistribute it and/or modify them - under the terms of the GNU General Public License as published by the - Free Software Foundation, either version 3 of the License, or (at your - option) any later version. - - It is distributed in the hope that it will be useful, but WITHOUT - ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - for more details. - - You should have received a copy of the GNU General Public License in gpl.txt. - If not, see http://www.gnu.org/licenses. - - For hls.js Copyright notice and license, see LICENSE file. -*/ - -import URLToolkit from '../../url-toolkit/url-toolkit.js'; -import LevelKey from './level-key.js'; - -export const ElementaryStreamTypes = { - AUDIO: 'audio', - VIDEO: 'video' -} - -export default class Fragment { - constructor() { - this._url = null; - this._byteRange = null; - this._decryptdata = null; - - // Holds the types of data this fragment supports - this._elementaryStreams = { - [ElementaryStreamTypes.AUDIO]: false, - [ElementaryStreamTypes.VIDEO]: false - }; - - // deltaPTS tracks the change in presentation timestamp between fragments - this.deltaPTS = 0; - - this.rawProgramDateTime = null; - this.programDateTime = null; - this.title = null; - this.tagList = []; - - // TODO: Move at least baseurl to constructor. - // Currently we do a two-pass construction as use the Fragment class almost like a object for holding parsing state. - // It may make more sense to just use a POJO to keep state during the parsing phase. - // Have Fragment be the representation once we have a known state? - // Something to think on. - - // Discontinuity Counter - this.cc; - this.type; - // relurl is the portion of the URL that comes from inside the playlist. - this.relurl; - // baseurl is the URL to the playlist - this.baseurl; - // EXTINF has to be present for a m3u8 to be considered valid - this.duration; - // When this segment starts in the timeline - this.start; - // sn notates the sequence number for a segment, and if set to a string can be 'initSegment' - this.sn = 0; - - this.urlId = 0; - // level matches this fragment to a index playlist - this.level = 0; - // levelkey is the EXT-X-KEY that applies to this segment for decryption - // core difference from the private field _decryptdata is the lack of the initialized IV - // _decryptdata will set the IV for this segment based on the segment number in the fragment - this.levelkey; - - // TODO(typescript-xhrloader) - this.loader; - } - - // setByteRange converts a EXT-X-BYTERANGE attribute into a two element array - setByteRange(value, previousFrag) { - const params = value.split('@', 2); - const byteRange = []; - if (params.length === 1) { - byteRange[0] = previousFrag ? previousFrag.byteRangeEndOffset : 0; - } else { - byteRange[0] = parseInt(params[1]); - } - byteRange[1] = parseInt(params[0]) + byteRange[0]; - this._byteRange = byteRange; - } - - get url() { - if (!this._url && this.relurl) { - this._url = URLToolkit.buildAbsoluteURL(this.baseurl, this.relurl, { alwaysNormalize: true }); - } - - return this._url; - } - - set url(value) { - this._url = value; - } - - get byteRange() { - if (!this._byteRange) { - return []; - } - - return this._byteRange; - } - - /** - * @type {number} - */ - get byteRangeStartOffset() { - return this.byteRange[0]; - } - - get byteRangeEndOffset() { - return this.byteRange[1]; - } - - get decryptdata() { - if (!this.levelkey && !this._decryptdata) { - return null; - } - - if (!this._decryptdata && this.levelkey) { - let sn = this.sn; - if (typeof sn !== 'number') { - // We are fetching decryption data for a initialization segment - // If the segment was encrypted with AES-128 - // It must have an IV defined. We cannot substitute the Segment Number in. - if (this.levelkey && this.levelkey.method === 'AES-128' && !this.levelkey.iv) { - console.warn(`missing IV for initialization segment with method="${this.levelkey.method}" - compliance issue`); - } - - /* - Be converted to a Number. - 'initSegment' will become NaN. - NaN, which when converted through ToInt32() -> +0. - --- - Explicitly set sn to resulting value from implicit conversions 'initSegment' values for IV generation. - */ - sn = 0; - } - this._decryptdata = this.setDecryptDataFromLevelKey(this.levelkey, sn); - } - - return this._decryptdata; - } - - get endProgramDateTime() { - if (this.programDateTime === null) { - return null; - } - - if (!Number.isFinite(this.programDateTime)) { - return null; - } - - let duration = !Number.isFinite(this.duration) ? 0 : this.duration; - - return this.programDateTime + (duration * 1000); - } - - get encrypted() { - return !!((this.decryptdata && this.decryptdata.uri !== null) && (this.decryptdata.key === null)); - } - - /** - * @param {ElementaryStreamTypes} type - */ - addElementaryStream(type) { - this._elementaryStreams[type] = true; - } - - /** - * @param {ElementaryStreamTypes} type - */ - hasElementaryStream(type) { - return this._elementaryStreams[type] === true; - } - - /** - * Utility method for parseLevelPlaylist to create an initialization vector for a given segment - * @param {number} segmentNumber - segment number to generate IV with - * @returns {Uint8Array} - */ - createInitializationVector(segmentNumber) { - let uint8View = new Uint8Array(16); - - for (let i = 12; i < 16; i++) { - uint8View[i] = (segmentNumber >> 8 * (15 - i)) & 0xff; - } - - return uint8View; - } - - /** - * Utility method for parseLevelPlaylist to get a fragment's decryption data from the currently parsed encryption key data - * @param levelkey - a playlist's encryption info - * @param segmentNumber - the fragment's segment number - * @returns {LevelKey} - an object to be applied as a fragment's decryptdata - */ - setDecryptDataFromLevelKey(levelkey, segmentNumber) { - let decryptdata = levelkey; - - if (levelkey && levelkey.method && levelkey.uri && !levelkey.iv) { - decryptdata = new LevelKey(levelkey.baseuri, levelkey.reluri); - decryptdata.method = levelkey.method; - decryptdata.iv = this.createInitializationVector(segmentNumber); - } - - return decryptdata; - } -} diff --git a/cmd/mjpeg-player/hlsjs/loader/level-key.js b/cmd/mjpeg-player/hlsjs/loader/level-key.js deleted file mode 100644 index 8bfe4331..00000000 --- a/cmd/mjpeg-player/hlsjs/loader/level-key.js +++ /dev/null @@ -1,47 +0,0 @@ -/* -AUTHOR - Trek Hopton - -LICENSE - This file is Copyright (C) 2020 the Australian Ocean Lab (AusOcean) - - It is free software: you can redistribute it and/or modify them - under the terms of the GNU General Public License as published by the - Free Software Foundation, either version 3 of the License, or (at your - option) any later version. - - It is distributed in the hope that it will be useful, but WITHOUT - ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - for more details. - - You should have received a copy of the GNU General Public License in gpl.txt. - If not, see http://www.gnu.org/licenses. - - For hls.js Copyright notice and license, see LICENSE file. -*/ - -import URLToolkit from '../../url-toolkit/url-toolkit.js'; - -export default class LevelKey { - constructor(baseURI, relativeURI) { - this._uri = null; - - this.baseuri; - this.reluri; - this.method = null; - this.key = null; - this.iv = null; - - this.baseuri = baseURI; - this.reluri = relativeURI; - } - - get uri() { - if (!this._uri && this.reluri) { - this._uri = URLToolkit.buildAbsoluteURL(this.baseuri, this.reluri, { alwaysNormalize: true }); - } - - return this._uri; - } -} diff --git a/cmd/mjpeg-player/hlsjs/loader/m3u8-parser.js b/cmd/mjpeg-player/hlsjs/loader/m3u8-parser.js deleted file mode 100644 index 8778dc75..00000000 --- a/cmd/mjpeg-player/hlsjs/loader/m3u8-parser.js +++ /dev/null @@ -1,388 +0,0 @@ -/* -AUTHOR - Trek Hopton - -LICENSE - This file is Copyright (C) 2020 the Australian Ocean Lab (AusOcean) - - It is free software: you can redistribute it and/or modify them - under the terms of the GNU General Public License as published by the - Free Software Foundation, either version 3 of the License, or (at your - option) any later version. - - It is distributed in the hope that it will be useful, but WITHOUT - ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - for more details. - - You should have received a copy of the GNU General Public License in gpl.txt. - If not, see http://www.gnu.org/licenses. - - For hls.js Copyright notice and license, see LICENSE file. -*/ - -import URLToolkit from '../../url-toolkit/url-toolkit.js'; -import Fragment from './fragment.js'; -import Level from './level.js'; -import LevelKey from './level-key.js'; -import AttrList from '../utils/attr-list.js'; -import { isCodecType } from '../utils/codecs.js'; - -/** - * M3U8 parser - * @module - */ - -// https://regex101.com is your friend -const MASTER_PLAYLIST_REGEX = /#EXT-X-STREAM-INF:([^\n\r]*)[\r\n]+([^\r\n]+)/g; -const MASTER_PLAYLIST_MEDIA_REGEX = /#EXT-X-MEDIA:(.*)/g; - -const LEVEL_PLAYLIST_REGEX_FAST = new RegExp([ - /#EXTINF:\s*(\d*(?:\.\d+)?)(?:,(.*)\s+)?/.source, // duration (#EXTINF:,), group 1 => duration, group 2 => title - /|(?!#)([\S+ ?]+)/.source, // segment URI, group 3 => the URI (note newline is not eaten) - /|#EXT-X-BYTERANGE:*(.+)/.source, // next segment's byterange, group 4 => range spec (x@y) - /|#EXT-X-PROGRAM-DATE-TIME:(.+)/.source, // next segment's program date/time group 5 => the datetime spec - /|#.*/.source // All other non-segment oriented tags will match with all groups empty -].join(''), 'g'); - -const LEVEL_PLAYLIST_REGEX_SLOW = /(?:(?:#(EXTM3U))|(?:#EXT-X-(PLAYLIST-TYPE):(.+))|(?:#EXT-X-(MEDIA-SEQUENCE): *(\d+))|(?:#EXT-X-(TARGETDURATION): *(\d+))|(?:#EXT-X-(KEY):(.+))|(?:#EXT-X-(START):(.+))|(?:#EXT-X-(ENDLIST))|(?:#EXT-X-(DISCONTINUITY-SEQ)UENCE:(\d+))|(?:#EXT-X-(DIS)CONTINUITY))|(?:#EXT-X-(VERSION):(\d+))|(?:#EXT-X-(MAP):(.+))|(?:(#)([^:]*):(.*))|(?:(#)(.*))(?:.*)\r?\n?/; - -const MP4_REGEX_SUFFIX = /\.(mp4|m4s|m4v|m4a)$/i; - -export default class M3U8Parser { - static findGroup(groups, mediaGroupId) { - for (let i = 0; i < groups.length; i++) { - const group = groups[i]; - if (group.id === mediaGroupId) { - return group; - } - } - } - - static convertAVC1ToAVCOTI(codec) { - let avcdata = codec.split('.'); - let result; - if (avcdata.length > 2) { - result = avcdata.shift() + '.'; - result += parseInt(avcdata.shift()).toString(16); - result += ('000' + parseInt(avcdata.shift()).toString(16)).substr(-4); - } else { - result = codec; - } - return result; - } - - static resolve(url, baseUrl) { - return URLToolkit.buildAbsoluteURL(baseUrl, url, { alwaysNormalize: true }); - } - - static parseMasterPlaylist(string, baseurl) { - // TODO(typescript-level) - let levels = []; - MASTER_PLAYLIST_REGEX.lastIndex = 0; - - // TODO(typescript-level) - function setCodecs(codecs, level) { - ['video', 'audio'].forEach((type) => { - const filtered = codecs.filter((codec) => isCodecType(codec, type)); - if (filtered.length) { - const preferred = filtered.filter((codec) => { - return codec.lastIndexOf('avc1', 0) === 0 || codec.lastIndexOf('mp4a', 0) === 0; - }); - level[`${type}Codec`] = preferred.length > 0 ? preferred[0] : filtered[0]; - - // remove from list - codecs = codecs.filter((codec) => filtered.indexOf(codec) === -1); - } - }); - - level.unknownCodecs = codecs; - } - - let result; - while ((result = MASTER_PLAYLIST_REGEX.exec(string)) != null) { - // TODO(typescript-level) - const level = {}; - - const attrs = level.attrs = new AttrList(result[1]); - level.url = M3U8Parser.resolve(result[2], baseurl); - - const resolution = attrs.decimalResolution('RESOLUTION'); - if (resolution) { - level.width = resolution.width; - level.height = resolution.height; - } - level.bitrate = attrs.decimalInteger('AVERAGE-BANDWIDTH') || attrs.decimalInteger('BANDWIDTH'); - level.name = attrs.NAME; - - setCodecs([].concat((attrs.CODECS || '').split(/[ ,]+/)), level); - - if (level.videoCodec && level.videoCodec.indexOf('avc1') !== -1) { - level.videoCodec = M3U8Parser.convertAVC1ToAVCOTI(level.videoCodec); - } - - levels.push(level); - } - return levels; - } - - static parseMasterPlaylistMedia(string, baseurl, type, audioGroups = []) { - let result; - let medias = []; - let id = 0; - MASTER_PLAYLIST_MEDIA_REGEX.lastIndex = 0; - while ((result = MASTER_PLAYLIST_MEDIA_REGEX.exec(string)) !== null) { - const attrs = new AttrList(result[1]); - if (attrs.TYPE === type) { - const media = { - id: id++, - groupId: attrs['GROUP-ID'], - name: attrs.NAME || attrs.LANGUAGE, - type, - default: (attrs.DEFAULT === 'YES'), - autoselect: (attrs.AUTOSELECT === 'YES'), - forced: (attrs.FORCED === 'YES'), - lang: attrs.LANGUAGE - }; - - if (attrs.URI) { - media.url = M3U8Parser.resolve(attrs.URI, baseurl); - } - - if (audioGroups.length) { - // If there are audio groups signalled in the manifest, let's look for a matching codec string for this track - const groupCodec = M3U8Parser.findGroup(audioGroups, media.groupId); - - // If we don't find the track signalled, lets use the first audio groups codec we have - // Acting as a best guess - media.audioCodec = groupCodec ? groupCodec.codec : audioGroups[0].codec; - } - - medias.push(media); - } - } - return medias; - } - - static parseLevelPlaylist(string, baseurl, id, type, levelUrlId) { - let currentSN = 0; - let totalduration = 0; - let level = new Level(baseurl); - let discontinuityCounter = 0; - let prevFrag = null; - let frag = new Fragment(); - let result; - let i; - let levelkey; - - let firstPdtIndex = null; - - LEVEL_PLAYLIST_REGEX_FAST.lastIndex = 0; - - while ((result = LEVEL_PLAYLIST_REGEX_FAST.exec(string)) !== null) { - const duration = result[1]; - if (duration) { // INF - frag.duration = parseFloat(duration); - // avoid sliced strings https://github.com/video-dev/hls.js/issues/939 - const title = (' ' + result[2]).slice(1); - frag.title = title || null; - frag.tagList.push(title ? ['INF', duration, title] : ['INF', duration]); - } else if (result[3]) { // url - if (Number.isFinite(frag.duration)) { - const sn = currentSN++; - frag.type = type; - frag.start = totalduration; - if (levelkey) { - frag.levelkey = levelkey; - } - frag.sn = sn; - frag.level = id; - frag.cc = discontinuityCounter; - frag.urlId = levelUrlId; - frag.baseurl = baseurl; - // avoid sliced strings https://github.com/video-dev/hls.js/issues/939 - frag.relurl = (' ' + result[3]).slice(1); - assignProgramDateTime(frag, prevFrag); - - level.fragments.push(frag); - prevFrag = frag; - totalduration += frag.duration; - - frag = new Fragment(); - } - } else if (result[4]) { // X-BYTERANGE - const data = (' ' + result[4]).slice(1); - if (prevFrag) { - frag.setByteRange(data, prevFrag); - } else { - frag.setByteRange(data); - } - } else if (result[5]) { // PROGRAM-DATE-TIME - // avoid sliced strings https://github.com/video-dev/hls.js/issues/939 - frag.rawProgramDateTime = (' ' + result[5]).slice(1); - frag.tagList.push(['PROGRAM-DATE-TIME', frag.rawProgramDateTime]); - if (firstPdtIndex === null) { - firstPdtIndex = level.fragments.length; - } - } else { - result = result[0].match(LEVEL_PLAYLIST_REGEX_SLOW); - if (!result) { - console.warn('No matches on slow regex match for level playlist!'); - continue; - } - for (i = 1; i < result.length; i++) { - if (typeof result[i] !== 'undefined') { - break; - } - } - - // avoid sliced strings https://github.com/video-dev/hls.js/issues/939 - const value1 = (' ' + result[i + 1]).slice(1); - const value2 = (' ' + result[i + 2]).slice(1); - - switch (result[i]) { - case '#': - frag.tagList.push(value2 ? [value1, value2] : [value1]); - break; - case 'PLAYLIST-TYPE': - level.type = value1.toUpperCase(); - break; - case 'MEDIA-SEQUENCE': - currentSN = level.startSN = parseInt(value1); - break; - case 'TARGETDURATION': - level.targetduration = parseFloat(value1); - break; - case 'VERSION': - level.version = parseInt(value1); - break; - case 'EXTM3U': - break; - case 'ENDLIST': - level.live = false; - break; - case 'DIS': - discontinuityCounter++; - frag.tagList.push(['DIS']); - break; - case 'DISCONTINUITY-SEQ': - discontinuityCounter = parseInt(value1); - break; - case 'KEY': { - // https://tools.ietf.org/html/draft-pantos-http-live-streaming-08#section-3.4.4 - const decryptparams = value1; - const keyAttrs = new AttrList(decryptparams); - const decryptmethod = keyAttrs.enumeratedString('METHOD'); - const decrypturi = keyAttrs.URI; - const decryptiv = keyAttrs.hexadecimalInteger('IV'); - - if (decryptmethod) { - levelkey = new LevelKey(baseurl, decrypturi); - if ((decrypturi) && (['AES-128', 'SAMPLE-AES', 'SAMPLE-AES-CENC'].indexOf(decryptmethod) >= 0)) { - levelkey.method = decryptmethod; - levelkey.key = null; - // Initialization Vector (IV) - levelkey.iv = decryptiv; - } - } - break; - } - case 'START': { - const startAttrs = new AttrList(value1); - const startTimeOffset = startAttrs.decimalFloatingPoint('TIME-OFFSET'); - // TIME-OFFSET can be 0 - if (Number.isFinite(startTimeOffset)) { - level.startTimeOffset = startTimeOffset; - } - break; - } - case 'MAP': { - const mapAttrs = new AttrList(value1); - frag.relurl = mapAttrs.URI; - if (mapAttrs.BYTERANGE) { - frag.setByteRange(mapAttrs.BYTERANGE); - } - frag.baseurl = baseurl; - frag.level = id; - frag.type = type; - frag.sn = 'initSegment'; - level.initSegment = frag; - frag = new Fragment(); - frag.rawProgramDateTime = level.initSegment.rawProgramDateTime; - break; - } - default: - console.warn(`line parsed but not handled: ${result}`); - break; - } - } - } - frag = prevFrag; - // console.log('found ' + level.fragments.length + ' fragments'); - if (frag && !frag.relurl) { - level.fragments.pop(); - totalduration -= frag.duration; - } - level.totalduration = totalduration; - level.averagetargetduration = totalduration / level.fragments.length; - level.endSN = currentSN - 1; - level.startCC = level.fragments[0] ? level.fragments[0].cc : 0; - level.endCC = discontinuityCounter; - - if (!level.initSegment && level.fragments.length) { - // this is a bit lurky but HLS really has no other way to tell us - // if the fragments are TS or MP4, except if we download them :/ - // but this is to be able to handle SIDX. - if (level.fragments.every((frag) => MP4_REGEX_SUFFIX.test(frag.relurl))) { - console.warn('MP4 fragments found but no init segment (probably no MAP, incomplete M3U8), trying to fetch SIDX'); - - frag = new Fragment(); - frag.relurl = level.fragments[0].relurl; - frag.baseurl = baseurl; - frag.level = id; - frag.type = type; - frag.sn = 'initSegment'; - - level.initSegment = frag; - level.needSidxRanges = true; - } - } - - /** - * Backfill any missing PDT values - "If the first EXT-X-PROGRAM-DATE-TIME tag in a Playlist appears after - one or more Media Segment URIs, the client SHOULD extrapolate - backward from that tag (using EXTINF durations and/or media - timestamps) to associate dates with those segments." - * We have already extrapolated forward, but all fragments up to the first instance of PDT do not have their PDTs - * computed. - */ - if (firstPdtIndex) { - backfillProgramDateTimes(level.fragments, firstPdtIndex); - } - - return level; - } -} - -function backfillProgramDateTimes(fragments, startIndex) { - let fragPrev = fragments[startIndex]; - for (let i = startIndex - 1; i >= 0; i--) { - const frag = fragments[i]; - frag.programDateTime = fragPrev.programDateTime - (frag.duration * 1000); - fragPrev = frag; - } -} - -function assignProgramDateTime(frag, prevFrag) { - if (frag.rawProgramDateTime) { - frag.programDateTime = Date.parse(frag.rawProgramDateTime); - } else if (prevFrag && prevFrag.programDateTime) { - frag.programDateTime = prevFrag.endProgramDateTime; - } - - if (!Number.isFinite(frag.programDateTime)) { - frag.programDateTime = null; - frag.rawProgramDateTime = null; - } -} diff --git a/cmd/mjpeg-player/hlsjs/loader/playlist-loader.js b/cmd/mjpeg-player/hlsjs/loader/playlist-loader.js deleted file mode 100644 index e34f236b..00000000 --- a/cmd/mjpeg-player/hlsjs/loader/playlist-loader.js +++ /dev/null @@ -1,343 +0,0 @@ -/* -AUTHOR - Trek Hopton <trek@ausocean.org> - -LICENSE - This file is Copyright (C) 2020 the Australian Ocean Lab (AusOcean) - - It is free software: you can redistribute it and/or modify them - under the terms of the GNU General Public License as published by the - Free Software Foundation, either version 3 of the License, or (at your - option) any later version. - - It is distributed in the hope that it will be useful, but WITHOUT - ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - for more details. - - You should have received a copy of the GNU General Public License in gpl.txt. - If not, see http://www.gnu.org/licenses. - - For hls.js Copyright notice and license, see LICENSE file. -*/ - -import { PlaylistContextType, PlaylistLevelType } from '../types/loader.js'; -import Event from '../events.js'; -import EventHandler from '../event-handler.js'; -import M3U8Parser from './m3u8-parser.js'; - -const { performance } = window; - -class PlaylistLoader extends EventHandler { - constructor(hls) { - super(hls, - Event.MANIFEST_LOADING, - Event.LEVEL_LOADING, - Event.AUDIO_TRACK_LOADING, - Event.SUBTITLE_TRACK_LOADING); - this.hls = hls; - this.loaders = {}; - } - - /** - * @param {PlaylistContextType} type - * @returns {boolean} - */ - static canHaveQualityLevels(type) { - return (type !== PlaylistContextType.AUDIO_TRACK && - type !== PlaylistContextType.SUBTITLE_TRACK); - } - - onManifestLoading(data) { - this.load({ - url: data.url, - type: PlaylistContextType.MANIFEST, - level: 0, - id: null, - responseType: 'text' - }); - } - - // param 1 -> data: { url: string; level: number | null; id: number | null; } - onLevelLoading(data) { - this.load({ - url: data.url, - type: PlaylistContextType.LEVEL, - level: data.level, - id: data.id, - responseType: 'text' - }); - } - - onAudioTrackLoading(data) { - this.load({ - url: data.url, - type: PlaylistContextType.AUDIO_TRACK, - level: null, - id: data.id, - responseType: 'text' - }); - } - - onSubtitleTrackLoading(data) { - this.load({ - url: data.url, - type: PlaylistContextType.SUBTITLE_TRACK, - level: null, - id: data.id, - responseType: 'text' - }); - } - - static getResponseUrl(response, context) { - let url = response.url; - // responseURL not supported on some browsers (it is used to detect URL redirection) - // data-uri mode also not supported (but no need to detect redirection) - if (url === undefined || url.indexOf('data:') === 0) { - // fallback to initial URL - url = context.url; - } - return url; - } - - /** - * Map context.type to LevelType - * @param {PlaylistLoaderContext} context - * @returns {LevelType} - */ - static mapContextToLevelType(context) { - const { type } = context; - - switch (type) { - case PlaylistContextType.AUDIO_TRACK: - return PlaylistLevelType.AUDIO; - case PlaylistContextType.SUBTITLE_TRACK: - return PlaylistLevelType.SUBTITLE; - default: - return PlaylistLevelType.MAIN; - } - } - - getInternalLoader(context) { - return this.loaders[context.type]; - } - - /** - * Returns defaults or configured loader-type overloads (pLoader and loader config params) - * Default loader is XHRLoader (see utils) - * @param {PlaylistLoaderContext} context - * @returns {Loader} or other compatible configured overload - */ - createInternalLoader(context) { - const config = this.hls.config; - const PLoader = config.pLoader; - const Loader = config.loader; - // TODO(typescript-config): Verify once config is typed that InternalLoader always returns a Loader - const InternalLoader = PLoader || Loader; - - const loader = new InternalLoader(config); - - // TODO - Do we really need to assign the instance or if the dep has been lost - context.loader = loader; - this.loaders[context.type] = loader; - - return loader; - } - - resetInternalLoader(contextType) { - if (this.loaders[contextType]) { - delete this.loaders[contextType]; - } - } - - load(context) { - const config = this.hls.config; - - // Check if a loader for this context already exists - let loader = this.getInternalLoader(context); - if (loader) { - const loaderContext = loader.context; - if (loaderContext && loaderContext.url === context.url) { // same URL can't overlap - return false; - } else { - loader.abort(); - } - } - - let maxRetry; - let timeout; - let retryDelay; - let maxRetryDelay; - - // apply different configs for retries depending on - // context (manifest, level, audio/subs playlist) - switch (context.type) { - case PlaylistContextType.MANIFEST: - maxRetry = config.manifestLoadingMaxRetry; - timeout = config.manifestLoadingTimeOut; - retryDelay = config.manifestLoadingRetryDelay; - maxRetryDelay = config.manifestLoadingMaxRetryTimeout; - break; - case PlaylistContextType.LEVEL: - // Disable internal loader retry logic, since we are managing retries in Level Controller - maxRetry = 0; - maxRetryDelay = 0; - retryDelay = 0; - timeout = config.levelLoadingTimeOut; - // TODO Introduce retry settings for audio-track and subtitle-track, it should not use level retry config - break; - default: - maxRetry = config.levelLoadingMaxRetry; - timeout = config.levelLoadingTimeOut; - retryDelay = config.levelLoadingRetryDelay; - maxRetryDelay = config.levelLoadingMaxRetryTimeout; - break; - } - - loader = this.createInternalLoader(context); - - const loaderConfig = { - timeout, - maxRetry, - retryDelay, - maxRetryDelay - }; - - const loaderCallbacks = { - onSuccess: this.loadsuccess.bind(this), - onError: this.loaderror.bind(this), - onTimeout: this.loadtimeout.bind(this) - }; - - loader.load(context, loaderConfig, loaderCallbacks); - - return true; - } - - loadsuccess(response, stats, context, networkDetails = null) { - if (context.isSidxRequest) { - this._handleSidxRequest(response, context); - this._handlePlaylistLoaded(response, stats, context, networkDetails); - return; - } - - this.resetInternalLoader(context.type); - if (typeof response.data !== 'string') { - throw new Error('expected responseType of "text" for PlaylistLoader'); - } - - const string = response.data; - - stats.tload = performance.now(); - - // Validate if it is an M3U8 at all - if (string.indexOf('#EXTM3U') !== 0) { - console.error("no EXTM3U delimiter"); - return; - } - - // Check if chunk-list or master. handle empty chunk list case (first EXTINF not signaled, but TARGETDURATION present) - if (string.indexOf('#EXTINF:') > 0 || string.indexOf('#EXT-X-TARGETDURATION:') > 0) { - this._handleTrackOrLevelPlaylist(response, stats, context, networkDetails); - } else { - console.log("handling of master playlists is not implemented"); - // this._handleMasterPlaylist(response, stats, context, networkDetails); - } - - } - - loaderror(response, context, networkDetails = null) { - console.error("network error while loading", response); - } - - loadtimeout(stats, context, networkDetails = null) { - console.error("network timeout while loading", stats); - } - - _handleTrackOrLevelPlaylist(response, stats, context, networkDetails) { - const hls = this.hls; - - const { id, level, type } = context; - - const url = PlaylistLoader.getResponseUrl(response, context); - - // if the values are null, they will result in the else conditional - const levelUrlId = Number.isFinite(id) ? id : 0; - const levelId = Number.isFinite(level) ? level : levelUrlId; - - const levelType = PlaylistLoader.mapContextToLevelType(context); - const levelDetails = M3U8Parser.parseLevelPlaylist(response.data, url, levelId, levelType, levelUrlId); - - // set stats on level structure - // TODO(jstackhouse): why? mixing concerns, is it just treated as value bag? - (levelDetails).tload = stats.tload; - - // We have done our first request (Manifest-type) and receive - // not a master playlist but a chunk-list (track/level) - // We fire the manifest-loaded event anyway with the parsed level-details - // by creating a single-level structure for it. - if (type === PlaylistContextType.MANIFEST) { - const singleLevel = { - url, - details: levelDetails - }; - - hls.trigger(Event.MANIFEST_LOADED, { - levels: [singleLevel], - audioTracks: [], - url, - stats, - networkDetails - }); - } - - // save parsing time - stats.tparsed = performance.now(); - - // extend the context with the new levelDetails property - context.levelDetails = levelDetails; - - this._handlePlaylistLoaded(response, stats, context, networkDetails); - } - - _handlePlaylistLoaded(response, stats, context, networkDetails) { - const { type, level, id, levelDetails } = context; - - if (!levelDetails || !levelDetails.targetduration) { - console.error("manifest parsing error"); - return; - } - - const canHaveLevels = PlaylistLoader.canHaveQualityLevels(context.type); - if (canHaveLevels) { - this.hls.trigger(Event.LEVEL_LOADED, { - details: levelDetails, - level: level || 0, - id: id || 0, - stats, - networkDetails - }); - } else { - switch (type) { - case PlaylistContextType.AUDIO_TRACK: - this.hls.trigger(Event.AUDIO_TRACK_LOADED, { - details: levelDetails, - id, - stats, - networkDetails - }); - break; - case PlaylistContextType.SUBTITLE_TRACK: - this.hls.trigger(Event.SUBTITLE_TRACK_LOADED, { - details: levelDetails, - id, - stats, - networkDetails - }); - break; - } - } - } -} - -export default PlaylistLoader; diff --git a/cmd/mjpeg-player/hlsjs/mts-demuxer.js b/cmd/mjpeg-player/hlsjs/mts-demuxer.js deleted file mode 100644 index 5fe7c351..00000000 --- a/cmd/mjpeg-player/hlsjs/mts-demuxer.js +++ /dev/null @@ -1,369 +0,0 @@ -/* -AUTHOR - Trek Hopton <trek@ausocean.org> - -LICENSE - This file is Copyright (C) 2020 the Australian Ocean Lab (AusOcean) - - It is free software: you can redistribute it and/or modify them - under the terms of the GNU General Public License as published by the - Free Software Foundation, either version 3 of the License, or (at your - option) any later version. - - It is distributed in the hope that it will be useful, but WITHOUT - ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - for more details. - - You should have received a copy of the GNU General Public License in gpl.txt. - If not, see http://www.gnu.org/licenses. - - For hls.js Copyright notice and license, see LICENSE file. -*/ - -// MTSDemuxer demultiplexes an MPEG-TS stream into its individual streams. -// While it is possible that the MPEG-TS stream may contain many streams, -// this demuxer will result in at most one stream of each type ie. video, audio, id3 metadata. -class MTSDemuxer { - constructor() { - this.init(); - } - - // init initialises MTSDemuxer's state. It can be used to reset an MTSDemuxer instance. - init() { - this.pmtParsed = false; - this._pmtId = -1; - } - - // createTrack creates and returns a track model. - /** - * @param {string} type 'audio' | 'video' | 'id3' | 'text' - * @return {object} MTSDemuxer's internal track model. - */ - static createTrack(type) { - return { - type, - pid: -1, - data: [] // This will contain Uint8Arrays representing each PES packet's payload for this track. - }; - } - - // _syncOffset scans the first 'maxScanWindow' bytes and returns an offset to the beginning of the first three MTS packets, - // or -1 if three are not found. - // A TS fragment should contain at least 3 TS packets, a PAT, a PMT, and one PID, each starting with 0x47. - static _syncOffset(data) { - const maxScanWindow = 1000; // 1000 is a reasonable number of bytes to search for the first MTS packets. - const scanWindow = Math.min(maxScanWindow, data.length - 3 * 188); - let i = 0; - while (i < scanWindow) { - if (data[i] === 0x47 && data[i + 188] === 0x47 && data[i + 2 * 188] === 0x47) { - return i; - } else { - i++; - } - } - return -1; - } - - demux(data) { - let start, len = data.length, pusi, pid, afc, offset, pes, - unknownPIDs = false; - let pmtParsed = this.pmtParsed, - videoTrack = MTSDemuxer.createTrack('video'), - audioTrack = MTSDemuxer.createTrack('audio'), - id3Track = MTSDemuxer.createTrack('id3'), - videoId, - audioId, - id3Id, - pmtId = this._pmtId, - videoData = this.videoPesData, - audioData = this.audioPesData, - id3Data = this.id3PesData, - parsePAT = this._parsePAT, - parsePMT = this._parsePMT, - parsePES = this._parsePES; - - const syncOffset = MTSDemuxer._syncOffset(data); - - // Don't parse last TS packet if incomplete. - len -= (len + syncOffset) % 188; - - // Loop through TS packets. - for (start = syncOffset; start < len; start += 188) { - if (data[start] === 0x47) { - pusi = !!(data[start + 1] & 0x40); - // pid is a 13-bit field starting at the last bit of TS[1]. - pid = ((data[start + 1] & 0x1f) << 8) + data[start + 2]; - afc = (data[start + 3] & 0x30) >> 4; - // If an adaption field is present, its length is specified by the fifth byte of the TS packet header. - if (afc > 1) { - offset = start + 5 + data[start + 4]; - // Continue if there is only adaptation field. - if (offset === (start + 188)) { - continue; - } - } else { - offset = start + 4; - } - switch (pid) { - case videoId: - if (pusi) { - if (videoData && (pes = parsePES(videoData)) && pes.pts !== undefined) { - videoTrack.data.push(pes.data); - // TODO: here pes contains data, pts, dts and len. Are all these needed? - } - videoData = { data: [], size: 0 }; - } - if (videoData) { - videoData.data.push(data.subarray(offset, start + 188)); - videoData.size += start + 188 - offset; - } - break; - case audioId: - if (pusi) { - if (audioData && (pes = parsePES(audioData)) && pes.pts !== undefined) { - audioTrack.data.push(pes.data); - } - audioData = { data: [], size: 0 }; - } - if (audioData) { - audioData.data.push(data.subarray(offset, start + 188)); - audioData.size += start + 188 - offset; - } - break; - case id3Id: - if (pusi) { - if (id3Data && (pes = parsePES(id3Data)) && pes.pts !== undefined) { - id3Track.data.push(pes.data); - } - id3Data = { data: [], size: 0 }; - } - if (id3Data) { - id3Data.data.push(data.subarray(offset, start + 188)); - id3Data.size += start + 188 - offset; - } - break; - case 0: - if (pusi) { - offset += data[offset] + 1; - } - - pmtId = this._pmtId = parsePAT(data, offset); - break; - case pmtId: - if (pusi) { - offset += data[offset] + 1; - } - - let parsedPIDs = parsePMT(data, offset); - - // Only update track id if track PID found while parsing PMT. - // This is to avoid resetting the PID to -1 in case track PID transiently disappears from the stream, - // this could happen in case of transient missing audio samples for example. - videoId = parsedPIDs.video; - if (videoId > 0) { - videoTrack.pid = videoId; - } - audioId = parsedPIDs.audio; - if (audioId > 0) { - audioTrack.pid = audioId; - } - id3Id = parsedPIDs.id3; - if (id3Id > 0) { - id3Track.pid = id3Id; - } - - if (unknownPIDs && !pmtParsed) { - // Reparse from beginning. - unknownPIDs = false; - // We set it to -188, the += 188 in the for loop will reset start to 0. - start = syncOffset - 188; - } - pmtParsed = this.pmtParsed = true; - break; - default: - unknownPIDs = true; - break; - } - } else { - console.error('TS packet did not start with 0x47'); - } - } - - // Try to parse last PES packets. - if (videoData && (pes = parsePES(videoData)) && pes.pts !== undefined) { - videoTrack.data.push(pes.data); - this.videoPesData = null; - } else { - // Either pesPkts null or PES truncated, keep it for next frag parsing. - this.videoPesData = videoData; - } - - if (audioData && (pes = parsePES(audioData)) && pes.pts !== undefined) { - audioTrack.data.push(pes.data); - this.audioPesData = null; - } else { - // Either pesPkts null or PES truncated, keep it for next frag parsing. - this.audioPesData = audioData; - } - - if (id3Data && (pes = parsePES(id3Data)) && pes.pts !== undefined) { - id3Track.data.push(pes.data); - this.id3PesData = null; - } else { - // Either pesPkts null or PES truncated, keep it for next frag parsing. - this.id3PesData = id3Data; - } - - return videoTrack; - } - - _parsePAT(data, offset) { - // Skip the PSI header and parse the first PMT entry. - return (data[offset + 10] & 0x1F) << 8 | data[offset + 11]; - // console.log('PMT PID:' + this._pmtId); - } - - _parsePMT(data, offset) { - let programInfoLength, pid, result = { audio: -1, video: -1, id3: -1 }, - sectionLength = (data[offset + 1] & 0x0f) << 8 | data[offset + 2], - tableEnd = offset + 3 + sectionLength - 4; - // To determine where the table is, we have to figure out how - // long the program info descriptors are. - programInfoLength = (data[offset + 10] & 0x0f) << 8 | data[offset + 11]; - // Advance the offset to the first entry in the mapping table. - offset += 12 + programInfoLength; - while (offset < tableEnd) { - pid = (data[offset + 1] & 0x1F) << 8 | data[offset + 2]; - switch (data[offset]) { - case 0x1c: // MJPEG - case 0xdb: // SAMPLE-AES AVC. - case 0x1b: // ITU-T Rec. H.264 and ISO/IEC 14496-10 (lower bit-rate video). - if (result.video === -1) { - result.video = pid; - } - break; - case 0xcf: // SAMPLE-AES AAC. - case 0x0f: // ISO/IEC 13818-7 ADTS AAC (MPEG-2 lower bit-rate audio). - case 0xd2: // ADPCM audio. - case 0x03: // ISO/IEC 11172-3 (MPEG-1 audio). - case 0x24: - // console.warn('HEVC stream type found, not supported for now'); - case 0x04: // or ISO/IEC 13818-3 (MPEG-2 halved sample rate audio). - if (result.audio === -1) { - result.audio = pid; - } - break; - case 0x15: // Packetized metadata (ID3) - // console.log('ID3 PID:' + pid); - if (result.id3 === -1) { - result.id3 = pid; - } - break; - default: - // console.log('unknown stream type:' + data[offset]); - break; - } - // Move to the next table entry, skip past the elementary stream descriptors, if present. - offset += ((data[offset + 3] & 0x0F) << 8 | data[offset + 4]) + 5; - } - return result; - } - - _parsePES(stream) { - let i = 0, frag, pesFlags, pesPrefix, pesLen, pesHdrLen, pesData, pesPts, pesDts, payloadStartOffset, data = stream.data; - // Safety check. - if (!stream || stream.size === 0) { - return null; - } - - // We might need up to 19 bytes to read PES header. - // If first chunk of data is less than 19 bytes, let's merge it with following ones until we get 19 bytes. - // Usually only one merge is needed (and this is rare ...). - while (data[0].length < 19 && data.length > 1) { - let newData = new Uint8Array(data[0].length + data[1].length); - newData.set(data[0]); - newData.set(data[1], data[0].length); - data[0] = newData; - data.splice(1, 1); - } - // Retrieve PTS/DTS from first fragment. - frag = data[0]; - pesPrefix = (frag[0] << 16) + (frag[1] << 8) + frag[2]; - if (pesPrefix === 1) { - pesLen = (frag[4] << 8) + frag[5]; - // If PES parsed length is not zero and greater than total received length, stop parsing. PES might be truncated. - // Minus 6 : PES header size. - if (pesLen && pesLen > stream.size - 6) { - return null; - } - - pesFlags = frag[7]; - if (pesFlags & 0xC0) { - // PES header described here : http://dvd.sourceforge.net/dvdinfo/pes-hdr.html - // As PTS / DTS is 33 bit we cannot use bitwise operator in JS, - // as Bitwise operators treat their operands as a sequence of 32 bits. - pesPts = (frag[9] & 0x0E) * 536870912 +// 1 << 29 - (frag[10] & 0xFF) * 4194304 +// 1 << 22 - (frag[11] & 0xFE) * 16384 +// 1 << 14 - (frag[12] & 0xFF) * 128 +// 1 << 7 - (frag[13] & 0xFE) / 2; - // Check if greater than 2^32 -1. - if (pesPts > 4294967295) { - // Decrement 2^33. - pesPts -= 8589934592; - } - if (pesFlags & 0x40) { - pesDts = (frag[14] & 0x0E) * 536870912 +// 1 << 29 - (frag[15] & 0xFF) * 4194304 +// 1 << 22 - (frag[16] & 0xFE) * 16384 +// 1 << 14 - (frag[17] & 0xFF) * 128 +// 1 << 7 - (frag[18] & 0xFE) / 2; - // Check if greater than 2^32 -1. - if (pesDts > 4294967295) { - // Decrement 2^33. - pesDts -= 8589934592; - } - if (pesPts - pesDts > 60 * 90000) { - // console.warn(`${Math.round((pesPts - pesDts) / 90000)}s delta between PTS and DTS, align them`); - pesPts = pesDts; - } - } else { - pesDts = pesPts; - } - } - pesHdrLen = frag[8]; - // 9 bytes : 6 bytes for PES header + 3 bytes for PES extension. - payloadStartOffset = pesHdrLen + 9; - - stream.size -= payloadStartOffset; - // Reassemble PES packet. - pesData = new Uint8Array(stream.size); - for (let j = 0, dataLen = data.length; j < dataLen; j++) { - frag = data[j]; - let len = frag.byteLength; - if (payloadStartOffset) { - if (payloadStartOffset > len) { - // Trim full frag if PES header bigger than frag. - payloadStartOffset -= len; - continue; - } else { - // Trim partial frag if PES header smaller than frag. - frag = frag.subarray(payloadStartOffset); - len -= payloadStartOffset; - payloadStartOffset = 0; - } - } - pesData.set(frag, i); - i += len; - } - if (pesLen) { - // Payload size : remove PES header + PES extension. - pesLen -= pesHdrLen + 3; - } - return { data: pesData, pts: pesPts, dts: pesDts, len: pesLen }; - } else { - return null; - } - } -} \ No newline at end of file diff --git a/cmd/mjpeg-player/hlsjs/observer.js b/cmd/mjpeg-player/hlsjs/observer.js deleted file mode 100644 index 9f300d23..00000000 --- a/cmd/mjpeg-player/hlsjs/observer.js +++ /dev/null @@ -1,38 +0,0 @@ -/* -AUTHOR - Trek Hopton <trek@ausocean.org> - -LICENSE - This file is Copyright (C) 2020 the Australian Ocean Lab (AusOcean) - - It is free software: you can redistribute it and/or modify them - under the terms of the GNU General Public License as published by the - Free Software Foundation, either version 3 of the License, or (at your - option) any later version. - - It is distributed in the hope that it will be useful, but WITHOUT - ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - for more details. - - You should have received a copy of the GNU General Public License in gpl.txt. - If not, see http://www.gnu.org/licenses. - - For hls.js Copyright notice and license, see LICENSE file. -*/ - -import EventEmitter from '../eventemitter3/index.js'; - -/** - * Simple adapter sub-class of Nodejs-like EventEmitter. - */ -export class Observer extends EventEmitter { - /** - * We simply want to pass along the event-name itself - * in every call to a handler, which is the purpose of our `trigger` method - * extending the standard API. - */ - trigger(event, ...data) { - this.emit(event, event, ...data); - } -} diff --git a/cmd/mjpeg-player/hlsjs/types/loader.js b/cmd/mjpeg-player/hlsjs/types/loader.js deleted file mode 100644 index 2a23cb1b..00000000 --- a/cmd/mjpeg-player/hlsjs/types/loader.js +++ /dev/null @@ -1,42 +0,0 @@ -/* -AUTHOR - Trek Hopton <trek@ausocean.org> - -LICENSE - This file is Copyright (C) 2020 the Australian Ocean Lab (AusOcean) - - It is free software: you can redistribute it and/or modify them - under the terms of the GNU General Public License as published by the - Free Software Foundation, either version 3 of the License, or (at your - option) any later version. - - It is distributed in the hope that it will be useful, but WITHOUT - ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - for more details. - - You should have received a copy of the GNU General Public License in gpl.txt. - If not, see http://www.gnu.org/licenses. - - For hls.js Copyright notice and license, see LICENSE file. -*/ - -/** - * @readonly - * @enum {string} - */ -export const PlaylistContextType = { - MANIFEST: 'manifest', - LEVEL: 'level', - AUDIO_TRACK: 'audioTrack', - SUBTITLE_TRACK: 'subtitleTrack' -} - -/** - * @enum {string} - */ -export const PlaylistLevelType = { - MAIN: 'main', - AUDIO: 'audio', - SUBTITLE: 'subtitle' -} diff --git a/cmd/mjpeg-player/hlsjs/utils/codecs.js b/cmd/mjpeg-player/hlsjs/utils/codecs.js deleted file mode 100644 index a9b345aa..00000000 --- a/cmd/mjpeg-player/hlsjs/utils/codecs.js +++ /dev/null @@ -1,98 +0,0 @@ -/* -AUTHOR - Trek Hopton <trek@ausocean.org> - -LICENSE - This file is Copyright (C) 2020 the Australian Ocean Lab (AusOcean) - - It is free software: you can redistribute it and/or modify them - under the terms of the GNU General Public License as published by the - Free Software Foundation, either version 3 of the License, or (at your - option) any later version. - - It is distributed in the hope that it will be useful, but WITHOUT - ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - for more details. - - You should have received a copy of the GNU General Public License in gpl.txt. - If not, see http://www.gnu.org/licenses. - - For hls.js Copyright notice and license, see LICENSE file. -*/ - -// from http://mp4ra.org/codecs.html -const sampleEntryCodesISO = { - audio: { - 'a3ds': true, - 'ac-3': true, - 'ac-4': true, - 'alac': true, - 'alaw': true, - 'dra1': true, - 'dts+': true, - 'dts-': true, - 'dtsc': true, - 'dtse': true, - 'dtsh': true, - 'ec-3': true, - 'enca': true, - 'g719': true, - 'g726': true, - 'm4ae': true, - 'mha1': true, - 'mha2': true, - 'mhm1': true, - 'mhm2': true, - 'mlpa': true, - 'mp4a': true, - 'raw ': true, - 'Opus': true, - 'samr': true, - 'sawb': true, - 'sawp': true, - 'sevc': true, - 'sqcp': true, - 'ssmv': true, - 'twos': true, - 'ulaw': true - }, - video: { - 'avc1': true, - 'avc2': true, - 'avc3': true, - 'avc4': true, - 'avcp': true, - 'drac': true, - 'dvav': true, - 'dvhe': true, - 'encv': true, - 'hev1': true, - 'hvc1': true, - 'mjp2': true, - 'mp4v': true, - 'mvc1': true, - 'mvc2': true, - 'mvc3': true, - 'mvc4': true, - 'resv': true, - 'rv60': true, - 's263': true, - 'svc1': true, - 'svc2': true, - 'vc-1': true, - 'vp08': true, - 'vp09': true - } -}; - -function isCodecType(codec, type) { - const typeCodes = sampleEntryCodesISO[type]; - return !!typeCodes && typeCodes[codec.slice(0, 4)] === true; -} - -function isCodecSupportedInMp4(codec, type) { - return MediaSource.isTypeSupported(`${type || 'video'}/mp4;codecs="${codec}"`); -} - -export { isCodecType, isCodecSupportedInMp4 }; diff --git a/cmd/mjpeg-player/hlsjs/utils/xhr-loader.js b/cmd/mjpeg-player/hlsjs/utils/xhr-loader.js deleted file mode 100644 index 67280a87..00000000 --- a/cmd/mjpeg-player/hlsjs/utils/xhr-loader.js +++ /dev/null @@ -1,189 +0,0 @@ -/* -AUTHOR - Trek Hopton <trek@ausocean.org> - -LICENSE - This file is Copyright (C) 2020 the Australian Ocean Lab (AusOcean) - - It is free software: you can redistribute it and/or modify them - under the terms of the GNU General Public License as published by the - Free Software Foundation, either version 3 of the License, or (at your - option) any later version. - - It is distributed in the hope that it will be useful, but WITHOUT - ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - for more details. - - You should have received a copy of the GNU General Public License in gpl.txt. - If not, see http://www.gnu.org/licenses. - - For hls.js Copyright notice and license, see LICENSE file. -*/ - -/** - * XHR based loader -*/ - -const { performance, XMLHttpRequest } = window; - -class XhrLoader { - constructor(config) { - if (config && config.xhrSetup) { - this.xhrSetup = config.xhrSetup; - } - } - - destroy() { - this.abort(); - this.loader = null; - } - - abort() { - let loader = this.loader; - if (loader && loader.readyState !== 4) { - this.stats.aborted = true; - loader.abort(); - } - - window.clearTimeout(this.requestTimeout); - this.requestTimeout = null; - window.clearTimeout(this.retryTimeout); - this.retryTimeout = null; - } - - load(context, config, callbacks) { - this.context = context; - this.config = config; - this.callbacks = callbacks; - this.stats = { trequest: performance.now(), retry: 0 }; - this.retryDelay = config.retryDelay; - this.loadInternal(); - } - - loadInternal() { - let xhr, context = this.context; - xhr = this.loader = new XMLHttpRequest(); - window.console.log("load internal xhr: " + context.url); - - let stats = this.stats; - stats.tfirst = 0; - stats.loaded = 0; - const xhrSetup = this.xhrSetup; - - try { - if (xhrSetup) { - try { - xhrSetup(xhr, context.url); - } catch (e) { - // fix xhrSetup: (xhr, url) => {xhr.setRequestHeader("Content-Language", "test");} - // not working, as xhr.setRequestHeader expects xhr.readyState === OPEN - xhr.open('GET', context.url, true); - xhrSetup(xhr, context.url); - } - } - if (!xhr.readyState) { - xhr.open('GET', context.url, true); - } - } catch (e) { - // IE11 throws an exception on xhr.open if attempting to access an HTTP resource over HTTPS - this.callbacks.onError({ code: xhr.status, text: e.message }, context, xhr); - return; - } - - if (context.rangeEnd) { - xhr.setRequestHeader('Range', 'bytes=' + context.rangeStart + '-' + (context.rangeEnd - 1)); - } - - xhr.onreadystatechange = this.readystatechange.bind(this); - xhr.onprogress = this.loadprogress.bind(this); - xhr.responseType = context.responseType; - - // setup timeout before we perform request - this.requestTimeout = window.setTimeout(this.loadtimeout.bind(this), this.config.timeout); - xhr.send(); - } - - readystatechange(event) { - let xhr = event.currentTarget, - readyState = xhr.readyState, - stats = this.stats, - context = this.context, - config = this.config; - - // don't proceed if xhr has been aborted - if (stats.aborted) { - return; - } - - // >= HEADERS_RECEIVED - if (readyState >= 2) { - // clear xhr timeout and rearm it if readyState less than 4 - window.clearTimeout(this.requestTimeout); - if (stats.tfirst === 0) { - stats.tfirst = Math.max(performance.now(), stats.trequest); - } - - if (readyState === 4) { - let status = xhr.status; - // http status between 200 to 299 are all successful - if (status >= 200 && status < 300) { - stats.tload = Math.max(stats.tfirst, performance.now()); - let data, len; - if (context.responseType === 'arraybuffer') { - data = xhr.response; - len = data.byteLength; - } else { - data = xhr.responseText; - len = data.length; - } - stats.loaded = stats.total = len; - let response = { url: xhr.responseURL, data: data }; - this.callbacks.onSuccess(response, stats, context, xhr); - } else { - // if max nb of retries reached or if http status between 400 and 499 (such error cannot be recovered, retrying is useless), return error - if (stats.retry >= config.maxRetry || (status >= 400 && status < 499)) { - console.error(`${status} while loading ${context.url}`); - this.callbacks.onError({ code: status, text: xhr.statusText }, context, xhr); - } else { - // retry - console.warn(`${status} while loading ${context.url}, retrying in ${this.retryDelay}...`); - // aborts and resets internal state - this.destroy(); - // schedule retry - this.retryTimeout = window.setTimeout(this.loadInternal.bind(this), this.retryDelay); - // set exponential backoff - this.retryDelay = Math.min(2 * this.retryDelay, config.maxRetryDelay); - stats.retry++; - } - } - } else { - // readyState >= 2 AND readyState !==4 (readyState = HEADERS_RECEIVED || LOADING) rearm timeout as xhr not finished yet - this.requestTimeout = window.setTimeout(this.loadtimeout.bind(this), config.timeout); - } - } - } - - loadtimeout() { - console.warn(`timeout while loading ${this.context.url}`); - this.callbacks.onTimeout(this.stats, this.context, null); - } - - loadprogress(event) { - let xhr = event.currentTarget, - stats = this.stats; - - stats.loaded = event.loaded; - if (event.lengthComputable) { - stats.total = event.total; - } - - let onProgress = this.callbacks.onProgress; - if (onProgress) { - // third arg is to provide on progress data - onProgress(stats, this.context, null, xhr); - } - } -} - -export default XhrLoader; diff --git a/cmd/mjpeg-player/index.html b/cmd/mjpeg-player/index.html deleted file mode 100644 index 60a40429..00000000 --- a/cmd/mjpeg-player/index.html +++ /dev/null @@ -1,59 +0,0 @@ -<!DOCTYPE html> -<!-- -AUTHOR - Trek Hopton <trek@ausocean.org> - -LICENSE - This file is Copyright (C) 2020 the Australian Ocean Lab (AusOcean) - - It is free software: you can redistribute it and/or modify them - under the terms of the GNU General Public License as published by the - Free Software Foundation, either version 3 of the License, or (at your - option) any later version. - - It is distributed in the hope that it will be useful, but WITHOUT - ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - for more details. - - You should have received a copy of the GNU General Public License in gpl.txt. - If not, see http://www.gnu.org/licenses. ---> - -<html lang="en"> - -<head> - <meta charset="utf-8"> - <title>Mjpeg Player - - - - -
-
-
-
- -
-
-
- - - -
-
- Frame Rate: fps -
-
- -
-
-
-
-
- ©2019 Australian Ocean Laboratory Limited (AusOcean) (License) -
-
- - - \ No newline at end of file diff --git a/cmd/mjpeg-player/main.js b/cmd/mjpeg-player/main.js deleted file mode 100644 index e4a0eba0..00000000 --- a/cmd/mjpeg-player/main.js +++ /dev/null @@ -1,143 +0,0 @@ -/* -AUTHOR - Trek Hopton - -LICENSE - This file is Copyright (C) 2020 the Australian Ocean Lab (AusOcean) - - It is free software: you can redistribute it and/or modify them - under the terms of the GNU General Public License as published by the - Free Software Foundation, either version 3 of the License, or (at your - option) any later version. - - It is distributed in the hope that it will be useful, but WITHOUT - ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - for more details. - - You should have received a copy of the GNU General Public License in gpl.txt. - If not, see http://www.gnu.org/licenses.. -*/ - -import Hls from "./hlsjs/hls.js"; - -let started = false; -let player, viewer; - -// init gets DOM elements once the document has been loaded and adds listeners where necessary. -function init() { - document.addEventListener('DOMContentLoaded', load); - document.addEventListener('DOMContentLoaded', function () { - document.getElementById('urlBtn').addEventListener('click', load); - document.getElementById('fileInput').addEventListener('change', play); - viewer = document.getElementById('viewer'); - } - ); -} - -init(); - -// load gets the URL from the URL input element or the browser's URL bar -// and creates an Hls instance to load the content from the URL. -function load() { - let url = document.getElementById('url').value; - if (url == "") { - url = getQuery() - document.getElementById('url').value = url; - } - if (url[0] == '/') { - url = window.location.protocol + '//' + window.location.host + url; - } - if (url == "") { - return; - } - - let hls = new Hls(); - hls.loadSource(url, append); -} - -// getQuery gets everything after the question mark from the URL in the browser's URL bar. -function getQuery() { - let regex = new RegExp("\\?(.*)"); - let match = regex.exec(window.location.href); - if (match == null) { - return ''; - } else { - return decodeURIComponent(match[1].replace(/\+/g, " ")); - } -} - -// append, on the first call, starts a player worker and passes it a frame rate and the video data, -// on subsequent calls it passes the video data to the player worker. -function append(data) { - if (!started) { - player = new Worker("player.js"); - - let rate = document.getElementById('rate'); - if (rate.value && rate.value > 0) { - player.postMessage({ msg: "setFrameRate", data: rate.value }); - } - - player.onmessage = handleMessage; - - player.postMessage({ msg: "loadMtsMjpeg", data: data }, [data]); - started = true; - } else { - player.postMessage({ msg: "appendMtsMjpeg", data: data }, [data]); - } - -} - -// play will process and play the target file chosen with the file input element. -function play() { - const input = event.target.files[0]; - const reader = new FileReader(); - - reader.onload = event => { - const player = new Worker("player.js"); - - let rate = document.getElementById('rate'); - if (rate.value && rate.value > 0) { - player.postMessage({ msg: "setFrameRate", data: rate.value }); - } - - player.onmessage = handleMessage; - - switch (input.name.split('.')[1]) { - case "mjpeg": - case "mjpg": - player.postMessage({ msg: "loadMjpeg", data: event.target.result }, [event.target.result]); - break; - case "ts": - player.postMessage({ msg: "loadMtsMjpeg", data: event.target.result }, [event.target.result]); - break; - default: - console.error("unknown file format"); - break; - } - }; - reader.onerror = error => reject(error); - reader.readAsArrayBuffer(input); -} - -// handleMessage handles messgaes from the player workers, its main job is to update the display when a frame is received. -function handleMessage(e) { - switch (e.data.msg) { - case "frame": - const blob = new Blob([new Uint8Array(e.data.data)], { - type: 'video/x-motion-jpeg' - }); - const url = URL.createObjectURL(blob); - viewer.src = url; - break; - case "log": - console.log(e.data.data); - break; - case "stop": - console.log("stopped"); - break; - default: - console.error("unknown message from player"); - break; - } -} \ No newline at end of file diff --git a/cmd/mjpeg-player/player.js b/cmd/mjpeg-player/player.js deleted file mode 100644 index ea96e031..00000000 --- a/cmd/mjpeg-player/player.js +++ /dev/null @@ -1,134 +0,0 @@ -/* -AUTHOR - Trek Hopton - -LICENSE - This file is Copyright (C) 2020 the Australian Ocean Lab (AusOcean) - - It is free software: you can redistribute it and/or modify them - under the terms of the GNU General Public License as published by the - Free Software Foundation, either version 3 of the License, or (at your - option) any later version. - - It is distributed in the hope that it will be useful, but WITHOUT - ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - for more details. - - You should have received a copy of the GNU General Public License in gpl.txt. - If not, see http://www.gnu.org/licenses. -*/ - -let frameRate = 25; //Keeps track of the frame rate, default is 25fps. -self.importScripts('./lex-mjpeg.js'); -self.importScripts('./hlsjs/mts-demuxer.js'); - -const codecs = { - MJPEG: 1, - MTS_MJPEG: 2, -} - -// onmessage is called whenever the main thread sends this worker a message. -onmessage = e => { - switch (e.data.msg) { - case "setFrameRate": - frameRate = e.data.data; - break; - case "loadMjpeg": - player = new PlayerWorker(); - player.init(codecs.MJPEG); - player.append(e.data.data); - player.setFrameRate(frameRate); - player.start(); - break; - case "loadMtsMjpeg": - player = new PlayerWorker(); - player.init(codecs.MTS_MJPEG); - player.append(e.data.data); - player.start(); - break; - case "appendMtsMjpeg": - player.append(e.data.data); - break; - default: - console.error("unknown message from main thread"); - break; - } -}; - -// PlayerWorker has a FrameBuffer to hold frames and once started, passes them one at a time to the main thread. -class PlayerWorker { - init(codec) { - this.frameRate = frameRate; - this.codec = codec; - switch (codec) { - case codecs.MJPEG: - this.frameSrc = new MJPEGLexer(); - break; - case codecs.MTS_MJPEG: - this.frameSrc = new FrameBuffer(); - break; - } - } - - setFrameRate(rate) { - this.frameRate = rate; - } - - start() { - let frame = this.frameSrc.read(); - if (frame != null) { - postMessage({ msg: "frame", data: frame.buffer }, [frame.buffer]); - setTimeout(() => { this.start(); }, 1000 / this.frameRate); - } else { - postMessage({ msg: "stop" }); - } - } - - append(data) { - this.frameSrc.append(data); - } -} - -// FrameBuffer allows an array of subarrays (MJPEG frames) to be read one at a time. -class FrameBuffer { - constructor() { - this.segments = []; - this.off = { segment: 0, frame: 0 }; - this.demuxer = new MTSDemuxer(); - } - - // read returns the next single frame. - read() { - let off = this.off; - let prevOff = off; - if (this.incrementOff()) { - return this.segments[prevOff.segment][prevOff.frame]; - } else { - return null; - } - } - - append(data) { - let demuxed = this.demuxer.demux(new Uint8Array(data)); - this.segments.push(demuxed.data); - } - - incrementOff() { - if (!this.segments || !this.segments[this.off.segment]) { - return false; - } - if (this.off.frame + 1 >= this.segments[this.off.segment].length) { - if (this.off.segment + 1 >= this.segments.length) { - return false; - } else { - this.off.segment++; - this.off.frame = 0; - return true; - } - } else { - this.off.frame++; - return true; - } - } -} \ No newline at end of file diff --git a/cmd/mjpeg-player/url-toolkit/url-toolkit.js b/cmd/mjpeg-player/url-toolkit/url-toolkit.js deleted file mode 100644 index 75c7f78a..00000000 --- a/cmd/mjpeg-player/url-toolkit/url-toolkit.js +++ /dev/null @@ -1,149 +0,0 @@ -// see https://tools.ietf.org/html/rfc1808 - -var URL_REGEX = /^((?:[a-zA-Z0-9+\-.]+:)?)(\/\/[^\/?#]*)?((?:[^\/\?#]*\/)*.*?)??(;.*?)?(\?.*?)?(#.*?)?$/; -var FIRST_SEGMENT_REGEX = /^([^\/?#]*)(.*)$/; -var SLASH_DOT_REGEX = /(?:\/|^)\.(?=\/)/g; -var SLASH_DOT_DOT_REGEX = /(?:\/|^)\.\.\/(?!\.\.\/).*?(?=\/)/g; - -var URLToolkit = { // jshint ignore:line - // If opts.alwaysNormalize is true then the path will always be normalized even when it starts with / or // - // E.g - // With opts.alwaysNormalize = false (default, spec compliant) - // http://a.com/b/cd + /e/f/../g => http://a.com/e/f/../g - // With opts.alwaysNormalize = true (not spec compliant) - // http://a.com/b/cd + /e/f/../g => http://a.com/e/g - buildAbsoluteURL: function (baseURL, relativeURL, opts) { - opts = opts || {}; - // remove any remaining space and CRLF - baseURL = baseURL.trim(); - relativeURL = relativeURL.trim(); - if (!relativeURL) { - // 2a) If the embedded URL is entirely empty, it inherits the - // entire base URL (i.e., is set equal to the base URL) - // and we are done. - if (!opts.alwaysNormalize) { - return baseURL; - } - var basePartsForNormalise = URLToolkit.parseURL(baseURL); - if (!basePartsForNormalise) { - throw new Error('Error trying to parse base URL.'); - } - basePartsForNormalise.path = URLToolkit.normalizePath(basePartsForNormalise.path); - return URLToolkit.buildURLFromParts(basePartsForNormalise); - } - var relativeParts = URLToolkit.parseURL(relativeURL); - if (!relativeParts) { - throw new Error('Error trying to parse relative URL.'); - } - if (relativeParts.scheme) { - // 2b) If the embedded URL starts with a scheme name, it is - // interpreted as an absolute URL and we are done. - if (!opts.alwaysNormalize) { - return relativeURL; - } - relativeParts.path = URLToolkit.normalizePath(relativeParts.path); - return URLToolkit.buildURLFromParts(relativeParts); - } - var baseParts = URLToolkit.parseURL(baseURL); - if (!baseParts) { - throw new Error('Error trying to parse base URL.'); - } - if (!baseParts.netLoc && baseParts.path && baseParts.path[0] !== '/') { - // If netLoc missing and path doesn't start with '/', assume everthing before the first '/' is the netLoc - // This causes 'example.com/a' to be handled as '//example.com/a' instead of '/example.com/a' - var pathParts = FIRST_SEGMENT_REGEX.exec(baseParts.path); - baseParts.netLoc = pathParts[1]; - baseParts.path = pathParts[2]; - } - if (baseParts.netLoc && !baseParts.path) { - baseParts.path = '/'; - } - var builtParts = { - // 2c) Otherwise, the embedded URL inherits the scheme of - // the base URL. - scheme: baseParts.scheme, - netLoc: relativeParts.netLoc, - path: null, - params: relativeParts.params, - query: relativeParts.query, - fragment: relativeParts.fragment - }; - if (!relativeParts.netLoc) { - // 3) If the embedded URL's is non-empty, we skip to - // Step 7. Otherwise, the embedded URL inherits the - // (if any) of the base URL. - builtParts.netLoc = baseParts.netLoc; - // 4) If the embedded URL path is preceded by a slash "/", the - // path is not relative and we skip to Step 7. - if (relativeParts.path[0] !== '/') { - if (!relativeParts.path) { - // 5) If the embedded URL path is empty (and not preceded by a - // slash), then the embedded URL inherits the base URL path - builtParts.path = baseParts.path; - // 5a) if the embedded URL's is non-empty, we skip to - // step 7; otherwise, it inherits the of the base - // URL (if any) and - if (!relativeParts.params) { - builtParts.params = baseParts.params; - // 5b) if the embedded URL's is non-empty, we skip to - // step 7; otherwise, it inherits the of the base - // URL (if any) and we skip to step 7. - if (!relativeParts.query) { - builtParts.query = baseParts.query; - } - } - } else { - // 6) The last segment of the base URL's path (anything - // following the rightmost slash "/", or the entire path if no - // slash is present) is removed and the embedded URL's path is - // appended in its place. - var baseURLPath = baseParts.path; - var newPath = baseURLPath.substring(0, baseURLPath.lastIndexOf('/') + 1) + relativeParts.path; - builtParts.path = URLToolkit.normalizePath(newPath); - } - } - } - if (builtParts.path === null) { - builtParts.path = opts.alwaysNormalize ? URLToolkit.normalizePath(relativeParts.path) : relativeParts.path; - } - return URLToolkit.buildURLFromParts(builtParts); - }, - parseURL: function (url) { - var parts = URL_REGEX.exec(url); - if (!parts) { - return null; - } - return { - scheme: parts[1] || '', - netLoc: parts[2] || '', - path: parts[3] || '', - params: parts[4] || '', - query: parts[5] || '', - fragment: parts[6] || '' - }; - }, - normalizePath: function (path) { - // The following operations are - // then applied, in order, to the new path: - // 6a) All occurrences of "./", where "." is a complete path - // segment, are removed. - // 6b) If the path ends with "." as a complete path segment, - // that "." is removed. - path = path.split('').reverse().join('').replace(SLASH_DOT_REGEX, ''); - // 6c) All occurrences of "/../", where is a - // complete path segment not equal to "..", are removed. - // Removal of these path segments is performed iteratively, - // removing the leftmost matching pattern on each iteration, - // until no matching pattern remains. - // 6d) If the path ends with "/..", where is a - // complete path segment not equal to "..", that - // "/.." is removed. - while (path.length !== (path = path.replace(SLASH_DOT_DOT_REGEX, '')).length) { } // jshint ignore:line - return path.split('').reverse().join(''); - }, - buildURLFromParts: function (parts) { - return parts.scheme + parts.netLoc + parts.path + parts.params + parts.query + parts.fragment; - } -}; - -export default URLToolkit;