@firstcoders/hls-web-audio
Version:
`@firstcoders/hls-web-audio` plays multiple streamed audio tracks in sync using the Web Audio API. It is designed for stem-like playback where each track can be independently loaded, buffered, scheduled, and mixed.
199 lines (198 loc) • 5.04 kB
JavaScript
/**
* Custom ESM build of https://github.com/xiyuyizhi/mpd-m3u8-to-json. Credits to xiyuyizhi.
*
* Copyright (c) xiyuyizhi
*/
/* eslint-disable */
const PREFIX_TAG_PATTERN = /#EXT(?:-X-)?([^:]+):?(.*)$/;
const TAG_PAIR_SPLIT_PATTERN = /([^,="]+)((="[^"]+")|(=[^,]+))*/g;
function formatNameToCamel(str) {
return str.split('-').reduce((all, c) => {
if (!all) {
all += c.toLowerCase();
return all;
}
all += c.charAt(0) + c.slice(1).toLowerCase();
return all;
}, '');
}
function parseTag(tagStr) {
if (!(/^#EXT/.test(tagStr) || !/^\s*#/.test(tagStr))) return null;
let tagName;
let attr;
let matched = PREFIX_TAG_PATTERN.exec(tagStr);
if (matched) {
tagName = formatNameToCamel(matched[1]);
attr = parseTagAttr(matched[2]);
} else {
tagName = 'url';
attr = tagStr;
}
return { [tagName]: attr };
}
function parseTagAttr(attrStr) {
if (!attrStr) return null;
let attrList = attrStr.match(TAG_PAIR_SPLIT_PATTERN).map((pairStr) => parseTagAttrPairs(pairStr));
if (attrList.length === 1) return attrList[0];
if (attrList.filter((x) => typeof x === 'object').length === 0) {
return attrList;
}
return attrList.reduce((all, c) => {
return {
...all,
...c,
};
}, {});
}
function parseTagAttrPairs(attrPairStr) {
let attrPairs = attrPairStr.trim().replace('=', '|').split('|');
if (attrPairs.length == 2) {
let key = formatNameToCamel(attrPairs[0]);
return {
[key]: attrPairs[1].replace(/("|')/g, ''),
};
}
let v = parseFloat(attrPairs[0]);
return Number.isNaN(v) ? attrPairs[0] : v;
}
function geneAbsUrl(url, base) {
if (/^https?/.test(url) || /^data:/.test(url) || /^sdk:/.test(url)) {
return url;
}
base = base.split('/').slice(0, -1);
url = url.split('/');
while (url.length) {
let c = url.shift();
if (base.indexOf(c) == -1) {
base.push(c);
}
}
return base.join('/');
}
function mergeTags(tagList, result, postHooks) {
let master = result.master;
let len = tagList.length;
let cc = 0;
let duration = 0;
let startSN = 0;
let segCount = 0;
let levelCount = 0;
let keyIndex = -1;
for (let i = 0; i < len; i++) {
let tagInfo = tagList[i];
for (let key in tagInfo) {
let v = tagInfo[key];
if (v && v['uri']) {
v['url'] = geneAbsUrl(v['uri'], result['m3u8Url']);
}
switch (key) {
case 'inf':
let segment = {
start: duration,
end: duration + (Array.isArray(v) ? v[0] : v),
cc,
sn: startSN + segCount,
};
if (keyIndex >= 0) {
segment['keyIndex'] = keyIndex;
}
segCount++;
duration = segment['end'];
result['segments'].push(segment);
break;
case 'start':
duration = v;
break;
case 'discontinuity':
cc++;
break;
case 'mediaSequence':
startSN = v;
break;
case 'streamInf':
levelCount++;
v['levelId'] = levelCount;
result['levels'].push(v);
break;
case 'media':
result['medias'].push(v);
break;
case 'endlist':
result['live'] = false;
break;
case 'key':
keyIndex++;
if (result['key']) {
result['key'].push(v);
} else {
result['key'] = [v];
}
break;
case 'url':
let list = master ? result['levels'] : result['segments'];
if (!list.length) {
throw new Error('invalid m3u8');
}
if (master) {
list[levelCount - 1].url = geneAbsUrl(v, result['m3u8Url']);
} else {
list[segCount - 1].url = geneAbsUrl(v, result['m3u8Url']);
}
break;
default:
if (v) {
result[key] = v;
}
}
}
if (postHooks) {
result = postHooks(tagInfo, result);
}
}
if (!master) {
result['startSN'] = startSN;
result['endSN'] = startSN + segCount - 1;
result['duration'] = duration;
}
return result;
}
function m3u8Parser(text, m3u8Url, postHooks) {
if (!text || !m3u8Url) {
return { error: 1, msg: 'invalid input' };
}
const tagList = text
.split('\n')
.filter(Boolean)
.map((x) => parseTag(x.trim()))
.filter(Boolean);
if (!tagList.length) {
return { error: 1, msg: 'invalid m3u8' };
}
const isMaster = tagList.filter((x) => !!x['streamInf']).length !== 0;
let result;
if (isMaster) {
result = {
master: true,
m3u8Url,
levels: [],
medias: [],
};
} else {
result = {
master: false,
m3u8Url,
duration: 0,
startSN: 0,
endSN: 0,
segments: [],
live: true,
};
}
try {
result = mergeTags(tagList, result, postHooks);
} catch (e) {
return { error: 1, msg: e.message };
}
return result;
}
export default m3u8Parser;