UNPKG

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
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 */