UNPKG

hjplayer

Version:

hjplayer, a HTML5 Player, can play flv and hls by Media Source Extension, based on typescript;

224 lines (175 loc) 6.92 kB
import URLToolkit from 'url-toolkit'; import logger from '../Utils/Logger'; import LevelKey from '../Parser/LevelKey'; import { PlaylistLevelType } from '../Interfaces/loader'; export enum ElementaryStreamTypes { AUDIO = 'audio', VIDEO = 'video' } export default class Fragment { Tag: string = 'Fragment' private _url: string | null = null private _byteRange: number[] | null = null private _decryptdata: LevelKey | null = null // Holds the types of data this fragment supports private _elementaryStreams: Record<ElementaryStreamTypes, boolean> = { [ElementaryStreamTypes.AUDIO]: false, [ElementaryStreamTypes.VIDEO]: false } // deltaPTS tracks the change in presentation timestamp between fragments public deltaPTS: number = 0 public rawProgramDateTime: string | null = null public programDateTime: number | null = null public title: string | null = null public tagList: Array<string[]> = [] // TODO: Move at least baseurl to constructor. // Currently we do a two-pass construction as use the Fragment class almost like a object for holding parsing state. // It may make more sense to just use a POJO to keep state during the parsing phase. // Have Fragment be the representation once we have a known state? // Something to think on. // Discontinuity Counter public cc!: number public type!: PlaylistLevelType // relurl is the portion of the URL that comes from inside the playlist. public relurl!: string // baseurl is the URL to the playlist public baseurl!: string // EXTINF has to be present for a m3u8 to be considered valid public duration!: number // When this segment starts in the timeline public start!: number // sn notates the sequence number for a segment, and if set to a string can be 'initSegment' public sn: number | 'initSegment' = 0 public urlId: number = 0 // level matches this fragment to a index playlist public level: number = 0 // levelkey is the EXT-X-KEY that applies to this segment for decryption // core difference from the private field _decryptdata is the lack of the initialized IV // _decryptdata will set the IV for this segment based on the segment number in the fragment public levelkey?: LevelKey // TODO(typescript-xhrloader) public loader: any // setByteRange converts a EXT-X-BYTERANGE attribute into a two element array setByteRange(value: string, previousFrag?: Fragment) { const params = value.split('@', 2); const byteRange: number[] = []; if(params.length === 1) { byteRange[0] = previousFrag ? previousFrag.byteRangeEndOffset : 0; } else { byteRange[0] = parseInt(params[1], 10); } byteRange[1] = parseInt(params[0], 10) + byteRange[0]; this._byteRange = byteRange; } 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 byteRange(): number[] { if(!this._byteRange) { return []; } return this._byteRange; } /** * @type {number} */ get byteRangeStartOffset() { return this.byteRange[0]; } get byteRangeEndOffset() { return this.byteRange[1]; } get decryptdata(): LevelKey | null { if(!this.levelkey && !this._decryptdata) { return null; } if(!this._decryptdata && this.levelkey) { let { sn } = this; if(typeof sn !== 'number') { // We are fetching decryption data for a initialization segment // If the segment was encrypted with AES-128 // It must have an IV defined. We cannot substitute the Segment Number in. if(this.levelkey && this.levelkey.method === 'AES-128' && !this.levelkey.iv) { logger.warn( this.Tag, `missing IV for initialization segment with method="${this.levelkey.method}" - compliance issue` ); } /* Be converted to a Number. 'initSegment' will become NaN. NaN, which when converted through ToInt32() -> +0. --- Explicitly set sn to resulting value from implicit conversions 'initSegment' values for IV generation. */ sn = 0; } this._decryptdata = this.setDecryptDataFromLevelKey(this.levelkey, sn); } return this._decryptdata; } get endProgramDateTime() { if(this.programDateTime === null) { return null; } if(!Number.isFinite(this.programDateTime)) { return null; } const duration = !Number.isFinite(this.duration) ? 0 : this.duration; return this.programDateTime + duration * 1000; } get encrypted() { return !!( this.decryptdata && this.decryptdata.uri !== null && this.decryptdata.key === null ); } /** * @param {ElementaryStreamTypes} type */ addElementaryStream(type: ElementaryStreamTypes) { this._elementaryStreams[type] = true; } /** * @param {ElementaryStreamTypes} type */ hasElementaryStream(type: ElementaryStreamTypes) { return this._elementaryStreams[type] === true; } /** * Utility method for parseLevelPlaylist to create an initialization vector for a given segment * @param {number} segmentNumber - segment number to generate IV with * @returns {Uint8Array} */ createInitializationVector(segmentNumber: number): Uint8Array { const uint8View = new Uint8Array(16); for(let 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 {LevelKey} - an object to be applied as a fragment's decryptdata */ setDecryptDataFromLevelKey(levelkey: LevelKey, segmentNumber: number): LevelKey { let decryptdata = levelkey; if(levelkey && levelkey.method && levelkey.uri && !levelkey.iv) { decryptdata = new LevelKey(levelkey.baseuri, levelkey.reluri); decryptdata.method = levelkey.method; decryptdata.iv = this.createInitializationVector(segmentNumber); } return decryptdata; } }