remove cmd/mjpeg-player from av to add to vidgrind repo

This commit is contained in:
Trek H 2020-02-06 17:37:14 +10:30
parent 4e60728a0f
commit cbf8c98c87
29 changed files with 0 additions and 3971 deletions

View File

@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2014 Arnout Kazemier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -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;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@ -1,28 +0,0 @@
Copyright (c) 2017 Dailymotion (http://www.dailymotion.com)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
src/remux/mp4-generator.js and src/demux/exp-golomb.js implementation in this project
are derived from the HLS library for video.js (https://github.com/videojs/videojs-contrib-hls)
That work is also covered by the Apache 2 License, following copyright:
Copyright (c) 2013-2015 Brightcove
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -1,48 +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 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 = {
startPosition: -1, // used by stream-controller
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
loader: XhrLoader,
fLoader: void 0, // used by fragment-loader
pLoader: void 0, // used by playlist-loader
xhrSetup: void 0, // used by xhr-loader
};

View File

@ -1,280 +0,0 @@
/*
* Level Controller
*/
import Event from '../events.js';
import EventHandler from '../event-handler.js';
import { addGroupId, computeReloadInterval } from './level-helper.js';
const { performance } = window;
let chromeOrFirefox;
export default class LevelController extends EventHandler {
constructor(hls) {
super(hls,
Event.MANIFEST_LOADED,
Event.LEVEL_LOADED);
this.canload = false;
this.curLvlIdx = 0;
this.manualLvlIdx = -1;
this.timer = null;
chromeOrFirefox = /chrome|firefox/.test(navigator.userAgent.toLowerCase());
}
onHandlerDestroying() {
this.clearTimer();
this.manualLvlIdx = -1;
}
clearTimer() {
if (this.timer !== null) {
clearTimeout(this.timer);
this.timer = null;
}
}
startLoad() {
let levels = this._levels;
this.canload = true;
this.levelRetryCount = 0;
// clean up live level details to force reload them, and reset load errors
if (levels) {
levels.forEach(level => {
level.loadError = 0;
const levelDetails = level.details;
if (levelDetails && levelDetails.live) {
level.details = undefined;
}
});
}
// speed up live playlist refresh if timer exists
if (this.timer !== null) {
this.loadLevel();
}
}
stopLoad() {
this.canload = false;
}
onManifestLoaded(data) {
let levels = [];
let audioTracks = [];
let bitrateStart;
let levelSet = {};
let levelFromSet = null;
let videoCodecFound = false;
let audioCodecFound = false;
// regroup redundant levels together
data.levels.forEach(level => {
const attributes = level.attrs;
level.loadError = 0;
level.fragmentError = false;
videoCodecFound = videoCodecFound || !!level.videoCodec;
audioCodecFound = audioCodecFound || !!level.audioCodec;
levelFromSet = levelSet[level.bitrate]; // FIXME: we would also have to match the resolution here
if (!levelFromSet) {
level.url = [level.url];
level.urlId = 0;
levelSet[level.bitrate] = level;
levels.push(level);
} else {
levelFromSet.url.push(level.url);
}
if (attributes) {
if (attributes.AUDIO) {
audioCodecFound = true;
addGroupId(levelFromSet || level, 'audio', attributes.AUDIO);
}
if (attributes.SUBTITLES) {
addGroupId(levelFromSet || level, 'text', attributes.SUBTITLES);
}
}
});
if (levels.length > 0) {
// start bitrate is the first bitrate of the manifest
bitrateStart = levels[0].bitrate;
// sort level on bitrate
levels.sort((a, b) => a.bitrate - b.bitrate);
this._levels = levels;
// find index of first level in sorted levels
for (let i = 0; i < levels.length; i++) {
if (levels[i].bitrate === bitrateStart) {
this._firstLevel = i;
break;
}
}
// Audio is only alternate if manifest include a URI along with the audio group tag
this.hls.trigger(Event.MANIFEST_PARSED, {
levels,
audioTracks,
firstLevel: this._firstLevel,
stats: data.stats,
audio: audioCodecFound,
video: videoCodecFound,
altAudio: audioTracks.some(t => !!t.url)
});
} else {
this.hls.trigger(Event.ERROR, {
type: ErrorTypes.MEDIA_ERROR,
details: ErrorDetails.MANIFEST_INCOMPATIBLE_CODECS_ERROR,
fatal: true,
url: this.hls.url,
reason: 'no level with compatible codecs found in manifest'
});
}
}
get levels() {
return this._levels;
}
get level() {
return this.curLvlIdx;
}
set level(newLevel) {
let levels = this._levels;
if (levels) {
newLevel = Math.min(newLevel, levels.length - 1);
if (this.curLvlIdx !== newLevel || !levels[newLevel].details) {
this.setLevelInternal(newLevel);
}
}
}
setLevelInternal(newLevel) {
const levels = this._levels;
const hls = this.hls;
// check if level idx is valid
if (newLevel >= 0 && newLevel < levels.length) {
// stopping live reloading timer if any
this.clearTimer();
if (this.curLvlIdx !== newLevel) {
console.log(`switching to level ${newLevel}`);
this.curLvlIdx = newLevel;
const levelProperties = levels[newLevel];
levelProperties.level = newLevel;
hls.trigger(Event.LEVEL_SWITCHING, levelProperties);
}
const level = levels[newLevel];
const levelDetails = level.details;
// check if we need to load playlist for this level
if (!levelDetails || levelDetails.live) {
// level not retrieved yet, or live playlist we need to (re)load it
let urlId = level.urlId;
hls.trigger(Event.LEVEL_LOADING, { url: level.url[urlId], level: newLevel, id: urlId });
}
} else {
// invalid level id given, trigger error
hls.trigger(Event.ERROR, {
type: ErrorTypes.OTHER_ERROR,
details: ErrorDetails.LEVEL_SWITCH_ERROR,
level: newLevel,
fatal: false,
reason: 'invalid level idx'
});
}
}
get manualLevel() {
return this.manualLvlIdx;
}
set manualLevel(newLevel) {
this.manualLvlIdx = newLevel;
if (this._startLevel === undefined) {
this._startLevel = newLevel;
}
if (newLevel !== -1) {
this.level = newLevel;
}
}
get firstLevel() {
return this._firstLevel;
}
set firstLevel(newLevel) {
this._firstLevel = newLevel;
}
get startLevel() {
// hls.startLevel takes precedence over config.startLevel
// if none of these values are defined, fallback on this._firstLevel (first quality level appearing in variant manifest)
if (this._startLevel === undefined) {
let configStartLevel = this.hls.config.startLevel;
if (configStartLevel !== undefined) {
return configStartLevel;
} else {
return this._firstLevel;
}
} else {
return this._startLevel;
}
}
set startLevel(newLevel) {
this._startLevel = newLevel;
}
onLevelLoaded(data) {
const { level, details } = data;
const curLevel = this._levels[level];
// if current playlist is a live playlist, arm a timer to reload it
if (details.live) {
const reloadInterval = computeReloadInterval(curLevel.details, details, data.stats.trequest);
console.log(`live playlist, reload in ${Math.round(reloadInterval)} ms`);
this.timer = setTimeout(() => this.loadLevel(), reloadInterval);
} else {
this.clearTimer();
}
}
loadLevel() {
console.log('call to loadLevel, index: ' + this.curLvlIdx + "canload: " + this.canload);
if (this.curLvlIdx !== null && this.canload) {
const levelObject = this._levels[this.curLvlIdx];
if (typeof levelObject === 'object' &&
levelObject.url.length > 0) {
const level = this.curLvlIdx;
const id = levelObject.urlId;
const url = levelObject.url[id];
console.log(`Attempt loading level index ${level} with URL-id ${id}`);
this.hls.trigger(Event.LEVEL_LOADING, { url, level, id });
}
}
}
get nextLoadLevel() {
if (this.manualLvlIdx !== -1) {
return this.manualLvlIdx;
} else {
return this.hls.nextAutoLevel;
}
}
set nextLoadLevel(nextLevel) {
this.level = nextLevel;
if (this.manualLvlIdx === -1) {
this.hls.nextAutoLevel = nextLevel;
}
}
}

View File

@ -1,227 +0,0 @@
/**
* @module LevelHelper
*
* Providing methods dealing with playlist sliding and drift
*
* TODO: Create an actual `Level` class/model that deals with all this logic in an object-oriented-manner.
*
* */
export function addGroupId(level, type, id) {
switch (type) {
case 'audio':
if (!level.audioGroupIds) {
level.audioGroupIds = [];
}
level.audioGroupIds.push(id);
break;
case 'text':
if (!level.textGroupIds) {
level.textGroupIds = [];
}
level.textGroupIds.push(id);
break;
}
}
export function updatePTS(fragments, fromIdx, toIdx) {
let fragFrom = fragments[fromIdx], fragTo = fragments[toIdx], fragToPTS = fragTo.startPTS;
// if we know startPTS[toIdx]
if (Number.isFinite(fragToPTS)) {
// update fragment duration.
// it helps to fix drifts between playlist reported duration and fragment real duration
if (toIdx > fromIdx) {
fragFrom.duration = fragToPTS - fragFrom.start;
if (fragFrom.duration < 0) {
console.warn(`negative duration computed for frag ${fragFrom.sn},level ${fragFrom.level}, there should be some duration drift between playlist and fragment!`);
}
} else {
fragTo.duration = fragFrom.start - fragToPTS;
if (fragTo.duration < 0) {
console.warn(`negative duration computed for frag ${fragTo.sn},level ${fragTo.level}, there should be some duration drift between playlist and fragment!`);
}
}
} else {
// we dont know startPTS[toIdx]
if (toIdx > fromIdx) {
fragTo.start = fragFrom.start + fragFrom.duration;
} else {
fragTo.start = Math.max(fragFrom.start - fragTo.duration, 0);
}
}
}
export function updateFragPTSDTS(details, frag, startPTS, endPTS, startDTS, endDTS) {
// update frag PTS/DTS
let maxStartPTS = startPTS;
if (Number.isFinite(frag.startPTS)) {
// delta PTS between audio and video
let deltaPTS = Math.abs(frag.startPTS - startPTS);
if (!Number.isFinite(frag.deltaPTS)) {
frag.deltaPTS = deltaPTS;
} else {
frag.deltaPTS = Math.max(deltaPTS, frag.deltaPTS);
}
maxStartPTS = Math.max(startPTS, frag.startPTS);
startPTS = Math.min(startPTS, frag.startPTS);
endPTS = Math.max(endPTS, frag.endPTS);
startDTS = Math.min(startDTS, frag.startDTS);
endDTS = Math.max(endDTS, frag.endDTS);
}
const drift = startPTS - frag.start;
frag.start = frag.startPTS = startPTS;
frag.maxStartPTS = maxStartPTS;
frag.endPTS = endPTS;
frag.startDTS = startDTS;
frag.endDTS = endDTS;
frag.duration = endPTS - startPTS;
const sn = frag.sn;
// exit if sn out of range
if (!details || sn < details.startSN || sn > details.endSN) {
return 0;
}
let fragIdx, fragments, i;
fragIdx = sn - details.startSN;
fragments = details.fragments;
// update frag reference in fragments array
// rationale is that fragments array might not contain this frag object.
// this will happen if playlist has been refreshed between frag loading and call to updateFragPTSDTS()
// if we don't update frag, we won't be able to propagate PTS info on the playlist
// resulting in invalid sliding computation
fragments[fragIdx] = frag;
// adjust fragment PTS/duration from seqnum-1 to frag 0
for (i = fragIdx; i > 0; i--) {
updatePTS(fragments, i, i - 1);
}
// adjust fragment PTS/duration from seqnum to last frag
for (i = fragIdx; i < fragments.length - 1; i++) {
updatePTS(fragments, i, i + 1);
}
details.PTSKnown = true;
return drift;
}
export function mergeDetails(oldDetails, newDetails) {
// potentially retrieve cached initsegment
if (newDetails.initSegment && oldDetails.initSegment) {
newDetails.initSegment = oldDetails.initSegment;
}
// check if old/new playlists have fragments in common
// loop through overlapping SN and update startPTS , cc, and duration if any found
let ccOffset = 0;
let PTSFrag;
mapFragmentIntersection(oldDetails, newDetails, (oldFrag, newFrag) => {
ccOffset = oldFrag.cc - newFrag.cc;
if (Number.isFinite(oldFrag.startPTS)) {
newFrag.start = newFrag.startPTS = oldFrag.startPTS;
newFrag.endPTS = oldFrag.endPTS;
newFrag.duration = oldFrag.duration;
newFrag.backtracked = oldFrag.backtracked;
newFrag.dropped = oldFrag.dropped;
PTSFrag = newFrag;
}
// PTS is known when there are overlapping segments
newDetails.PTSKnown = true;
});
if (!newDetails.PTSKnown) {
return;
}
if (ccOffset) {
console.log('discontinuity sliding from playlist, take drift into account');
const newFragments = newDetails.fragments;
for (let i = 0; i < newFragments.length; i++) {
newFragments[i].cc += ccOffset;
}
}
// if at least one fragment contains PTS info, recompute PTS information for all fragments
if (PTSFrag) {
updateFragPTSDTS(newDetails, PTSFrag, PTSFrag.startPTS, PTSFrag.endPTS, PTSFrag.startDTS, PTSFrag.endDTS);
} else {
// ensure that delta is within oldFragments range
// also adjust sliding in case delta is 0 (we could have old=[50-60] and new=old=[50-61])
// in that case we also need to adjust start offset of all fragments
adjustSliding(oldDetails, newDetails);
}
// if we are here, it means we have fragments overlapping between
// old and new level. reliable PTS info is thus relying on old level
newDetails.PTSKnown = oldDetails.PTSKnown;
}
export function mergeSubtitlePlaylists(oldPlaylist, newPlaylist, referenceStart = 0) {
let lastIndex = -1;
mapFragmentIntersection(oldPlaylist, newPlaylist, (oldFrag, newFrag, index) => {
newFrag.start = oldFrag.start;
lastIndex = index;
});
const frags = newPlaylist.fragments;
if (lastIndex < 0) {
frags.forEach(frag => {
frag.start += referenceStart;
});
return;
}
for (let i = lastIndex + 1; i < frags.length; i++) {
frags[i].start = (frags[i - 1].start + frags[i - 1].duration);
}
}
export function mapFragmentIntersection(oldPlaylist, newPlaylist, intersectionFn) {
if (!oldPlaylist || !newPlaylist) {
return;
}
const start = Math.max(oldPlaylist.startSN, newPlaylist.startSN) - newPlaylist.startSN;
const end = Math.min(oldPlaylist.endSN, newPlaylist.endSN) - newPlaylist.startSN;
const delta = newPlaylist.startSN - oldPlaylist.startSN;
for (let i = start; i <= end; i++) {
const oldFrag = oldPlaylist.fragments[delta + i];
const newFrag = newPlaylist.fragments[i];
if (!oldFrag || !newFrag) {
break;
}
intersectionFn(oldFrag, newFrag, i);
}
}
export function adjustSliding(oldPlaylist, newPlaylist) {
const delta = newPlaylist.startSN - oldPlaylist.startSN;
const oldFragments = oldPlaylist.fragments;
const newFragments = newPlaylist.fragments;
if (delta < 0 || delta > oldFragments.length) {
return;
}
for (let i = 0; i < newFragments.length; i++) {
newFragments[i].start += oldFragments[delta].start;
}
}
export function computeReloadInterval(currentPlaylist, newPlaylist, lastRequestTime) {
let reloadInterval = 1000 * (newPlaylist.averagetargetduration ? newPlaylist.averagetargetduration : newPlaylist.targetduration);
const minReloadInterval = reloadInterval / 2;
if (currentPlaylist && newPlaylist.endSN === currentPlaylist.endSN) {
// follow HLS Spec, If the client reloads a Playlist file and finds that it has not
// changed then it MUST wait for a period of one-half the target
// duration before retrying.
reloadInterval = minReloadInterval;
}
if (lastRequestTime) {
reloadInterval = Math.max(minReloadInterval, reloadInterval - (window.performance.now() - lastRequestTime));
}
// in any case, don't reload more than half of target duration
return Math.round(reloadInterval);
}

View File

@ -1,113 +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.
*/
/*
* 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.currentFragIdx = 0;
this.lastSN = 0;
this.fragments = [];
}
_fetchPayloadOrEos(levelDetails) {
// Keep track of any new frags and load them.
for (let i = 0; i < levelDetails.fragments.length; i++) {
let frag = levelDetails.fragments[i];
if (frag.sn > this.lastSN) {
console.log("adding fragment: " + frag.sn);
this.fragments.push(frag);
this.lastSN = frag.sn;
}
}
this._loadFragment();
}
_loadFragment() {
let fragLen = this.fragments.length;
if (this.currentFragIdx >= fragLen) {
return;
}
this.hls.trigger(Event.FRAG_LOADING, { frag: this.fragments[this.currentFragIdx++] });
}
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}`);
// 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;

View File

@ -1,105 +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.
*/
/*
*
* 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;

View File

@ -1,55 +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}
*/
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;

View File

@ -1,53 +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 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 LevelController from './controller/level-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.levelController = new LevelController(this);
this.fragmentLoader = new FragmentLoader(this);
this.config = hlsDefaultConfig;
}
// url is the source URL. Can be relative or absolute.
loadSource(url, callback) {
this.levelController.startLoad();
this.loadSuccess = callback;
url = URLToolkit.buildAbsoluteURL(window.location.href, url, { alwaysNormalize: true });
this.trigger(HlsEvents.MANIFEST_LOADING, { url: url });
}
}
export default Hls

View File

@ -1,137 +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.
*/
/*
* 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;

View File

@ -1,222 +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 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;
}
}

View File

@ -1,47 +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 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;
}
}

View File

@ -1,23 +0,0 @@
export default class Level {
constructor (baseUrl) {
// Please keep properties in alphabetical order
this.endCC = 0;
this.endSN = 0;
this.fragments = [];
this.initSegment = null;
this.live = true;
this.needSidxRanges = false;
this.startCC = 0;
this.startSN = 0;
this.startTimeOffset = null;
this.targetduration = 0;
this.totalduration = 0;
this.type = null;
this.url = baseUrl;
this.version = null;
}
get hasProgramDateTime () {
return !!(this.fragments[0] && Number.isFinite(this.fragments[0].programDateTime));
}
}

View File

@ -1,388 +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 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:<duration>,<title>), 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;
}
}

View File

@ -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);
}
/**
* 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;
}
}
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;
}
/**
* 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;
}
getInternalLoader(context) {
return this.loaders[context.type];
}
resetInternalLoader(contextType) {
if (this.loaders[contextType]) {
delete this.loaders[contextType];
}
}
onManifestLoading(data) {
this.load({
url: data.url,
type: PlaylistContextType.MANIFEST,
level: 0,
id: null,
responseType: 'text'
});
}
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'
});
}
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 {
console.warn(`aborting previous loader for type: ${context.type}`);
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;

View File

@ -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;
}
}
}

View File

@ -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);
}
}

View File

@ -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'
}

View File

@ -1,89 +0,0 @@
const DECIMAL_RESOLUTION_REGEX = /^(\d+)x(\d+)$/; // eslint-disable-line no-useless-escape
const ATTR_LIST_REGEX = /\s*(.+?)\s*=((?:\".*?\")|.*?)(?:,|$)/g; // eslint-disable-line no-useless-escape
// adapted from https://github.com/kanongil/node-m3u8parse/blob/master/attrlist.js
class AttrList {
constructor (attrs) {
if (typeof attrs === 'string') {
attrs = AttrList.parseAttrList(attrs);
}
for (let attr in attrs) {
if (attrs.hasOwnProperty(attr)) {
this[attr] = attrs[attr];
}
}
}
decimalInteger (attrName) {
const intValue = parseInt(this[attrName], 10);
if (intValue > Number.MAX_SAFE_INTEGER) {
return Infinity;
}
return intValue;
}
hexadecimalInteger (attrName) {
if (this[attrName]) {
let stringValue = (this[attrName] || '0x').slice(2);
stringValue = ((stringValue.length & 1) ? '0' : '') + stringValue;
const value = new Uint8Array(stringValue.length / 2);
for (let i = 0; i < stringValue.length / 2; i++) {
value[i] = parseInt(stringValue.slice(i * 2, i * 2 + 2), 16);
}
return value;
} else {
return null;
}
}
hexadecimalIntegerAsNumber (attrName) {
const intValue = parseInt(this[attrName], 16);
if (intValue > Number.MAX_SAFE_INTEGER) {
return Infinity;
}
return intValue;
}
decimalFloatingPoint (attrName) {
return parseFloat(this[attrName]);
}
enumeratedString (attrName) {
return this[attrName];
}
decimalResolution (attrName) {
const res = DECIMAL_RESOLUTION_REGEX.exec(this[attrName]);
if (res === null) {
return undefined;
}
return {
width: parseInt(res[1], 10),
height: parseInt(res[2], 10)
};
}
static parseAttrList (input) {
let match, attrs = {};
ATTR_LIST_REGEX.lastIndex = 0;
while ((match = ATTR_LIST_REGEX.exec(input)) !== null) {
let value = match[2], quote = '"';
if (value.indexOf(quote) === 0 &&
value.lastIndexOf(quote) === (value.length - 1)) {
value = value.slice(1, -1);
}
attrs[match[1]] = value;
}
return attrs;
}
}
export default AttrList;

View File

@ -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 };

View File

@ -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;

View File

@ -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</title>
<script type="module" src="./main.js"></script>
</head>
<body style="height: 100%">
<div style="width: 40rem;">
<div>
<div>
<div>
<input type="file" id="fileInput">
</div>
</div>
<div>
<label>Playlist URL: </label>
<input type="url" id="url" placeholder="Enter URL">
<button id="urlBtn" type="submit">Load</button>
</div>
<div>
Frame Rate: <input type="text" id="rate" value="25"> fps
</div>
<div>
<img src="" id="viewer" style="max-width:100%; height:auto;">
</div>
</div>
</div>
<footer id="sticky-footer" style="width: 100%;">
<div>
<small>&copy;2020 Australian Ocean Laboratory Limited (AusOcean) (<a rel="license" href="https://www.ausocean.org/license">License</a>)</small>
</div>
</footer>
</body>
</html>

View File

@ -1,63 +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.
*/
// MJPEGLexer lexes a byte array containing MJPEG into individual JPEGs.
class MJPEGLexer {
constructor() {
this.off = 0;
}
append(data) {
this.src = new Uint8Array(data);
}
// read returns the next single frame.
read() {
// Check if the src can contain at least the start and end flags (4B).
if (this.off + 4 > this.src.length) {
return null;
}
// Iterate through bytes until the start flag is found.
while (this.src[this.off] != 0xff || this.src[this.off + 1] != 0xd8) {
this.off++;
if (this.off + 4 > this.src.length) {
return null;
}
}
// Start after the start flag and loop until the end flag is found.
let end = this.off + 2;
while (true) {
if (end + 2 > this.src.length) {
return null;
}
if (this.src[end] == 0xff && this.src[end + 1] == 0xd9) {
break;
}
end++;
}
// Copy the frame's bytes to a new array to return.
// Note: optimally this would return a reference but since we are in a worker thread,
// the main thread doesn't have access to the ArrayBuffer that we are working with.
let frame = this.src.slice(this.off, end + 2);
this.off = end + 2
return frame;
}
}

View File

@ -1,132 +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.
*/
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 == "") {
// Get everything following the ? from the browser's URL bar.
url = window.location.search.slice(1);
document.getElementById('url').value = url;
} else if (url[0] == '/') {
url = window.location.protocol + '//' + window.location.host + url;
}
if (url == "") {
return;
}
let hls = new Hls();
hls.loadSource(url, append);
}
// 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 > 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;
}
}

View File

@ -1,132 +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.
*/
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);
}
}
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;
}
}
}

View File

@ -1,190 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2016 Tom Jenkinson
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -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 <net_loc> is non-empty, we skip to
// Step 7. Otherwise, the embedded URL inherits the <net_loc>
// (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 <params> is non-empty, we skip to
// step 7; otherwise, it inherits the <params> of the base
// URL (if any) and
if (!relativeParts.params) {
builtParts.params = baseParts.params;
// 5b) if the embedded URL's <query> is non-empty, we skip to
// step 7; otherwise, it inherits the <query> 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 "<segment>/../", where <segment> 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 "<segment>/..", where <segment> is a
// complete path segment not equal to "..", that
// "<segment>/.." 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;