上一篇 我们分析了 hls.js 的基本目录架构和代码执行流程,这一篇将主要分析 主链路实现代码的分析。
源码分析会点出核心的调用流程,其中一些不常用的方法或者比较容易理解的不做详细的注释。
hls.js 是模块打包的入口
import URLToolkit from 'url-toolkit';
import Event from './events';
import {ErrorTypes, ErrorDetails} from './errors';
import PlaylistLoader from './loader/playlist-loader';
import FragmentLoader from './loader/fragment-loader';
import KeyLoader from './loader/key-loader';
import StreamController from './controller/stream-controller';
import LevelController from './controller/level-controller';
// import ID3TrackController from './controller/id3-track-controller';
import {getMediaSource} from './helper/mediasource-helper';
import {logger, enableLogs} from './utils/logger';
import EventEmitter from 'events';
import {hlsDefaultConfig} from './config';
export default class Hls {
static get version() {
return __VERSION__;
}
static isSupported() {
const mediaSource = getMediaSource();
const sourceBuffer = window.SourceBuffer || window.WebKitSourceBuffer;
// 需要监测 mediaSource 的 Api 在浏览器下可用 并且需要确保支持这种视频源的格式
const isTypeSupported = mediaSource &&
typeof mediaSource.isTypeSupported === 'function' &&
mediaSource.isTypeSupported('video/mp4; codecs="avc1.42E01E,mp4a.40.2"');
// 确保 appendBuffer 和 remove 方法的支持
const sourceBufferValidAPI = !sourceBuffer ||
(sourceBuffer.prototype &&
typeof sourceBuffer.prototype.appendBuffer === 'function' &&
typeof sourceBuffer.prototype.remove === 'function');
return isTypeSupported && sourceBufferValidAPI;
}
static get Events() {
return Event;
}
static get ErrorTypes() {
return ErrorTypes;
}
static get ErrorDetails() {
return ErrorDetails;
}
static get DefaultConfig() {
if(!Hls.defaultConfig) {
return hlsDefaultConfig;
}
return Hls.defaultConfig;
}
static set DefaultConfig(defaultConfig) {
Hls.defaultConfig = defaultConfig;
}
constructor(config = {}) {
var defaultConfig = Hls.DefaultConfig;
if ((config.liveSyncDurationCount || config.liveMaxLatencyDurationCount) && (config.liveSyncDuration || config.liveMaxLatencyDuration)) {
throw new Error('Illegal hls.js config: don\'t mix up liveSyncDurationCount/liveMaxLatencyDurationCount and liveSyncDuration/liveMaxLatencyDuration');
}
for (var prop in defaultConfig) {
if (prop in config) { continue; }
config[prop] = defaultConfig[prop];
}
if (config.liveMaxLatencyDurationCount !== undefined && config.liveMaxLatencyDurationCount <= config.liveSyncDurationCount) {
throw new Error('Illegal hls.js config: "liveMaxLatencyDurationCount" must be gt "liveSyncDurationCount"');
}
if (config.liveMaxLatencyDuration !== undefined && (config.liveMaxLatencyDuration <= config.liveSyncDuration || config.liveSyncDuration === undefined)) {
throw new Error('Illegal hls.js config: "liveMaxLatencyDuration" must be gt "liveSyncDuration"');
}
enableLogs(config.debug);
this.config = config;
this._autoLevelCapping = -1;
// observer setup
var observer = this.observer = new EventEmitter();
observer.trigger = function trigger (event, ...data) {
observer.emit(event, event, ...data);
};
observer.off = function off (event, ...data) {
observer.removeListener(event, ...data);
};
this.on = observer.on.bind(observer);
this.off = observer.off.bind(observer);
this.trigger = observer.trigger.bind(observer);
// 初始化 各种 controller
const abrController = this.abrController = new config.abrController(this);
const bufferController = new config.bufferController(this);
const capLevelController = new config.capLevelController(this);
const fpsController = new config.fpsController(this);
const playListLoader = new PlaylistLoader(this);
const fragmentLoader = new FragmentLoader(this);
const keyLoader = new KeyLoader(this);
//const id3TrackController = new ID3TrackController(this);
// network controllers
const levelController = this.levelController = new LevelController(this);
const streamController = this.streamController = new StreamController(this);
let networkControllers = [levelController, streamController];
// 需要通过配置加入的一些 controller
let Controller = config.audioStreamController;
if (Controller) {
networkControllers.push(new Controller(this));
}
this.networkControllers = networkControllers;
let coreComponents = [ playListLoader, fragmentLoader, keyLoader, abrController, bufferController, capLevelController, fpsController ];
// optional audio track and subtitle controller
Controller = config.audioTrackController;
if (Controller) {
let audioTrackController = new Controller(this);
this.audioTrackController = audioTrackController;
coreComponents.push(audioTrackController);
}
Controller = config.subtitleTrackController;
if (Controller) {
let subtitleTrackController = new Controller(this);
this.subtitleTrackController = subtitleTrackController;
coreComponents.push(subtitleTrackController);
}
// 可选的外挂字幕
[config.subtitleStreamController, config.timelineController].forEach(Controller => {
if (Controller) {
coreComponents.push(new Controller(this));
}
});
this.coreComponents = coreComponents;
}
destroy() {
logger.log('destroy');
this.trigger(Event.DESTROYING);
this.detachMedia();
this.coreComponents.concat(this.networkControllers).forEach(component => {component.destroy();});
this.url = null;
this.observer.removeAllListeners();
this._autoLevelCapping = -1;
}
attachMedia(media) {
logger.log('attachMedia');
this.media = media;
this.trigger(Event.MEDIA_ATTACHING, {media: media});
}
detachMedia() {
logger.log('detachMedia');
this.trigger(Event.MEDIA_DETACHING);
this.media = null;
}
// 触发加载 m3u8文件
loadSource(url) {
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(Event.MANIFEST_LOADING, {url: url});
}
startLoad(startPosition=-1) {
logger.log(`startLoad(${startPosition})`);
this.networkControllers.forEach(controller => {controller.startLoad(startPosition);});
}
stopLoad() {
logger.log('stopLoad');
this.networkControllers.forEach(controller => {controller.stopLoad();});
}
swapAudioCodec() {
logger.log('swapAudioCodec');
this.streamController.swapAudioCodec();
}
// 恢复media error
recoverMediaError() {
logger.log('recoverMediaError');
var media = this.media;
this.detachMedia();
this.attachMedia(media);
}
// 返回当前源的播放质量
get levels() {
return this.levelController.levels;
}
get currentLevel() {
return this.streamController.currentLevel;
}
// 设置媒体清晰度
set currentLevel(newLevel) {
logger.log(`set currentLevel:${newLevel}`);
this.loadLevel = newLevel;
this.streamController.immediateLevelSwitch();
}
/** Return next playback quality level (quality level of next fragment) **/
get nextLevel() {
return this.streamController.nextLevel;
}
/* set quality level for next fragment (-1 for automatic level selection) */
set nextLevel(newLevel) {
logger.log(`set nextLevel:${newLevel}`);
this.levelController.manualLevel = newLevel;
this.streamController.nextLevelSwitch();
}
/** Return the quality level of current/last loaded fragment **/
get loadLevel() {
return this.levelController.level;
}
/* set quality level for current/next loaded fragment (-1 for automatic level selection) */
set loadLevel(newLevel) {
logger.log(`set loadLevel:${newLevel}`);
this.levelController.manualLevel = newLevel;
}
/** Return the quality level of next loaded fragment **/
get nextLoadLevel() {
return this.levelController.nextLoadLevel;
}
/** set quality level of next loaded fragment **/
set nextLoadLevel(level) {
this.levelController.nextLoadLevel = level;
}
/** Return first level (index of first level referenced in manifest)
**/
get firstLevel() {
return Math.max(this.levelController.firstLevel, this.minAutoLevel);
}
/** set first level (index of first level referenced in manifest)
**/
set firstLevel(newLevel) {
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)
**/
get startLevel() {
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)
**/
set startLevel(newLevel) {
logger.log(`set startLevel:${newLevel}`);
const hls = this;
// if not in automatic start level detection, ensure startLevel is greater than minAutoLevel
if (newLevel !== -1) {
newLevel = Math.max(newLevel,hls.minAutoLevel);
}
hls.levelController.startLevel = newLevel;
}
/** Return the capping/max level value that could be used by automatic level selection algorithm **/
get autoLevelCapping() {
return this._autoLevelCapping;
}
/** set the capping/max level value that could be used by automatic level selection algorithm **/
set autoLevelCapping(newLevel) {
logger.log(`set autoLevelCapping:${newLevel}`);
this._autoLevelCapping = newLevel;
}
/* check if we are in automatic level selection mode */
get autoLevelEnabled() {
return (this.levelController.manualLevel === -1);
}
/* return manual level */
get manualLevel() {
return this.levelController.manualLevel;
}
/* return min level selectable in auto mode according to config.minAutoBitrate */
get minAutoLevel() {
let hls = this, levels = hls.levels, minAutoBitrate = hls.config.minAutoBitrate, 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;
}
/* return max level selectable in auto mode according to autoLevelCapping */
get maxAutoLevel() {
const hls = this;
const levels = hls.levels;
const autoLevelCapping = hls.autoLevelCapping;
let maxAutoLevel;
if (autoLevelCapping=== -1 && levels && levels.length) {
maxAutoLevel = levels.length - 1;
} else {
maxAutoLevel = autoLevelCapping;
}
return maxAutoLevel;
}
// return next auto level
get nextAutoLevel() {
const hls = this;
// ensure next auto level is between min and max auto level
return Math.min(Math.max(hls.abrController.nextAutoLevel,hls.minAutoLevel),hls.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
set nextAutoLevel(nextLevel) {
const hls = this;
hls.abrController.nextAutoLevel = Math.max(hls.minAutoLevel,nextLevel);
}
// 从播放列表里获取附加的音频
get audioTracks() {
const audioTrackController = this.audioTrackController;
return audioTrackController ? audioTrackController.audioTracks : [];
}
/** get index of the selected audio track (index in audio track lists) **/
get audioTrack() {
const audioTrackController = this.audioTrackController;
return audioTrackController ? audioTrackController.audioTrack : -1;
}
/** select an audio track, based on its index in audio track lists**/
set audioTrack(audioTrackId) {
const audioTrackController = this.audioTrackController;
if (audioTrackController) {
audioTrackController.audioTrack = audioTrackId;
}
}
get liveSyncPosition() {
return this.streamController.liveSyncPosition;
}
/** get alternate subtitle tracks list from playlist **/
get subtitleTracks() {
const subtitleTrackController = this.subtitleTrackController;
return subtitleTrackController ? subtitleTrackController.subtitleTracks : [];
}
/** get index of the selected subtitle track (index in subtitle track lists) **/
get subtitleTrack() {
const subtitleTrackController = this.subtitleTrackController;
return subtitleTrackController ? subtitleTrackController.subtitleTrack : -1;
}
/** select an subtitle track, based on its index in subtitle track lists**/
set subtitleTrack(subtitleTrackId) {
const subtitleTrackController = this.subtitleTrackController;
if (subtitleTrackController) {
subtitleTrackController.subtitleTrack = subtitleTrackId;
}
}
get subtitleDisplay() {
const subtitleTrackController = this.subtitleTrackController;
return subtitleTrackController ? subtitleTrackController.subtitleDisplay : false;
}
set subtitleDisplay(value) {
const subtitleTrackController = this.subtitleTrackController;
if (subtitleTrackController) {
subtitleTrackController.subtitleDisplay = value;
}
}
}
了解 hls.js 的基本源码后,我们再看它在外部基本调用的流程;
if(Hls.isSupported()) {
var video = document.getElementById('video');
var hls = new Hls();
hls.loadSource('https://video-dev.github.io/streams/x36xhzz/x36xhzz.m3u8');
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED,function() {
video.play();
});
大概其中会触发,实例化 Hls
这个类,然后是触发 loadSource
和 attachMedia
这个方法。而在源码中我们可以查看到触发 MEDIA_ATTACHING
和 MANIFEST_LOADING
事件。我们可以搜索这个关键词找到对应的文件:
- loader/playlist-loader.js
- controller/subtitle-track-controller.js
- controller/subtitle-stream-controller.js
这里我们只关心主链路下的源码,字幕相关暂且忽略。
我们在查看 loader/playlist-loader.js
之前,先需要了解基类 event-handler.js
。它实际主要做的事情就是,用于内部的事件通信,触发 trigger
和 on
。
event-handler.js
import {logger} from './utils/logger';
import {ErrorTypes, ErrorDetails} from './errors';
import Event from './events';
class EventHandler {
// 能够拿到 hls 的对象,然后自动进行事件注册
constructor(hls, ...events) {
this.hls = hls;
this.onEvent = this.onEvent.bind(this);
this.handledEvents = events;
this.useGenericHandler = true;
this.registerListeners();
}
destroy() {
this.unregisterListeners();
}
isEventHandler() {
return typeof this.handledEvents === 'object' && this.handledEvents.length && typeof this.onEvent === 'function';
}
registerListeners() {
if (this.isEventHandler()) {
this.handledEvents.forEach(function(event) {
if (event === 'hlsEventGeneric') {
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);
}
// 主要是将 hls 替换成 on
onEventGeneric(event, data) {
var eventToFunction = function(event, data) {
var 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) {
logger.error(`internal error happened while processing ${event}:${err.message}`);
this.hls.trigger(Event.ERROR, {type: ErrorTypes.OTHER_ERROR, details: ErrorDetails.INTERNAL_EXCEPTION, fatal: false, event : event, err : err});
}
}
}
export default EventHandler;
支持的事件列表可以查看 events.js
接下来我们需要 review loader/playlist-loader.js
的代码。
在这之前我们先看下 m3u8 文件基本的内容;
一个基本的 m3u8 文件内容如下面所示
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:17
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:11.910889,
index0.ts
#EXTINF:16.601022,
index1.ts
#EXTINF:5.088756,
index2.ts
#EXTINF:9.051311,
index3.ts
#EXTINF:7.466289,
index4.ts
#EXTINF:14.724022,
index5.ts
#EXTINF:13.848089,
index6.ts
#EXTINF:3.462022,
index7.ts
#EXTINF:14.306911,
index8.ts
#EXTINF:10.844889,
index9.ts
#EXTINF:6.131533,
index10.ts
#EXTINF:7.508000,
index11.ts
#EXTINF:10.052378,
index12.ts
#EXT-X-ENDLIST
loader/playlist-loader.js
/**
* Playlist Loader
*/
import URLToolkit from 'url-toolkit';
import Event from '../events';
import EventHandler from '../event-handler';
import {ErrorTypes, ErrorDetails} from '../errors';
import AttrList from '../utils/attr-list';
import {logger} from '../utils/logger';
import {isCodecType} from '../utils/codecs';
// https://regex101.com is your friend
// 这里我们需要解析 m3u8 的文件,作者用的 正则表达式 m3u8 格式分析 http://blog.csdn.net/langeldep/article/details/8603045
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:(\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?/;
class LevelKey {
constructor() {
this.method = null;
this.key = null;
this.iv = null;
this._uri = null;
}
get uri() {
if (!this._uri && this.reluri) {
this._uri = URLToolkit.buildAbsoluteURL(this.baseuri, this.reluri, { alwaysNormalize: true });
}
return this._uri;
}
}
// 对每个分片构建的基类 目的是进行方便获取数据
class Fragment {
constructor() {
this._url = null;
this._byteRange = null;
this._decryptdata = null;
this.tagList = [];
}
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 programDateTime() {
if (!this._programDateTime && this.rawProgramDateTime) {
this._programDateTime = new Date(Date.parse(this.rawProgramDateTime));
}
return this._programDateTime;
}
get byteRange() {
if (!this._byteRange) {
let byteRange = this._byteRange = [];
if (this.rawByteRange) {
const params = this.rawByteRange.split('@', 2);
if (params.length === 1) {
const lastByteRangeEndOffset = this.lastByteRangeEndOffset;
byteRange[0] = lastByteRangeEndOffset ? lastByteRangeEndOffset : 0;
} else {
byteRange[0] = parseInt(params[1]);
}
byteRange[1] = parseInt(params[0]) + byteRange[0];
}
}
return this._byteRange;
}
get byteRangeStartOffset() {
return this.byteRange[0];
}
get byteRangeEndOffset() {
return this.byteRange[1];
}
get decryptdata() {
if (!this._decryptdata) {
this._decryptdata = this.fragmentDecryptdataFromLevelkey(this.levelkey, this.sn);
}
return this._decryptdata;
}
/**
* Utility method for parseLevelPlaylist to create an initialization vector for a given segment
* @returns {Uint8Array}
*/
createInitializationVector(segmentNumber) {
var uint8View = new Uint8Array(16);
for (var 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 {*} - an object to be applied as a fragment's decryptdata
*/
fragmentDecryptdataFromLevelkey(levelkey, segmentNumber) {
var decryptdata = levelkey;
if (levelkey && levelkey.method && levelkey.uri && !levelkey.iv) {
decryptdata = new LevelKey();
decryptdata.method = levelkey.method;
decryptdata.baseuri = levelkey.baseuri;
decryptdata.reluri = levelkey.reluri;
decryptdata.iv = this.createInitializationVector(segmentNumber);
}
return decryptdata;
}
cloneObj(obj) {
return JSON.parse(JSON.stringify(obj));
}
}
// 解析成播放列表
class PlaylistLoader extends EventHandler {
constructor(hls) {
super(hls,
Event.MANIFEST_LOADING,
Event.LEVEL_LOADING,
Event.AUDIO_TRACK_LOADING,
Event.SUBTITLE_TRACK_LOADING);
this.loaders = {};
}
// 主要对 loader 进行销毁,loader 可以从配置中传入
destroy() {
for (let loaderName in this.loaders) {
let loader = this.loaders[loaderName];
if (loader) {
loader.destroy();
}
}
this.loaders = {};
EventHandler.prototype.destroy.call(this);
}
// 开始进行 m3u8 文件的加载
onManifestLoading(data) {
this.load(data.url, { type : 'manifest'});
}
onLevelLoading(data) {
this.load(data.url, { type : 'level', level : data.level, id : data.id});
}
onAudioTrackLoading(data) {
this.load(data.url, { type : 'audioTrack', id : data.id});
}
onSubtitleTrackLoading(data) {
this.load(data.url, { type : 'subtitleTrack', id : data.id});
}
load(url, context) {
let loader = this.loaders[context.type];
if (loader) {
let loaderContext = loader.context;
if (loaderContext && loaderContext.url === url) {
logger.trace(`playlist request ongoing`);
return;
} else {
logger.warn(`abort previous loader for type:${context.type}`);
loader.abort();
}
}
let config = this.hls.config,
retry,
timeout,
retryDelay,
maxRetryDelay;
// 主要获取一些配置的选项,对 loader 进行网络请求配置
if(context.type === 'manifest') {
retry = config.manifestLoadingMaxRetry;
timeout = config.manifestLoadingTimeOut;
retryDelay = config.manifestLoadingRetryDelay;
maxRetryDelay = config.manifestLoadingMaxRetryTimeout;
} else {
retry = config.levelLoadingMaxRetry;
timeout = config.levelLoadingTimeOut;
retryDelay = config.levelLoadingRetryDelay;
maxRetryDelay = config.levelLoadingMaxRetryTimeout;
logger.log(`loading playlist for ${context.type} ${context.level || context.id}`);
}
// 如果一般没有设置过一般会使用国内制的 xhr-loader
// xhr-loader源码: https://github.com/JackPu/hls-no-audio.js/blob/master/src/utils/xhr-loader.js
loader = this.loaders[context.type] = context.loader = typeof(config.pLoader) !== 'undefined' ? new config.pLoader(config) : new config.loader(config);
context.url = url;
context.responseType = '';
let loaderConfig, loaderCallbacks;
loaderConfig = { timeout : timeout, maxRetry : retry , retryDelay : retryDelay, maxRetryDelay : maxRetryDelay};
// 一般情况下都能走到这里
loaderCallbacks = { onSuccess : this.loadsuccess.bind(this), onError :this.loaderror.bind(this), onTimeout : this.loadtimeout.bind(this)};
loader.load(context,loaderConfig,loaderCallbacks);
}
resolve(url, baseUrl) {
return URLToolkit.buildAbsoluteURL(baseUrl, url, { alwaysNormalize: true });
}
parseMasterPlaylist(string, baseurl) {
let levels = [], result;
MASTER_PLAYLIST_REGEX.lastIndex = 0;
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;
}
while ((result = MASTER_PLAYLIST_REGEX.exec(string)) != null){
const level = {};
var attrs = level.attrs = new AttrList(result[1]);
level.url = this.resolve(result[2], baseurl);
var 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 = this.avc1toavcoti(level.videoCodec);
}
levels.push(level);
}
return levels;
}
parseMasterPlaylistMedia(string, baseurl, type, audioCodec=null) {
let result, medias = [], id = 0;
MASTER_PLAYLIST_MEDIA_REGEX.lastIndex = 0;
while ((result = MASTER_PLAYLIST_MEDIA_REGEX.exec(string)) != null){
const media = {};
var attrs = new AttrList(result[1]);
if(attrs.TYPE === type) {
media.groupId = attrs['GROUP-ID'];
media.name = attrs.NAME;
media.type = type;
media.default = (attrs.DEFAULT === 'YES');
media.autoselect = (attrs.AUTOSELECT === 'YES');
media.forced = (attrs.FORCED === 'YES');
if (attrs.URI) {
media.url = this.resolve(attrs.URI, baseurl);
}
media.lang = attrs.LANGUAGE;
if(!media.name) {
media.name = media.lang;
}
if (audioCodec) {
media.audioCodec = audioCodec;
}
media.id = id++;
medias.push(media);
}
}
return medias;
}
avc1toavcoti(codec) {
var result, avcdata = codec.split('.');
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;
}
// 这里我们需要解析 m3u8 的文件,作者用的 正则表达式 m3u8 格式分析 http://blog.csdn.net/langeldep/article/details/8603045
parseLevelPlaylist(string, baseurl, id, type) {
var currentSN = 0, // 当前分片编号
totalduration = 0,
// 当前播放源
level = {type: null, version: null, url: baseurl, fragments: [], live: true, startSN: 0},
levelkey = new LevelKey(),
cc = 0,
prevFrag = null,
frag = new Fragment(),
result,
i;
LEVEL_PLAYLIST_REGEX_FAST.lastIndex = 0;
// meu8
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 ? title : null;
frag.tagList.push(title ? [ 'INF',duration,title ] : [ 'INF',duration ]);
} else if (result[3]) { // url
if (!isNaN(frag.duration)) {
// 获取每个分片的数据然后添加到分片列表里面
const sn = currentSN++;
frag.type = type;
frag.start = totalduration;
frag.levelkey = levelkey;
frag.sn = sn;
frag.level = id;
frag.cc = cc;
frag.baseurl = baseurl;
// avoid sliced strings https://github.com/video-dev/hls.js/issues/939
frag.relurl = (' ' + result[3]).slice(1);
level.fragments.push(frag);
prevFrag = frag;
totalduration += frag.duration;
frag = new Fragment();
}
} else if (result[4]) { // X-BYTERANGE
frag.rawByteRange = (' ' + result[4]).slice(1);
if (prevFrag) {
const lastByteRangeEndOffset = prevFrag.byteRangeEndOffset;
if (lastByteRangeEndOffset) {
frag.lastByteRangeEndOffset = lastByteRangeEndOffset;
}
}
} 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 (level.programDateTime === undefined) {
level.programDateTime = new Date(new Date(Date.parse(result[5])) - 1000 * totalduration);
}
} else {
result = result[0].match(LEVEL_PLAYLIST_REGEX_SLOW);
for (i = 1; i < result.length; i++) {
if (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':
cc++;
frag.tagList.push(['DIS']);
break;
case 'DISCONTINUITY-SEQ':
cc = parseInt(value1);
break;
case 'KEY':
// 用于视频加密处理
var decryptparams = value1;
var keyAttrs = new AttrList(decryptparams);
var decryptmethod = keyAttrs.enumeratedString('METHOD'),
decrypturi = keyAttrs.URI,
decryptiv = keyAttrs.hexadecimalInteger('IV');
if (decryptmethod) {
levelkey = new LevelKey();
if ((decrypturi) && (['AES-128', 'SAMPLE-AES'].indexOf(decryptmethod) >= 0)) {
levelkey.method = decryptmethod;
// URI to get the key
levelkey.baseuri = baseurl;
levelkey.reluri = decrypturi;
levelkey.key = null;
// Initialization Vector (IV)
levelkey.iv = decryptiv;
}
}
break;
case 'START':
let startParams = value1;
let startAttrs = new AttrList(startParams);
let startTimeOffset = startAttrs.decimalFloatingPoint('TIME-OFFSET');
//TIME-OFFSET can be 0
if ( !isNaN(startTimeOffset) ) {
level.startTimeOffset = startTimeOffset;
}
break;
case 'MAP':
let mapAttrs = new AttrList(value1);
frag.relurl = mapAttrs.URI;
frag.rawByteRange = mapAttrs.BYTERANGE;
frag.baseurl = baseurl;
frag.level = id;
frag.type = type;
frag.sn = 'initSegment';
level.initSegment = frag;
frag = new Fragment();
break;
default:
logger.warn(`line parsed but not handled: ${result}`);
break;
}
}
}
frag = prevFrag;
//logger.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 = cc;
return level;
}
// 请求成功后,需要对响应的数据进行处理
loadsuccess(response, stats, context, networkDetails=null) {
var string = response.data,
url = response.url,
type = context.type,
id = context.id,
level = context.level,
hls = this.hls;
this.loaders[type] = undefined;
// 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;
}
// performance 是更为精确的计时 API
stats.tload = performance.now();
//stats.mtime = new Date(target.getResponseHeader('Last-Modified'));
// 探测是否为 m3u8 字符串开头
if (string.indexOf('#EXTM3U') === 0) {
if (string.indexOf('#EXTINF:') > 0) {
let isLevel = (type !== 'audioTrack' && type !== 'subtitleTrack'),
levelId = !isNaN(level) ? level : !isNaN(id) ? id : 0,
// 需要解析出 m3u8 文件的基本信息
levelDetails = this.parseLevelPlaylist(string, url, levelId, (type === 'audioTrack' ? 'audio' : (type === 'subtitleTrack' ? 'subtitle' : 'main') ));
levelDetails.tload = stats.tload;
if (type === 'manifest') {
// 触发事件 MANIFEST_LOADED
hls.trigger(Event.MANIFEST_LOADED, {levels: [{url: url, details : levelDetails}], audioTracks : [], url: url, stats: stats, networkDetails: networkDetails});
}
stats.tparsed = performance.now();
if (levelDetails.targetduration) {
if (isLevel) {
hls.trigger(Event.LEVEL_LOADED, {details: levelDetails, level: level || 0, id: id || 0, stats: stats, networkDetails: networkDetails});
} else {
if (type === 'audioTrack') {
hls.trigger(Event.AUDIO_TRACK_LOADED, {details: levelDetails, id: id, stats: stats, networkDetails: networkDetails});
}
else if (type === 'subtitleTrack') {
hls.trigger(Event.SUBTITLE_TRACK_LOADED, {details: levelDetails, id: id, stats: stats, networkDetails: networkDetails});
}
}
} else {
hls.trigger(Event.ERROR, {type: ErrorTypes.NETWORK_ERROR, details: ErrorDetails.MANIFEST_PARSING_ERROR, fatal: true, url: url, reason: 'invalid targetduration', networkDetails: networkDetails});
}
} else {
let levels = this.parseMasterPlaylist(string, url);
// multi level playlist, parse level info
if (levels.length) {
let audioTracks = this.parseMasterPlaylistMedia(string, url, 'AUDIO', levels[0].audioCodec);
let subtitles = this.parseMasterPlaylistMedia(string, url, 'SUBTITLES');
if (audioTracks.length) {
// check if we have found an audio track embedded in main playlist (audio track without URI attribute)
let embeddedAudioFound = false;
audioTracks.forEach(audioTrack => {
if(!audioTrack.url) {
embeddedAudioFound = true;
}
});
// if no embedded audio track defined, but audio codec signaled in quality level, we need to signal this main audio track
// this could happen with playlists with alt audio rendition in which quality levels (main) contains both audio+video. but with mixed audio track not signaled
if (embeddedAudioFound === false && levels[0].audioCodec && !levels[0].attrs.AUDIO) {
logger.log('audio codec signaled in quality level, but no embedded audio track signaled, create one');
audioTracks.unshift({ type : 'main', name : 'main'});
}
}
hls.trigger(Event.MANIFEST_LOADED, {levels, audioTracks, subtitles, url, stats, networkDetails});
} else {
hls.trigger(Event.ERROR, {type: ErrorTypes.NETWORK_ERROR, details: ErrorDetails.MANIFEST_PARSING_ERROR, fatal: true, url: url, reason: 'no level found in manifest', networkDetails: networkDetails});
}
}
} else {
hls.trigger(Event.ERROR, {type: ErrorTypes.NETWORK_ERROR, details: ErrorDetails.MANIFEST_PARSING_ERROR, fatal: true, url: url, reason: 'no EXTM3U delimiter', networkDetails: networkDetails});
}
}
loaderror(response, context, networkDetails=null) {
var details, fatal,loader = context.loader;
switch(context.type) {
case 'manifest':
details = ErrorDetails.MANIFEST_LOAD_ERROR;
fatal = true;
break;
case 'level':
details = ErrorDetails.LEVEL_LOAD_ERROR;
fatal = false;
break;
case 'audioTrack':
details = ErrorDetails.AUDIO_TRACK_LOAD_ERROR;
fatal = false;
break;
}
if (loader) {
loader.abort();
this.loaders[context.type] = undefined;
}
this.hls.trigger(Event.ERROR, {type: ErrorTypes.NETWORK_ERROR, details: details, fatal: fatal, url: loader.url, loader: loader, response: response, context : context, networkDetails: networkDetails});
}
loadtimeout(stats, context, networkDetails=null) {
var details, fatal, loader = context.loader;
switch(context.type) {
case 'manifest':
details = ErrorDetails.MANIFEST_LOAD_TIMEOUT;
fatal = true;
break;
case 'level':
details = ErrorDetails.LEVEL_LOAD_TIMEOUT;
fatal = false;
break;
case 'audioTrack':
details = ErrorDetails.AUDIO_TRACK_LOAD_TIMEOUT;
fatal = false;
break;
}
if (loader) {
loader.abort();
this.loaders[context.type] = undefined;
}
this.hls.trigger(Event.ERROR, {type: ErrorTypes.NETWORK_ERROR, details: details, fatal: fatal, url: loader.url, loader: loader, context : context, networkDetails: networkDetails});
}
}
export default PlaylistLoader;
大概这段代码基本调用栈就是;
onManifestLoading -> load -> loadsuccess -> parseLevelPlaylist -> 触发 `MANIFEST_LOADED`
接下来我们需要知道在触发 MANIFEST_LOADED
,传递过去的 levelDetails的基本数据如下:
这个时候我们搜索有哪些地方需要使用到这个信息。
controller/level-controller.js
我们核心关注下 onManifestLoaded
/*
* 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';
class LevelController extends EventHandler {
constructor(hls) {
super(hls,
Event.MANIFEST_LOADED,
Event.LEVEL_LOADED,
Event.FRAG_LOADED,
Event.ERROR);
this._manualLevel = -1;
this.timer = null;
}
destroy() {
this.cleanTimer();
this._manualLevel = -1;
}
cleanTimer() {
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;
}
});
}
// 加速 playlist 的刷新
if (this.timer) {
this.tick();
}
}
stopLoad() {
this.canload = false;
}
// 拿到playlist-loader.js 解析的数据
onManifestLoaded(data) {
let levels = [],
bitrateStart,
levelSet = {},
levelFromSet = null,
videoCodecFound = false,
audioCodecFound = false,
chromeOrFirefox = /chrome|firefox/.test(navigator.userAgent.toLowerCase());
// 我们需要根据码率进行不同播放质量的媒体进行组合
data.levels.forEach(level => {
level.loadError = 0;
level.fragmentError = false;
videoCodecFound = videoCodecFound || !!level.videoCodec;
audioCodecFound = audioCodecFound || !!level.audioCodec || !!(level.attrs && level.attrs.AUDIO);
// erase audio codec info if browser does not support mp4a.40.34.
// demuxer will autodetect codec and fallback to mpeg/audio
if (chromeOrFirefox === true && level.audioCodec && level.audioCodec.indexOf('mp4a.40.34') !== -1) {
level.audioCodec = undefined;
}
levelFromSet = levelSet[level.bitrate];
if (levelFromSet === undefined) {
level.url = [level.url];
level.urlId = 0;
levelSet[level.bitrate] = level;
levels.push(level);
} else {
levelFromSet.url.push(level.url);
}
});
// remove audio-only level if we also have levels with audio+video codecs signalled
if (videoCodecFound === true && audioCodecFound === true) {
levels = levels.filter(({videoCodec}) => !!videoCodec);
}
// 保证 所有播放质量都是可支持的 可以阅读 https://github.com/JackPu/hls-no-audio.js/blob/master/src/utils/codecs.js 查看实现
levels = levels.filter(({audioCodec, videoCodec}) => {
return (!audioCodec || isCodecSupportedInMp4(audioCodec)) && (!videoCodec || isCodecSupportedInMp4(videoCodec));
});
if (levels.length > 0) {
// 开始的比特率
bitrateStart = levels[0].bitrate;
// sort level on bitrate
levels.sort(function (a, b) {
return 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;
logger.log(`manifest loaded,${levels.length} level(s) found, first bitrate:${bitrateStart}`);
break;
}
}
// 触发 事件 MANIFEST_PARSED
this.hls.trigger(Event.MANIFEST_PARSED, {
levels : levels,
firstLevel: this._firstLevel,
stats : data.stats,
audio : audioCodecFound,
video : videoCodecFound,
altAudio : data.audioTracks.length > 0
});
} 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._level;
}
set level(newLevel) {
let levels = this._levels;
if (levels) {
newLevel = Math.min(newLevel, levels.length-1);
if (this._level !== newLevel || levels[newLevel].details === undefined) {
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.cleanTimer();
if (this._level !== newLevel) {
logger.log(`switching to level ${newLevel}`);
this._level = newLevel;
var levelProperties = levels[newLevel];
levelProperties.level = newLevel;
// LEVEL_SWITCH to be deprecated in next major release
hls.trigger(Event.LEVEL_SWITCH, levelProperties);
hls.trigger(Event.LEVEL_SWITCHING, levelProperties);
}
var level = levels[newLevel], levelDetails = level.details;
// check if we need to load playlist for this level
if (!levelDetails || levelDetails.live === true) {
// level not retrieved yet, or live playlist we need to (re)load it
var 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._manualLevel;
}
set manualLevel(newLevel) {
this._manualLevel = 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;
}
onError(data) {
if (data.fatal === true) {
if (data.type === ErrorTypes.NETWORK_ERROR) {
this.cleanTimer();
}
return;
}
let details = data.details, levelError = false, fragmentError = false;
let levelIndex, level;
let {config} = this.hls;
// try to recover not fatal errors
switch (details) {
case ErrorDetails.FRAG_LOAD_ERROR:
case ErrorDetails.FRAG_LOAD_TIMEOUT:
case ErrorDetails.FRAG_LOOP_LOADING_ERROR:
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;
break;
}
/* try to switch to a redundant stream if any available.
* if no redundant stream available, emergency switch down (if in auto mode and current level not 0)
* otherwise, we cannot recover this network error ...
*/
if (levelIndex !== undefined) {
level = this._levels[levelIndex];
level.loadError++;
level.fragmentError = fragmentError;
// Allow fragment retry as long as configuration allows.
// Since fragment retry logic could depend on the levels, we should not enforce retry limits when there is an issue with fragments
// FIXME Find a better abstraction where fragment/level retry management is well decoupled
if (fragmentError === true){
// if any redundant streams available and if we haven't try them all (level.loadError is reseted on successful frag/level load.
// if level.loadError reaches redundantLevels it means that we tried them all, no hope => let's switch down
const redundantLevels = level.url.length;
if (redundantLevels > 1 && level.loadError < redundantLevels) {
level.urlId = (level.urlId + 1) % redundantLevels;
level.details = undefined;
logger.warn(`level controller,${details} for level ${levelIndex}: switching to redundant stream id ${level.urlId}`);
} else {
// we could try to recover if in auto mode and current level not lowest level (0)
if ((this._manualLevel === -1) && levelIndex !== 0) {
logger.warn(`level controller,${details}: switch-down for next fragment`);
this.hls.nextAutoLevel = levelIndex - 1;
} else {
logger.warn(`level controller, ${details}: reload a fragment`);
// reset this._level so that another call to set level() will trigger again a frag load
this._level = undefined;
}
}
} else if (levelError === true){
if ((this.levelRetryCount + 1) <= config.levelLoadingMaxRetry) {
// exponential backoff capped to max retry timeout
const delay = Math.min(Math.pow(2, this.levelRetryCount) * config.levelLoadingRetryDelay, config.levelLoadingMaxRetryTimeout);
// reset load counter to avoid frag loop loading error
this.timer = setTimeout(() => this.tick(), delay);
// boolean used to inform stream controller not to switch back to IDLE on non fatal error
data.levelRetry = true;
this.levelRetryCount++;
logger.warn(`level controller,${details}, retry in ${delay} ms, current retry count is ${this.levelRetryCount}`);
} else {
logger.error(`cannot recover ${details} error`);
this._level = undefined;
// stopping live reloading timer if any
this.cleanTimer();
// switch error to fatal
data.fatal = true;
}
}
}
}
// 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) {
const levelId = data.level;
// only process level loaded events matching with expected level
if (levelId === this._level) {
let curLevel = this._levels[levelId];
// reset level load error counter on successful level loaded only if there is no issues with fragments
if(curLevel.fragmentError === false){
curLevel.loadError = 0;
this.levelRetryCount = 0;
}
let newDetails = data.details;
// if current playlist is a live playlist, arm a timer to reload it
if (newDetails.live) {
let reloadInterval = 1000 * ( newDetails.averagetargetduration ? newDetails.averagetargetduration : newDetails.targetduration),
curDetails = curLevel.details;
if (curDetails && newDetails.endSN === curDetails.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 /= 2;
logger.log(`same live playlist, reload twice faster`);
}
// decrement reloadInterval with level loading delay
reloadInterval -= performance.now() - data.stats.trequest;
// in any case, don't reload more than every second
reloadInterval = Math.max(1000, Math.round(reloadInterval));
logger.log(`live playlist, reload in ${reloadInterval} ms`);
this.timer = setTimeout(() => this.tick(), reloadInterval);
} else {
this.cleanTimer();
}
}
}
tick() {
var levelId = this._level;
if (levelId !== undefined && this.canload) {
var level = this._levels[levelId];
if (level && level.url) {
var urlId = level.urlId;
this.hls.trigger(Event.LEVEL_LOADING, {url: level.url[urlId], level: levelId, id: urlId});
}
}
}
get nextLoadLevel() {
if (this._manualLevel !== -1) {
return this._manualLevel;
} else {
return this.hls.nextAutoLevel;
}
}
set nextLoadLevel(nextLevel) {
this.level = nextLevel;
if (this._manualLevel === -1) {
this.hls.nextAutoLevel = nextLevel;
}
}
}
export default LevelController;
其基本栈调用如下;
onManifestLoaded -> 触发 `MANIFEST_PARSED`
这个时候我们在查找 监听MANIFEST_PARSED
的文件
- controller/buffer-controller.js
- controller/stream-controller.js
我们先看 controller/stram-controller.js
中的方法: onManifestParsed
onManifestParsed(data) {
var aac = false, heaac = false, codec;
data.levels.forEach(level => {
// 我们需要判断播放源里面是否存在不同的音频 codec 值
codec = level.audioCodec;
if (codec) {
if (codec.indexOf('mp4a.40.2') !== -1) {
aac = true;
}
if (codec.indexOf('mp4a.40.5') !== -1) {
heaac = true;
}
}
});
this.audioCodecSwitch = (aac && heaac);
if (this.audioCodecSwitch) {
logger.log('both AAC/HE-AAC audio found in levels; declaring level codec as HE-AAC');
}
this.levels = data.levels;
this.startFragRequested = false;
let config = this.config;
// 如果设置了自动加载,则触发 hls startLoad
if (config.autoStartLoad || this.forceStartLoad) {
需要触发 netController [levelController, streamController] 各个 controller里面的 load this.hls.startLoad(config.startPosition);
}
}
levelController 里面的 startLoad
方法可以从上面看到。重点看下 sreamController 的 startLoad
方法。
// 它接受一个传入点
startLoad(startPosition) {
if (this.levels) {
let lastCurrentTime = this.lastCurrentTime, hls = this.hls;
this.stopLoad();
// 如果没有开启 timer 则自动开启
if (!this.timer) {
this.timer = setInterval(this.ontick, 100);
}
this.level = -1;
this.fragLoadError = 0;
// 如果没有开始第一个分片的请求
if (!this.startFragRequested) {
// determine load level
let startLevel = hls.startLevel;
if (startLevel === -1) {
// -1 : guess start Level by doing a bitrate test by loading first fragment of lowest quality level
startLevel = 0;
this.bitrateTest = true;
}
// set new level to playlist loader : this will trigger start level load
// hls.nextLoadLevel remains until it is set to a new value or until a new frag is successfully loaded
this.level = hls.nextLoadLevel = startLevel;
this.loadedmetadata = false;
}
//设置开始的点到 currentTime
if (lastCurrentTime > 0 && startPosition === -1) {
logger.log(`override startPosition with lastCurrentTime @${lastCurrentTime.toFixed(3)}`);
startPosition = lastCurrentTime;
}
this.state = State.IDLE;
this.nextLoadPosition = this.startPosition = this.lastCurrentTime = startPosition;
this.tick();
} else {
this.forceStartLoad = true;
this.state = State.STOPPED;
}
}
....
tick() {
this.ticks++;
if (this.ticks === 1) {
this.doTick();
if (this.ticks > 1) {
setTimeout(this.tick, 1);
}
this.ticks = 0;
}
}
// 根据不同的状态
doTick() {
switch(this.state) {
case State.ERROR:
//don't do anything in error state to avoid breaking further ...
break;
case State.BUFFER_FLUSHING:
// in buffer flushing state, reset fragLoadError counter
this.fragLoadError = 0;
break;
case State.IDLE:
this._doTickIdle();
break;
case State.WAITING_LEVEL:
var level = this.levels[this.level];
// check if playlist is already loaded
if (level && level.details) {
this.state = State.IDLE;
}
break;
case State.FRAG_LOADING_WAITING_RETRY:
var now = performance.now();
var retryDate = this.retryDate;
// if current time is gt than retryDate, or if media seeking let's switch to IDLE state to retry loading
if(!retryDate || (now >= retryDate) || (this.media && this.media.seeking)) {
logger.log(`mediaController: retryDate reached, switch back to IDLE state`);
this.state = State.IDLE;
}
break;
case State.ERROR:
case State.STOPPED:
case State.FRAG_LOADING:
case State.PARSING:
case State.PARSED:
case State.ENDED:
break;
default:
break;
}
// check buffer
this._checkBuffer();
// check/update current fragment
this._checkFragmentChanged();
}
// 这里需要进行大量的逻辑操作
// NOTE: Maybe we could rather schedule a check for buffer length after half of the currently
// played segment, or on pause/play/seek instead of naively checking every 100ms?
_doTickIdle() {
const hls = this.hls,
config = hls.config,
media = this.media;
// 如果没有进行媒体绑定或者配置中没有设置 startFragPrefetch 的话
if (this.levelLastLoaded === undefined || (
!media && (this.startFragRequested || !config.startFragPrefetch))) {
return;
}
let pos;
if (this.loadedmetadata) {
pos = media.currentTime;
} else {
pos = this.nextLoadPosition;
}
// 需要判断下一个加载媒体源
let level = hls.nextLoadLevel,
levelInfo = this.levels[level];
if (!levelInfo) {
return;
}
let levelBitrate = levelInfo.bitrate,
maxBufLen;
// 需要判断最大的 buffer 长度 buffeer 不要超过 60 MB 或者时间超过 30s
if (levelBitrate) {
maxBufLen = Math.max(8 * config.maxBufferSize / levelBitrate, config.maxBufferLength);
} else {
maxBufLen = config.maxBufferLength;
}
maxBufLen = Math.min(maxBufLen, config.maxMaxBufferLength);
// 拿到 buffer 的一些信息
const bufferInfo = BufferHelper.bufferInfo(this.mediaBuffer ? this.mediaBuffer : media, pos, config.maxBufferHole),
bufferLen = bufferInfo.len;
// 保持 idle 状态知道超出
if (bufferLen >= maxBufLen) {
return;
}
//如果 buffer 的长度小于预计的则继续加载下个分片
logger.trace(`buffer length of ${bufferLen.toFixed(3)} is below max of ${maxBufLen.toFixed(3)}. checking for more payload ...`);
// set next load level : this will trigger a playlist load if needed
this.level = hls.nextLoadLevel = level;
const levelDetails = levelInfo.details;
if (typeof levelDetails === 'undefined' || levelDetails.live && this.levelLastLoaded !== level) {
this.state = State.WAITING_LEVEL;
return;
}
// we just got done loading the final fragment and there is no other buffered range after ...
// rationale is that in case there are any buffered ranges after, it means that there are unbuffered portion in between
// so we should not switch to ENDED in that case, to be able to buffer them
// dont switch to ENDED if we need to backtrack last fragment
let fragPrevious = this.fragPrevious;
if (!levelDetails.live && fragPrevious && !fragPrevious.backtracked && fragPrevious.sn === levelDetails.endSN && !bufferInfo.nextStart) {
// fragPrevious is last fragment. retrieve level duration using last frag start offset + duration
// real duration might be lower than initial duration if there are drifts between real frag duration and playlist signaling
const duration = Math.min(media.duration,fragPrevious.start + fragPrevious.duration);
// if everything (almost) til the end is buffered, let's signal eos
// we don't compare exactly media.duration === bufferInfo.end as there could be some subtle media duration difference (audio/video offsets...)
// tolerate up to one frag duration to cope with these cases.
// also cope with almost zero last frag duration (max last frag duration with 200ms) refer to https://github.com/video-dev/hls.js/pull/657
if (duration - Math.max(bufferInfo.end,fragPrevious.start) <= Math.max(0.2,fragPrevious.duration)) {
// Finalize the media stream
let data = {};
if (this.altAudio) {
data.type = 'video';
}
this.hls.trigger(Event.BUFFER_EOS,data);
this.state = State.ENDED;
return;
}
}
// 这个时候可以开始请求了
this._fetchPayloadOrEos(pos, bufferInfo, levelDetails);
}