hjplayer
Version:
hjplayer, a HTML5 Player, can play flv and hls by Media Source Extension, based on typescript;
403 lines (357 loc) • 16.2 kB
text/typescript
import URLToolkit from 'url-toolkit';
import Fragment from '../Loaders/Fragment';
import Level from './Level';
import LevelKey from './LevelKey';
import AttrList from '../Utils/attr-list';
import logger from '../Utils/Logger';
import { MediaPlaylist, AudioGroup, MediaPlaylistType } from '../Interfaces/media-playlist';
import { PlaylistLevelType, SingleLevels } from '../Interfaces/loader';
import { isCodecType, CodecType } from '../Utils/codecs';
/* eslint-disable */
/**
* M3U8 parser
* @module
*/
// https://regex101.com is your friend
const MASTER_PLAYLIST_REGEX = /#EXT-X-STREAM-INF:([^\n\r]*)[\r\n]+([^\r\n]+)/g
const MASTER_PLAYLIST_MEDIA_REGEX = /#EXT-X-MEDIA:(.*)/g
const LEVEL_PLAYLIST_REGEX_FAST = new RegExp(
[
/#EXTINF:\s*(\d*(?:\.\d+)?)(?:,(.*)\s+)?/.source, // duration (#EXTINF:<duration>,<title>), group 1 => duration, group 2 => title
/|(?!#)([\S+ ?]+)/.source, // segment URI, group 3 => the URI (note newline is not eaten)
/|#EXT-X-BYTERANGE:*(.+)/.source, // next segment's byterange, group 4 => range spec (x@y)
/|#EXT-X-PROGRAM-DATE-TIME:(.+)/.source, // next segment's program date/time group 5 => the datetime spec
/|#.*/.source // All other non-segment oriented tags will match with all groups empty
].join(''),
'g'
)
const LEVEL_PLAYLIST_REGEX_SLOW = /(?:(?:#(EXTM3U))|(?:#EXT-X-(PLAYLIST-TYPE):(.+))|(?:#EXT-X-(MEDIA-SEQUENCE): *(\d+))|(?:#EXT-X-(TARGETDURATION): *(\d+))|(?:#EXT-X-(KEY):(.+))|(?:#EXT-X-(START):(.+))|(?:#EXT-X-(ENDLIST))|(?:#EXT-X-(DISCONTINUITY-SEQ)UENCE:(\d+))|(?:#EXT-X-(DIS)CONTINUITY))|(?:#EXT-X-(VERSION):(\d+))|(?:#EXT-X-(MAP):(.+))|(?:(#)([^:]*):(.*))|(?:(#)(.*))(?:.*)\r?\n?/
const MP4_REGEX_SUFFIX = /\.(mp4|m4s|m4v|m4a)$/i
export default class M3U8Parser {
static Tag: string = 'M3U8Parser'
static findGroup(groups: Array<AudioGroup>, mediaGroupId: string): AudioGroup | null {
if (!groups) {
return null
}
let matchingGroup = null
for (let i = 0; i < groups.length; i++) {
const group = groups[i]
if (group.id === mediaGroupId) {
matchingGroup = group
}
}
return matchingGroup
}
static convertAVC1ToAVCOTI(codec: string) {
let avcdata: Array<any> = codec.split('.')
let result
if (avcdata.length > 2) {
result = avcdata.shift() + '.'
result += parseInt(avcdata.shift()).toString(16)
result += ('000' + parseInt(avcdata.shift()).toString(16)).substr(-4)
} else {
result = codec
}
return result
}
static resolve(url: string, baseUrl: string): string {
return URLToolkit.buildAbsoluteURL(baseUrl, url, { alwaysNormalize: true })
}
static parseMasterPlaylist(string: string, baseurl: string): SingleLevels[] {
let levels: Array<any> = []
MASTER_PLAYLIST_REGEX.lastIndex = 0
function setCodecs(codecs: Array<string>, level: any) {
;['video', 'audio'].forEach(type => {
const filtered = codecs.filter(codec => isCodecType(codec, type as CodecType))
if (filtered.length) {
const preferred = filtered.filter(codec => {
return (
codec.lastIndexOf('avc1', 0) === 0 || codec.lastIndexOf('mp4a', 0) === 0
)
})
level[`${type}Codec`] = preferred.length > 0 ? preferred[0] : filtered[0]
// remove from list
codecs = codecs.filter(codec => filtered.indexOf(codec) === -1)
}
})
level.unknownCodecs = codecs
}
let result: RegExpExecArray | null
while ((result = MASTER_PLAYLIST_REGEX.exec(string)) != null) {
const level: any = {}
const attrs = (level.attrs = new AttrList(result[1]))
level.url = M3U8Parser.resolve(result[2], baseurl)
const resolution = attrs.decimalResolution('RESOLUTION')
if (resolution) {
level.width = resolution.width
level.height = resolution.height
}
level.bitrate =
attrs.decimalInteger('AVERAGE-BANDWIDTH') || attrs.decimalInteger('BANDWIDTH')
level.name = (<any>attrs).NAME
setCodecs([].concat(((<any>attrs).CODECS || '').split(/[ ,]+/)), level)
if (level.videoCodec && level.videoCodec.indexOf('avc1') !== -1) {
level.videoCodec = M3U8Parser.convertAVC1ToAVCOTI(level.videoCodec)
}
levels.push(level)
}
return levels
}
static parseMasterPlaylistMedia(
string: string,
baseurl: string,
type: MediaPlaylistType,
audioGroups: Array<AudioGroup> = []
): Array<MediaPlaylist> {
let result: RegExpExecArray | null
let medias: Array<MediaPlaylist> = []
let id = 0
MASTER_PLAYLIST_MEDIA_REGEX.lastIndex = 0
while ((result = MASTER_PLAYLIST_MEDIA_REGEX.exec(string)) !== null) {
const media = {}
const attrs: any = new AttrList(result[1])
if (attrs.TYPE === type) {
const media: MediaPlaylist = {
id: id++,
groupId: attrs['GROUP-ID'],
name: attrs.NAME || attrs.LANGUAGE,
type,
default: attrs.DEFAULT === 'YES',
autoselect: attrs.AUTOSELECT === 'YES',
forced: attrs.FORCED === 'YES',
lang: attrs.LANGUAGE
}
if (attrs.URI) {
media.url = M3U8Parser.resolve(attrs.URI, baseurl)
}
if (audioGroups.length) {
const groupCodec = M3U8Parser.findGroup(audioGroups, <string>media.groupId)
media.audioCodec = groupCodec ? groupCodec.codec : audioGroups[0].codec
}
medias.push(media)
}
}
return medias
}
static parseLevelPlaylist(
string: string,
baseurl: string,
id: number,
type: PlaylistLevelType,
levelUrlId: number
): Level {
let currentSN = 0
let totalduration = 0
let level = new Level(baseurl)
let levelkey: LevelKey | undefined // todo 不用传输吗???
let cc = 0
let prevFrag: Fragment | null = null
let frag: Fragment | null = new Fragment()
let result: RegExpExecArray | RegExpMatchArray | null
let i: number
let firstPdtIndex = null
LEVEL_PLAYLIST_REGEX_FAST.lastIndex = 0
while ((result = LEVEL_PLAYLIST_REGEX_FAST.exec(string)) !== null) {
const duration = result[1]
if (duration) {
// 持续时间
frag.duration = parseFloat(duration)
// avoid sliced strings https://github.com/video-dev/hls.js/issues/939
const title = (' ' + result[2]).slice(1)
frag.title = title || null
frag.tagList.push(title ? ['INF', duration, title] : ['INF', duration])
} else if (result[3]) {
// url
if (Number.isFinite(frag.duration)) {
const sn = currentSN++
frag.type = type
frag.start = totalduration
frag.levelkey = levelkey
frag.sn = sn
frag.level = id
frag.cc = cc
frag.urlId = levelUrlId
frag.baseurl = baseurl
// avoid sliced strings https://github.com/video-dev/hls.js/issues/939
frag.relurl = (' ' + result[3]).slice(1)
assignProgramDateTime(frag, prevFrag)
level.fragments.push(frag)
prevFrag = frag
totalduration += frag.duration
frag = new Fragment()
}
} else if (result[4]) {
// X-BYTERANGE
const data = (' ' + result[4]).slice(1)
if (prevFrag) {
frag.setByteRange(data, prevFrag)
} else {
frag.setByteRange(data)
}
} else if (result[5]) {
// PROGRAM-DATE-TIME
// avoid sliced strings https://github.com/video-dev/hls.js/issues/939
frag.rawProgramDateTime = (' ' + result[5]).slice(1)
frag.tagList.push(['PROGRAM-DATE-TIME', frag.rawProgramDateTime])
if (firstPdtIndex === null) {
firstPdtIndex = level.fragments.length
}
} else {
result = result[0].match(LEVEL_PLAYLIST_REGEX_SLOW) as RegExpExecArray
for (i = 1; i < result.length; i++) {
if (typeof result[i] !== 'undefined') {
break
}
}
// avoid sliced strings https://github.com/video-dev/hls.js/issues/939
const value1 = (' ' + result[i + 1]).slice(1)
const value2 = (' ' + result[i + 2]).slice(1)
switch (result[i]) {
case '#':
frag.tagList.push(value2 ? [value1, value2] : [value1])
break
case 'PLAYLIST-TYPE':
level.type = value1.toUpperCase()
break
case 'MEDIA-SEQUENCE':
currentSN = level.startSN = parseInt(value1)
break
case 'TARGETDURATION':
level.targetduration = parseFloat(value1)
break
case 'VERSION':
level.version = parseInt(value1)
break
case 'EXTM3U':
break
case 'ENDLIST':
level.live = false
break
case 'DIS':
cc++
frag.tagList.push(['DIS'])
break
case 'DISCONTINUITY-SEQ':
cc = parseInt(value1)
break
case 'KEY': {
// https://tools.ietf.org/html/draft-pantos-http-live-streaming-08#section-3.4.4
const decryptparams = value1
const keyAttrs: any = new AttrList(decryptparams)
const decryptmethod = keyAttrs.enumeratedString('METHOD')
const decrypturi = keyAttrs.URI
const decryptiv = keyAttrs.hexadecimalInteger('IV')
if (decryptmethod) {
levelkey = new LevelKey(baseurl, decrypturi)
if (
decrypturi &&
['AES-128', 'SAMPLE-AES', 'SAMPLE-AES-CENC'].indexOf(
decryptmethod
) >= 0
) {
levelkey.method = decryptmethod
// URI to get the key
levelkey.baseuri = baseurl
levelkey.reluri = decrypturi
levelkey.key = null
// Initialization Vector (IV)
levelkey.iv = decryptiv
}
}
break
}
case 'START': {
const startAttrs = new AttrList(value1)
const startTimeOffset = startAttrs.decimalFloatingPoint('TIME-OFFSET')
// TIME-OFFSET can be 0
if (Number.isFinite(startTimeOffset)) {
level.startTimeOffset = startTimeOffset
}
break
}
case 'MAP': {
const mapAttrs: any = new AttrList(value1)
frag.relurl = mapAttrs.URI
if (mapAttrs.BYTERANGE) {
frag.setByteRange(mapAttrs.BYTERANGE)
}
frag.baseurl = baseurl
frag.level = id
frag.type = type
frag.sn = 'initSegment'
level.initSegment = frag
frag = new Fragment()
frag.rawProgramDateTime = level.initSegment.rawProgramDateTime
break
}
default:
logger.warn(this.Tag, `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 // ts 总时长
level.averagetargetduration = totalduration / level.fragments.length // 计算平均时长
level.endSN = currentSN - 1 // 最后ts文件的序列号
level.startCC = level.fragments[0] ? level.fragments[0].cc : 0
level.endCC = cc
if (!level.initSegment && level.fragments.length) {
// this is a bit lurky but HLS really has no other way to tell us
// if the fragments are TS or MP4, except if we download them :/
// but this is to be able to handle SIDX.
if (level.fragments.every(frag => MP4_REGEX_SUFFIX.test(frag.relurl))) {
logger.warn(
this.Tag,
'MP4 fragments found but no init segment (probably no MAP, incomplete M3U8), trying to fetch SIDX'
)
frag = new Fragment()
frag.relurl = level.fragments[0].relurl
frag.baseurl = baseurl
frag.level = id
frag.type = type
frag.sn = 'initSegment'
level.initSegment = frag
level.needSidxRanges = true
}
}
/**
* Backfill any missing PDT values
"If the first EXT-X-PROGRAM-DATE-TIME tag in a Playlist appears after
one or more Media Segment URIs, the client SHOULD extrapolate
backward from that tag (using EXTINF durations and/or media
timestamps) to associate dates with those segments."
* We have already extrapolated forward, but all fragments up to the first instance of PDT do not have their PDTs
* computed.
*/
if (firstPdtIndex) {
backfillProgramDateTimes(level.fragments, firstPdtIndex)
}
return level
}
}
function backfillProgramDateTimes(fragments: any, startIndex: number): void {
let fragPrev = fragments[startIndex]
for (let i = startIndex - 1; i >= 0; i--) {
const frag = fragments[i]
frag.programDateTime = fragPrev.programDateTime - frag.duration * 1000
fragPrev = frag
}
}
function assignProgramDateTime(frag: any, prevFrag: any): void {
if (frag.rawProgramDateTime) {
frag.programDateTime = Date.parse(frag.rawProgramDateTime)
} else if (prevFrag && prevFrag.programDateTime) {
frag.programDateTime = prevFrag.endProgramDateTime
}
if (!Number.isFinite(frag.programDateTime)) {
frag.programDateTime = null
frag.rawProgramDateTime = null
}
}
/* eslint-enable */