From c6a096f5a076ac6e6ecd8ec1dbcce5acb4bfebfd Mon Sep 17 00:00:00 2001 From: Trek H Date: Fri, 24 Jan 2020 15:24:29 +1030 Subject: [PATCH] mjpeg-player: added reloading of live playlists Reduced level-contoller.js to keep only what we need (reloading of live playlists). Added a LevelController to Hls in hls.js. Removed stop message from player.js which was causing false stops. Minor changes to level-helper.js (formatting and logger). --- .../hlsjs/controller/level-controller.js | 241 +----- .../hlsjs/controller/level-helper.js | 48 +- cmd/mjpeg-player/hlsjs/hls.js | 697 +----------------- cmd/mjpeg-player/player.js | 151 ++-- 4 files changed, 188 insertions(+), 949 deletions(-) diff --git a/cmd/mjpeg-player/hlsjs/controller/level-controller.js b/cmd/mjpeg-player/hlsjs/controller/level-controller.js index 9360f57f..8cef6d48 100644 --- a/cmd/mjpeg-player/hlsjs/controller/level-controller.js +++ b/cmd/mjpeg-player/hlsjs/controller/level-controller.js @@ -2,46 +2,40 @@ * Level Controller */ -import Event from '../events'; -import EventHandler from '../event-handler'; -import { logger } from '../utils/logger'; -import { ErrorTypes, ErrorDetails } from '../errors'; -import { isCodecSupportedInMp4 } from '../utils/codecs'; -import { addGroupId, computeReloadInterval } from './level-helper'; +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) { + constructor(hls) { super(hls, Event.MANIFEST_LOADED, - Event.LEVEL_LOADED, - Event.AUDIO_TRACK_SWITCHED, - Event.FRAG_LOADED, - Event.ERROR); + Event.LEVEL_LOADED); this.canload = false; - this.currentLevelIndex = null; + this.currentLevelIndex = 0; this.manualLevelIndex = -1; this.timer = null; chromeOrFirefox = /chrome|firefox/.test(navigator.userAgent.toLowerCase()); } - onHandlerDestroying () { + onHandlerDestroying() { this.clearTimer(); this.manualLevelIndex = -1; } - clearTimer () { + clearTimer() { if (this.timer !== null) { clearTimeout(this.timer); this.timer = null; } } - startLoad () { + startLoad() { let levels = this._levels; this.canload = true; @@ -63,11 +57,11 @@ export default class LevelController extends EventHandler { } } - stopLoad () { + stopLoad() { this.canload = false; } - onManifestLoaded (data) { + onManifestLoaded(data) { let levels = []; let audioTracks = []; let bitrateStart; @@ -113,24 +107,6 @@ export default class LevelController extends EventHandler { } }); - // remove audio-only level if we also have levels with audio+video codecs signalled - if (videoCodecFound && audioCodecFound) { - levels = levels.filter(({ videoCodec }) => !!videoCodec); - } - - // only keep levels with supported audio/video codecs - levels = levels.filter(({ audioCodec, videoCodec }) => { - return (!audioCodec || isCodecSupportedInMp4(audioCodec, 'audio')) && (!videoCodec || isCodecSupportedInMp4(videoCodec, 'video')); - }); - - if (data.audioTracks) { - audioTracks = data.audioTracks.filter(track => !track.audioCodec || isCodecSupportedInMp4(track.audioCodec, 'audio')); - // Reassign id's after filtering since they're used as array indices - audioTracks.forEach((track, index) => { - track.id = index; - }); - } - if (levels.length > 0) { // start bitrate is the first bitrate of the manifest bitrateStart = levels[0].bitrate; @@ -141,7 +117,7 @@ export default class LevelController extends EventHandler { for (let i = 0; i < levels.length; i++) { if (levels[i].bitrate === bitrateStart) { this._firstLevel = i; - logger.log(`manifest loaded,${levels.length} level(s) found, first bitrate:${bitrateStart}`); + // console.log(`manifest loaded,${levels.length} level(s) found, first bitrate:${bitrateStart}`); break; } } @@ -167,15 +143,15 @@ export default class LevelController extends EventHandler { } } - get levels () { + get levels() { return this._levels; } - get level () { + get level() { return this.currentLevelIndex; } - set level (newLevel) { + set level(newLevel) { let levels = this._levels; if (levels) { newLevel = Math.min(newLevel, levels.length - 1); @@ -185,7 +161,7 @@ export default class LevelController extends EventHandler { } } - setLevelInternal (newLevel) { + setLevelInternal(newLevel) { const levels = this._levels; const hls = this.hls; // check if level idx is valid @@ -193,7 +169,7 @@ export default class LevelController extends EventHandler { // stopping live reloading timer if any this.clearTimer(); if (this.currentLevelIndex !== newLevel) { - logger.log(`switching to level ${newLevel}`); + console.log(`switching to level ${newLevel}`); this.currentLevelIndex = newLevel; const levelProperties = levels[newLevel]; levelProperties.level = newLevel; @@ -220,11 +196,11 @@ export default class LevelController extends EventHandler { } } - get manualLevel () { + get manualLevel() { return this.manualLevelIndex; } - set manualLevel (newLevel) { + set manualLevel(newLevel) { this.manualLevelIndex = newLevel; if (this._startLevel === undefined) { this._startLevel = newLevel; @@ -235,15 +211,15 @@ export default class LevelController extends EventHandler { } } - get firstLevel () { + get firstLevel() { return this._firstLevel; } - set firstLevel (newLevel) { + set firstLevel(newLevel) { this._firstLevel = newLevel; } - get startLevel () { + 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) { @@ -258,179 +234,25 @@ export default class LevelController extends EventHandler { } } - set startLevel (newLevel) { + set startLevel(newLevel) { this._startLevel = newLevel; } - onError (data) { - if (data.fatal) { - if (data.type === ErrorTypes.NETWORK_ERROR) { - this.clearTimer(); - } - - return; - } - - let levelError = false, fragmentError = false; - let levelIndex; - - // try to recover not fatal errors - switch (data.details) { - case ErrorDetails.FRAG_LOAD_ERROR: - case ErrorDetails.FRAG_LOAD_TIMEOUT: - case ErrorDetails.KEY_LOAD_ERROR: - case ErrorDetails.KEY_LOAD_TIMEOUT: - levelIndex = data.frag.level; - fragmentError = true; - break; - case ErrorDetails.LEVEL_LOAD_ERROR: - case ErrorDetails.LEVEL_LOAD_TIMEOUT: - levelIndex = data.context.level; - levelError = true; - break; - case ErrorDetails.REMUX_ALLOC_ERROR: - levelIndex = data.level; - levelError = true; - break; - } - - if (levelIndex !== undefined) { - this.recoverLevel(data, levelIndex, levelError, fragmentError); - } - } - - /** - * Switch to a redundant stream if any available. - * If redundant stream is not available, emergency switch down if ABR mode is enabled. - * - * @param {Object} errorEvent - * @param {Number} levelIndex current level index - * @param {Boolean} levelError - * @param {Boolean} fragmentError - */ - // FIXME Find a better abstraction where fragment/level retry management is well decoupled - recoverLevel (errorEvent, levelIndex, levelError, fragmentError) { - let { config } = this.hls; - let { details: errorDetails } = errorEvent; - let level = this._levels[levelIndex]; - let redundantLevels, delay, nextLevel; - - level.loadError++; - level.fragmentError = fragmentError; - - if (levelError) { - if ((this.levelRetryCount + 1) <= config.levelLoadingMaxRetry) { - // exponential backoff capped to max retry timeout - delay = Math.min(Math.pow(2, this.levelRetryCount) * config.levelLoadingRetryDelay, config.levelLoadingMaxRetryTimeout); - // Schedule level reload - this.timer = setTimeout(() => this.loadLevel(), delay); - // boolean used to inform stream controller not to switch back to IDLE on non fatal error - errorEvent.levelRetry = true; - this.levelRetryCount++; - logger.warn(`level controller, ${errorDetails}, retry in ${delay} ms, current retry count is ${this.levelRetryCount}`); - } else { - logger.error(`level controller, cannot recover from ${errorDetails} error`); - this.currentLevelIndex = null; - // stopping live reloading timer if any - this.clearTimer(); - // switch error to fatal - errorEvent.fatal = true; - return; - } - } - - // Try any redundant streams if available for both errors: level and fragment - // If level.loadError reaches redundantLevels it means that we tried them all, no hope => let's switch down - if (levelError || fragmentError) { - redundantLevels = level.url.length; - - if (redundantLevels > 1 && level.loadError < redundantLevels) { - level.urlId = (level.urlId + 1) % redundantLevels; - level.details = undefined; - - logger.warn(`level controller, ${errorDetails} for level ${levelIndex}: switching to redundant URL-id ${level.urlId}`); - - // console.log('Current audio track group ID:', this.hls.audioTracks[this.hls.audioTrack].groupId); - // console.log('New video quality level audio group id:', level.attrs.AUDIO); - } else { - // Search for available level - if (this.manualLevelIndex === -1) { - // When lowest level has been reached, let's start hunt from the top - nextLevel = (levelIndex === 0) ? this._levels.length - 1 : levelIndex - 1; - logger.warn(`level controller, ${errorDetails}: switch to ${nextLevel}`); - this.hls.nextAutoLevel = this.currentLevelIndex = nextLevel; - } else if (fragmentError) { - // Allow fragment retry as long as configuration allows. - // reset this._level so that another call to set level() will trigger again a frag load - logger.warn(`level controller, ${errorDetails}: reload a fragment`); - this.currentLevelIndex = null; - } - } - } - } - - // reset errors on the successful load of a fragment - onFragLoaded ({ frag }) { - if (frag !== undefined && frag.type === 'main') { - const level = this._levels[frag.level]; - if (level !== undefined) { - level.fragmentError = false; - level.loadError = 0; - this.levelRetryCount = 0; - } - } - } - - onLevelLoaded (data) { + onLevelLoaded(data) { const { level, details } = data; - // only process level loaded events matching with expected level - if (level !== this.currentLevelIndex) { - return; - } - const curLevel = this._levels[level]; - // reset level load error counter on successful level loaded only if there is no issues with fragments - if (!curLevel.fragmentError) { - curLevel.loadError = 0; - this.levelRetryCount = 0; - } // 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); - logger.log(`live playlist, reload in ${Math.round(reloadInterval)} ms`); + console.log(`live playlist, reload in ${Math.round(reloadInterval)} ms`); this.timer = setTimeout(() => this.loadLevel(), reloadInterval); } else { this.clearTimer(); } } - onAudioTrackSwitched (data) { - const audioGroupId = this.hls.audioTracks[data.id].groupId; - - const currentLevel = this.hls.levels[this.currentLevelIndex]; - if (!currentLevel) { - return; - } - - if (currentLevel.audioGroupIds) { - let urlId = -1; - - for (let i = 0; i < currentLevel.audioGroupIds.length; i++) { - if (currentLevel.audioGroupIds[i] === audioGroupId) { - urlId = i; - break; - } - } - - if (urlId !== currentLevel.urlId) { - currentLevel.urlId = urlId; - this.startLoad(); - } - } - } - - loadLevel () { - logger.debug('call to loadLevel'); + loadLevel() { + console.log('call to loadLevel, index: ' + this.currentLevelIndex + "canload: " + this.canload); if (this.currentLevelIndex !== null && this.canload) { const levelObject = this._levels[this.currentLevelIndex]; @@ -441,17 +263,14 @@ export default class LevelController extends EventHandler { const id = levelObject.urlId; const url = levelObject.url[id]; - logger.log(`Attempt loading level index ${level} with URL-id ${id}`); - - // console.log('Current audio track group ID:', this.hls.audioTracks[this.hls.audioTrack].groupId); - // console.log('New video quality level audio group id:', levelObject.attrs.AUDIO, level); + console.log(`Attempt loading level index ${level} with URL-id ${id}`); this.hls.trigger(Event.LEVEL_LOADING, { url, level, id }); } } } - get nextLoadLevel () { + get nextLoadLevel() { if (this.manualLevelIndex !== -1) { return this.manualLevelIndex; } else { @@ -459,7 +278,7 @@ export default class LevelController extends EventHandler { } } - set nextLoadLevel (nextLevel) { + set nextLoadLevel(nextLevel) { this.level = nextLevel; if (this.manualLevelIndex === -1) { this.hls.nextAutoLevel = nextLevel; diff --git a/cmd/mjpeg-player/hlsjs/controller/level-helper.js b/cmd/mjpeg-player/hlsjs/controller/level-helper.js index 23f59c35..65d1c053 100644 --- a/cmd/mjpeg-player/hlsjs/controller/level-helper.js +++ b/cmd/mjpeg-player/hlsjs/controller/level-helper.js @@ -7,26 +7,24 @@ * * */ -import { logger } from '../utils/logger'; - -export function addGroupId (level, type, id) { +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; + 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) { +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)) { @@ -35,12 +33,12 @@ export function updatePTS (fragments, fromIdx, toIdx) { if (toIdx > fromIdx) { fragFrom.duration = fragToPTS - fragFrom.start; if (fragFrom.duration < 0) { - logger.warn(`negative duration computed for frag ${fragFrom.sn},level ${fragFrom.level}, there should be some duration drift between playlist and fragment!`); + 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) { - logger.warn(`negative duration computed for frag ${fragTo.sn},level ${fragTo.level}, there should be some duration drift between playlist and fragment!`); + console.warn(`negative duration computed for frag ${fragTo.sn},level ${fragTo.level}, there should be some duration drift between playlist and fragment!`); } } } else { @@ -53,7 +51,7 @@ export function updatePTS (fragments, fromIdx, toIdx) { } } -export function updateFragPTSDTS (details, frag, startPTS, endPTS, startDTS, endDTS) { +export function updateFragPTSDTS(details, frag, startPTS, endPTS, startDTS, endDTS) { // update frag PTS/DTS let maxStartPTS = startPTS; if (Number.isFinite(frag.startPTS)) { @@ -109,7 +107,7 @@ export function updateFragPTSDTS (details, frag, startPTS, endPTS, startDTS, end return drift; } -export function mergeDetails (oldDetails, newDetails) { +export function mergeDetails(oldDetails, newDetails) { // potentially retrieve cached initsegment if (newDetails.initSegment && oldDetails.initSegment) { newDetails.initSegment = oldDetails.initSegment; @@ -138,7 +136,7 @@ export function mergeDetails (oldDetails, newDetails) { } if (ccOffset) { - logger.log('discontinuity sliding from playlist, take drift into account'); + 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; @@ -159,7 +157,7 @@ export function mergeDetails (oldDetails, newDetails) { newDetails.PTSKnown = oldDetails.PTSKnown; } -export function mergeSubtitlePlaylists (oldPlaylist, newPlaylist, referenceStart = 0) { +export function mergeSubtitlePlaylists(oldPlaylist, newPlaylist, referenceStart = 0) { let lastIndex = -1; mapFragmentIntersection(oldPlaylist, newPlaylist, (oldFrag, newFrag, index) => { newFrag.start = oldFrag.start; @@ -179,7 +177,7 @@ export function mergeSubtitlePlaylists (oldPlaylist, newPlaylist, referenceStart } } -export function mapFragmentIntersection (oldPlaylist, newPlaylist, intersectionFn) { +export function mapFragmentIntersection(oldPlaylist, newPlaylist, intersectionFn) { if (!oldPlaylist || !newPlaylist) { return; } @@ -198,7 +196,7 @@ export function mapFragmentIntersection (oldPlaylist, newPlaylist, intersectionF } } -export function adjustSliding (oldPlaylist, newPlaylist) { +export function adjustSliding(oldPlaylist, newPlaylist) { const delta = newPlaylist.startSN - oldPlaylist.startSN; const oldFragments = oldPlaylist.fragments; const newFragments = newPlaylist.fragments; @@ -211,7 +209,7 @@ export function adjustSliding (oldPlaylist, newPlaylist) { } } -export function computeReloadInterval (currentPlaylist, newPlaylist, lastRequestTime) { +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) { diff --git a/cmd/mjpeg-player/hlsjs/hls.js b/cmd/mjpeg-player/hlsjs/hls.js index 88107af2..d94c4c03 100644 --- a/cmd/mjpeg-player/hlsjs/hls.js +++ b/cmd/mjpeg-player/hlsjs/hls.js @@ -1,674 +1,53 @@ -import * as URLToolkit from 'url-toolkit'; +/* +AUTHOR + Trek Hopton -import { - ErrorTypes, - ErrorDetails -} from './errors'; +LICENSE + This file is Copyright (C) 2020 the Australian Ocean Lab (AusOcean) -import PlaylistLoader from './loader/playlist-loader'; -import FragmentLoader from './loader/fragment-loader'; -import KeyLoader from './loader/key-loader'; + 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. -import { FragmentTracker } from './controller/fragment-tracker'; -import StreamController from './controller/stream-controller'; -import LevelController from './controller/level-controller'; -import ID3TrackController from './controller/id3-track-controller'; + 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. -import { isSupported } from './is-supported'; -import { logger, enableLogs } from './utils/logger'; -import { hlsDefaultConfig, HlsConfig } from './config'; + You should have received a copy of the GNU General Public License in gpl.txt. + If not, see http://www.gnu.org/licenses. -import HlsEvents from './events'; + For hls.js Copyright notice and license, see LICENSE file. +*/ -import { Observer } from './observer'; +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'; -/** - * @module Hls - * @class - * @constructor - */ -export default class Hls extends Observer { - public static defaultConfig?: HlsConfig; - public config: HlsConfig; - - private _autoLevelCapping: number; - private abrController: any; - private capLevelController: any; - private levelController: any; - private streamController: any; - private networkControllers: any[]; - private audioTrackController: any; - private subtitleTrackController: any; - private emeController: any; - private coreComponents: any[]; - private media: HTMLMediaElement | null = null; - private url: string | null = null; - - /** - * @type {string} - */ - static get version (): string { - return __VERSION__; - } - - /** - * @type {boolean} - */ - static isSupported (): boolean { - return isSupported(); - } - - /** - * @type {HlsEvents} - */ - static get Events () { - return HlsEvents; - } - - /** - * @type {HlsErrorTypes} - */ - static get ErrorTypes () { - return ErrorTypes; - } - - /** - * @type {HlsErrorDetails} - */ - static get ErrorDetails () { - return ErrorDetails; - } - - /** - * @type {HlsConfig} - */ - static get DefaultConfig (): HlsConfig { - if (!Hls.defaultConfig) { - return hlsDefaultConfig; - } - - return Hls.defaultConfig; - } - - /** - * @type {HlsConfig} - */ - static set DefaultConfig (defaultConfig: HlsConfig) { - Hls.defaultConfig = defaultConfig; - } - - /** - * Creates an instance of an HLS client that can attach to exactly one `HTMLMediaElement`. - * - * @constructs Hls - * @param {HlsConfig} config - */ - constructor (userConfig: Partial = {}) { +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); - const defaultConfig = Hls.DefaultConfig; - - if ((userConfig.liveSyncDurationCount || userConfig.liveMaxLatencyDurationCount) && (userConfig.liveSyncDuration || userConfig.liveMaxLatencyDuration)) { - throw new Error('Illegal hls.js config: don\'t mix up liveSyncDurationCount/liveMaxLatencyDurationCount and liveSyncDuration/liveMaxLatencyDuration'); - } - - // Shallow clone - this.config = { - ...defaultConfig, - ...userConfig - }; - - const { config } = this; - - if (config.liveMaxLatencyDurationCount !== void 0 && config.liveMaxLatencyDurationCount <= config.liveSyncDurationCount) { - throw new Error('Illegal hls.js config: "liveMaxLatencyDurationCount" must be gt "liveSyncDurationCount"'); - } - - if (config.liveMaxLatencyDuration !== void 0 && (config.liveSyncDuration === void 0 || config.liveMaxLatencyDuration <= config.liveSyncDuration)) { - throw new Error('Illegal hls.js config: "liveMaxLatencyDuration" must be gt "liveSyncDuration"'); - } - - enableLogs(config.debug); - - this._autoLevelCapping = -1; - - // core controllers and network loaders - - /** - * @member {AbrController} abrController - */ - const abrController = this.abrController = new config.abrController(this); // eslint-disable-line new-cap - const bufferController = new config.bufferController(this); // eslint-disable-line new-cap - const capLevelController = this.capLevelController = new config.capLevelController(this); // eslint-disable-line new-cap - const fpsController = new config.fpsController(this); // eslint-disable-line new-cap - const playListLoader = new PlaylistLoader(this); - const fragmentLoader = new FragmentLoader(this); - const keyLoader = new KeyLoader(this); - const id3TrackController = new ID3TrackController(this); - - // network controllers - - /** - * @member {LevelController} levelController - */ - const levelController = this.levelController = new LevelController(this); - - // FIXME: FragmentTracker must be defined before StreamController because the order of event handling is important - const fragmentTracker = new FragmentTracker(this); - - /** - * @member {StreamController} streamController - */ - const streamController = this.streamController = new StreamController(this, fragmentTracker); - - let networkControllers = [levelController, streamController]; - - // optional audio stream controller - /** - * @var {ICoreComponent | Controller} - */ - let Controller = config.audioStreamController; - if (Controller) { - networkControllers.push(new Controller(this, fragmentTracker)); - } - - /** - * @member {INetworkController[]} networkControllers - */ - this.networkControllers = networkControllers; - - /** - * @var {ICoreComponent[]} - */ - const coreComponents = [ - playListLoader, - fragmentLoader, - keyLoader, - abrController, - bufferController, - capLevelController, - fpsController, - id3TrackController, - fragmentTracker - ]; - - // optional audio track and subtitle controller - Controller = config.audioTrackController; - if (Controller) { - const audioTrackController = new Controller(this); - - /** - * @member {AudioTrackController} audioTrackController - */ - this.audioTrackController = audioTrackController; - coreComponents.push(audioTrackController); - } - - Controller = config.subtitleTrackController; - if (Controller) { - const subtitleTrackController = new Controller(this); - - /** - * @member {SubtitleTrackController} subtitleTrackController - */ - this.subtitleTrackController = subtitleTrackController; - networkControllers.push(subtitleTrackController); - } - - Controller = config.emeController; - if (Controller) { - const emeController = new Controller(this); - - /** - * @member {EMEController} emeController - */ - this.emeController = emeController; - coreComponents.push(emeController); - } - - // optional subtitle controllers - Controller = config.subtitleStreamController; - if (Controller) { - networkControllers.push(new Controller(this, fragmentTracker)); - } - Controller = config.timelineController; - if (Controller) { - coreComponents.push(new Controller(this)); - } - - /** - * @member {ICoreComponent[]} - */ - this.coreComponents = coreComponents; + this.config = hlsDefaultConfig; } - /** - * Dispose of the instance - */ - destroy () { - logger.log('destroy'); - this.trigger(HlsEvents.DESTROYING); - this.detachMedia(); - this.coreComponents.concat(this.networkControllers).forEach(component => { - component.destroy(); - }); - this.url = null; - this.removeAllListeners(); - this._autoLevelCapping = -1; - } - - /** - * Attach a media element - * @param {HTMLMediaElement} media - */ - attachMedia (media: HTMLMediaElement) { - logger.log('attachMedia'); - this.media = media; - this.trigger(HlsEvents.MEDIA_ATTACHING, { media: media }); - } - - /** - * Detach from the media - */ - detachMedia () { - logger.log('detachMedia'); - this.trigger(HlsEvents.MEDIA_DETACHING); - this.media = null; - } - - /** - * Set the source URL. Can be relative or absolute. - * @param {string} url - */ - loadSource (url: string) { + // 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 }); - logger.log(`loadSource:${url}`); - this.url = url; - // when attaching to a source URL, trigger a playlist load this.trigger(HlsEvents.MANIFEST_LOADING, { url: url }); } - - /** - * Start loading data from the stream source. - * Depending on default config, client starts loading automatically when a source is set. - * - * @param {number} startPosition Set the start position to stream from - * @default -1 None (from earliest point) - */ - startLoad (startPosition: number = -1) { - logger.log(`startLoad(${startPosition})`); - this.networkControllers.forEach(controller => { - controller.startLoad(startPosition); - }); - } - - /** - * Stop loading of any stream data. - */ - stopLoad () { - logger.log('stopLoad'); - this.networkControllers.forEach(controller => { - controller.stopLoad(); - }); - } - - /** - * Swap through possible audio codecs in the stream (for example to switch from stereo to 5.1) - */ - swapAudioCodec () { - logger.log('swapAudioCodec'); - this.streamController.swapAudioCodec(); - } - - /** - * When the media-element fails, this allows to detach and then re-attach it - * as one call (convenience method). - * - * Automatic recovery of media-errors by this process is configurable. - */ - recoverMediaError () { - logger.log('recoverMediaError'); - let media = this.media; - this.detachMedia(); - if (media) { - this.attachMedia(media); - } - } - - /** - * @type {QualityLevel[]} - */ - // todo(typescript-levelController) - get levels (): any[] { - return this.levelController.levels; - } - - /** - * Index of quality level currently played - * @type {number} - */ - get currentLevel (): number { - return this.streamController.currentLevel; - } - - /** - * Set quality level index immediately . - * This will flush the current buffer to replace the quality asap. - * That means playback will interrupt at least shortly to re-buffer and re-sync eventually. - * @type {number} -1 for automatic level selection - */ - set currentLevel (newLevel: number) { - logger.log(`set currentLevel:${newLevel}`); - this.loadLevel = newLevel; - this.streamController.immediateLevelSwitch(); - } - - /** - * Index of next quality level loaded as scheduled by stream controller. - * @type {number} - */ - get nextLevel (): number { - return this.streamController.nextLevel; - } - - /** - * Set quality level index for next loaded data. - * This will switch the video quality asap, without interrupting playback. - * May abort current loading of data, and flush parts of buffer (outside currently played fragment region). - * @type {number} -1 for automatic level selection - */ - set nextLevel (newLevel: number) { - logger.log(`set nextLevel:${newLevel}`); - this.levelController.manualLevel = newLevel; - this.streamController.nextLevelSwitch(); - } - - /** - * Return the quality level of the currently or last (of none is loaded currently) segment - * @type {number} - */ - get loadLevel (): number { - return this.levelController.level; - } - - /** - * Set quality level index for next loaded data in a conservative way. - * This will switch the quality without flushing, but interrupt current loading. - * Thus the moment when the quality switch will appear in effect will only be after the already existing buffer. - * @type {number} newLevel -1 for automatic level selection - */ - set loadLevel (newLevel: number) { - logger.log(`set loadLevel:${newLevel}`); - this.levelController.manualLevel = newLevel; - } - - /** - * get next quality level loaded - * @type {number} - */ - get nextLoadLevel (): number { - return this.levelController.nextLoadLevel; - } - - /** - * Set quality level of next loaded segment in a fully "non-destructive" way. - * Same as `loadLevel` but will wait for next switch (until current loading is done). - * @type {number} level - */ - set nextLoadLevel (level: number) { - this.levelController.nextLoadLevel = level; - } - - /** - * Return "first level": like a default level, if not set, - * falls back to index of first level referenced in manifest - * @type {number} - */ - get firstLevel (): number { - return Math.max(this.levelController.firstLevel, this.minAutoLevel); - } - - /** - * Sets "first-level", see getter. - * @type {number} - */ - set firstLevel (newLevel: number) { - logger.log(`set firstLevel:${newLevel}`); - this.levelController.firstLevel = newLevel; - } - - /** - * Return start level (level of first fragment that will be played back) - * if not overrided by user, first level appearing in manifest will be used as start level - * if -1 : automatic start level selection, playback will start from level matching download bandwidth - * (determined from download of first segment) - * @type {number} - */ - get startLevel (): number { - return this.levelController.startLevel; - } - - /** - * set start level (level of first fragment that will be played back) - * if not overrided by user, first level appearing in manifest will be used as start level - * if -1 : automatic start level selection, playback will start from level matching download bandwidth - * (determined from download of first segment) - * @type {number} newLevel - */ - set startLevel (newLevel: number) { - logger.log(`set startLevel:${newLevel}`); - // if not in automatic start level detection, ensure startLevel is greater than minAutoLevel - if (newLevel !== -1) { - newLevel = Math.max(newLevel, this.minAutoLevel); - } - - this.levelController.startLevel = newLevel; - } - - /** - * set dynamically set capLevelToPlayerSize against (`CapLevelController`) - * - * @type {boolean} - */ - set capLevelToPlayerSize (shouldStartCapping: boolean) { - const newCapLevelToPlayerSize = !!shouldStartCapping; - - if (newCapLevelToPlayerSize !== this.config.capLevelToPlayerSize) { - if (newCapLevelToPlayerSize) { - this.capLevelController.startCapping(); // If capping occurs, nextLevelSwitch will happen based on size. - } else { - this.capLevelController.stopCapping(); - this.autoLevelCapping = -1; - this.streamController.nextLevelSwitch(); // Now we're uncapped, get the next level asap. - } - - this.config.capLevelToPlayerSize = newCapLevelToPlayerSize; - } - } - - /** - * Capping/max level value that should be used by automatic level selection algorithm (`ABRController`) - * @type {number} - */ - get autoLevelCapping (): number { - return this._autoLevelCapping; - } - - /** - * get bandwidth estimate - * @type {number} - */ - get bandwidthEstimate (): number { - const bwEstimator = this.abrController._bwEstimator; - return bwEstimator ? bwEstimator.getEstimate() : NaN; - } - - /** - * Capping/max level value that should be used by automatic level selection algorithm (`ABRController`) - * @type {number} - */ - set autoLevelCapping (newLevel: number) { - logger.log(`set autoLevelCapping:${newLevel}`); - this._autoLevelCapping = newLevel; - } - - /** - * True when automatic level selection enabled - * @type {boolean} - */ - get autoLevelEnabled (): boolean { - return (this.levelController.manualLevel === -1); - } - - /** - * Level set manually (if any) - * @type {number} - */ - get manualLevel (): number { - return this.levelController.manualLevel; - } - - /** - * min level selectable in auto mode according to config.minAutoBitrate - * @type {number} - */ - get minAutoLevel (): number { - const { levels, config: { minAutoBitrate } } = this; - const len = levels ? levels.length : 0; - - for (let i = 0; i < len; i++) { - const levelNextBitrate = levels[i].realBitrate - ? Math.max(levels[i].realBitrate, levels[i].bitrate) - : levels[i].bitrate; - - if (levelNextBitrate > minAutoBitrate) { - return i; - } - } - - return 0; - } - - /** - * max level selectable in auto mode according to autoLevelCapping - * @type {number} - */ - get maxAutoLevel (): number { - const { levels, autoLevelCapping } = this; - - let maxAutoLevel; - if (autoLevelCapping === -1 && levels && levels.length) { - maxAutoLevel = levels.length - 1; - } else { - maxAutoLevel = autoLevelCapping; - } - - return maxAutoLevel; - } - - /** - * next automatically selected quality level - * @type {number} - */ - get nextAutoLevel (): number { - // ensure next auto level is between min and max auto level - return Math.min(Math.max(this.abrController.nextAutoLevel, this.minAutoLevel), this.maxAutoLevel); - } - - /** - * this setter is used to force next auto level. - * this is useful to force a switch down in auto mode: - * in case of load error on level N, hls.js can set nextAutoLevel to N-1 for example) - * forced value is valid for one fragment. upon succesful frag loading at forced level, - * this value will be resetted to -1 by ABR controller. - * @type {number} - */ - set nextAutoLevel (nextLevel: number) { - this.abrController.nextAutoLevel = Math.max(this.minAutoLevel, nextLevel); - } - - /** - * @type {AudioTrack[]} - */ - // todo(typescript-audioTrackController) - get audioTracks (): any[] { - const audioTrackController = this.audioTrackController; - return audioTrackController ? audioTrackController.audioTracks : []; - } - - /** - * index of the selected audio track (index in audio track lists) - * @type {number} - */ - get audioTrack (): number { - const audioTrackController = this.audioTrackController; - return audioTrackController ? audioTrackController.audioTrack : -1; - } - - /** - * selects an audio track, based on its index in audio track lists - * @type {number} - */ - set audioTrack (audioTrackId: number) { - const audioTrackController = this.audioTrackController; - if (audioTrackController) { - audioTrackController.audioTrack = audioTrackId; - } - } - - /** - * @type {Seconds} - */ - get liveSyncPosition (): number { - return this.streamController.liveSyncPosition; - } - - /** - * get alternate subtitle tracks list from playlist - * @type {SubtitleTrack[]} - */ - // todo(typescript-subtitleTrackController) - get subtitleTracks (): any[] { - const subtitleTrackController = this.subtitleTrackController; - return subtitleTrackController ? subtitleTrackController.subtitleTracks : []; - } - - /** - * index of the selected subtitle track (index in subtitle track lists) - * @type {number} - */ - get subtitleTrack (): number { - const subtitleTrackController = this.subtitleTrackController; - return subtitleTrackController ? subtitleTrackController.subtitleTrack : -1; - } - - /** - * select an subtitle track, based on its index in subtitle track lists - * @type {number} - */ - set subtitleTrack (subtitleTrackId: number) { - const subtitleTrackController = this.subtitleTrackController; - if (subtitleTrackController) { - subtitleTrackController.subtitleTrack = subtitleTrackId; - } - } - - /** - * @type {boolean} - */ - get subtitleDisplay (): boolean { - const subtitleTrackController = this.subtitleTrackController; - return subtitleTrackController ? subtitleTrackController.subtitleDisplay : false; - } - - /** - * Enable/disable subtitle display rendering - * @type {boolean} - */ - set subtitleDisplay (value: boolean) { - const subtitleTrackController = this.subtitleTrackController; - if (subtitleTrackController) { - subtitleTrackController.subtitleDisplay = value; - } - } } + +export default Hls \ No newline at end of file diff --git a/cmd/mjpeg-player/player.js b/cmd/mjpeg-player/player.js index 34ff75ab..3d872c92 100644 --- a/cmd/mjpeg-player/player.js +++ b/cmd/mjpeg-player/player.js @@ -1,12 +1,9 @@ /* -NAME - player.js - AUTHOR Trek Hopton LICENSE - This file is Copyright (C) 2019 the Australian Ocean Lab (AusOcean) + 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 @@ -22,68 +19,114 @@ LICENSE If not, see http://www.gnu.org/licenses. */ -let frameRate = 30; +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": - self.importScripts('./lex-mjpeg.js'); - let mjpeg = new Uint8Array(e.data.data); - let lex = new MJPEGLexer(mjpeg); - player = new Player(lex); - player.setFrameRate(frameRate); - player.start(); - break; - case "loadMtsMjpeg": - self.importScripts('./hlsjs/mts-demuxer.js'); - let mtsMjpeg = new Uint8Array(e.data.data); - let demux = new MTSDemuxer(); - let tracks = demux._getTracks(); - demux.append(mtsMjpeg); - buf = new FrameBuffer(tracks.video.data); - player = new Player(buf); - player.setFrameRate(frameRate); //TODO: read frame rate from metadata. - player.start(); - break; - default: - console.error("unknown message from main thread"); - break; - } + 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; + } }; -class Player { - constructor(buffer) { - this.buffer = buffer; - this.frameRate = frameRate; +// 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; - } + setFrameRate(rate) { + this.frameRate = rate; + } - start() { - let frame = this.buffer.read(); - if (frame != null) { - postMessage({ msg: "frame", data: frame.buffer }, [frame.buffer]); - setTimeout(() => { this.start(); }, 1000 / this.frameRate); - } else { - postMessage({ msg: "stop" }); - } + 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(src) { - this.src = src; - this.off = 0; - } + constructor() { + this.segments = []; + this.off = { segment: 0, frame: 0 }; + this.demuxer = new MTSDemuxer(); + } - // read returns the next single frame. - read() { - return this.src[this.off++]; + // read returns the next single frame. + read() { + let off = this.off; + let prevOff = off; + if (this.incrementOff()) { + return this.segments[prevOff.segment][prevOff.frame]; + } else { + return null; } + } + + append(data) { + let demuxed = this.demuxer.demux(new Uint8Array(data)); + this.segments.push(demuxed.data); + } + + incrementOff() { + if (!this.segments || !this.segments[this.off.segment]) { + return false; + } + if (this.off.frame + 1 >= this.segments[this.off.segment].length) { + if (this.off.segment + 1 >= this.segments.length) { + return false; + } else { + this.off.segment++; + this.off.frame = 0; + return true; + } + } else { + this.off.frame++; + return true; + } + } } \ No newline at end of file