├── tslint.json ├── types ├── index.d.ts ├── test-flv.ts └── tsconfig.json ├── docs ├── architecture.png ├── design.md ├── livestream.md ├── multipart.md └── cors.md ├── d.ts └── src │ ├── utils │ └── typedarray-equality.d.ts │ ├── player │ ├── player-engine-worker.d.ts │ ├── player-engine-dedicated-thread-worker.d.ts │ ├── live-latency-synchronizer.d.ts │ ├── live-latency-chaser.d.ts │ ├── startup-stall-jumper.d.ts │ ├── player-engine.d.ts │ ├── loading-controller.d.ts │ ├── seeking-handler.d.ts │ ├── player-events.d.ts │ ├── mse-player.d.ts │ ├── player-engine-worker-cmd-def.d.ts │ ├── player-engine-dedicated-thread.d.ts │ ├── player-engine-main-thread.d.ts │ └── player-engine-worker-msg-def.d.ts │ ├── demux │ ├── mp3.d.ts │ ├── pgs-data.d.ts │ ├── pes-private-data.d.ts │ ├── av1.d.ts │ ├── klv.d.ts │ ├── smpte2038.d.ts │ ├── mpeg4-audio.d.ts │ ├── patpmt.d.ts │ ├── h264.d.ts │ ├── aac.d.ts │ ├── base-demuxer.d.ts │ ├── ac3.d.ts │ ├── h265.d.ts │ ├── pat-pmt-pes.d.ts │ ├── av1-parser.d.ts │ └── ts-demuxer.d.ts │ └── core │ ├── mse-events.d.ts │ └── transmuxing-events.d.ts ├── src ├── index.js ├── demux │ ├── mp3.ts │ ├── pgs-data.ts │ ├── pes-private-data.ts │ ├── mpeg4-audio.ts │ ├── demux-errors.js │ ├── klv.ts │ ├── base-demuxer.ts │ ├── smpte2038.ts │ ├── pat-pmt-pes.ts │ ├── av1.ts │ ├── exp-golomb.js │ └── h264.ts ├── core │ ├── mse-events.ts │ ├── transmuxing-events.ts │ ├── media-info.js │ ├── features.js │ └── media-segment-info.js ├── player │ ├── player-engine.ts │ ├── player-errors.js │ ├── player-events.ts │ ├── player-engine-worker-cmd-def.ts │ ├── live-latency-chaser.ts │ ├── startup-stall-jumper.ts │ ├── player-engine-worker-msg-def.ts │ ├── live-latency-synchronizer.ts │ ├── mse-player.ts │ └── loading-controller.ts ├── io │ ├── range-seek-handler.js │ ├── param-seek-handler.js │ ├── speed-sampler.js │ ├── loader.js │ ├── websocket-loader.js │ └── xhr-moz-chunked-loader.js ├── utils │ ├── exception.js │ ├── typedarray-equality.ts │ ├── polyfill.js │ ├── utf8-conv.js │ ├── logger.js │ ├── browser.js │ ├── logging-control.js │ └── webworkify-webpack.js ├── config.js ├── mpegts.js └── remux │ └── aac-silent.js ├── tsconfig.json ├── webpack.config.js ├── package.json ├── demo └── demo.css ├── .gitignore ├── .npmignore ├── README_zh.md ├── README_ja.md └── README.md /tslint.json: -------------------------------------------------------------------------------- 1 | { "extends": "dtslint/dt.json" } 2 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | // TypeScript Version: 2.3 2 | 3 | import '../d.ts/mpegts.d.ts'; 4 | -------------------------------------------------------------------------------- /docs/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsukumijima/mpegts.js/master/docs/architecture.png -------------------------------------------------------------------------------- /docs/design.md: -------------------------------------------------------------------------------- 1 | 2 | flv.js design 3 | ====== 4 | 5 | Architecture overview: 6 | 7 | ![arch](architecture.png) 8 | -------------------------------------------------------------------------------- /d.ts/src/utils/typedarray-equality.d.ts: -------------------------------------------------------------------------------- 1 | declare function buffersAreEqual(a: Uint8Array, b: Uint8Array): boolean; 2 | export default buffersAreEqual; 3 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // entry/index file 2 | 3 | // make it compatible with browserify's umd wrapper 4 | module.exports = require('./mpegts.js').default; 5 | -------------------------------------------------------------------------------- /d.ts/src/player/player-engine-worker.d.ts: -------------------------------------------------------------------------------- 1 | declare const PlayerEngineWorker: (self: DedicatedWorkerGlobalScope) => void; 2 | export default PlayerEngineWorker; 3 | -------------------------------------------------------------------------------- /src/demux/mp3.ts: -------------------------------------------------------------------------------- 1 | export class MP3Data { 2 | object_type: number; 3 | sample_rate: number; 4 | channel_count: number; 5 | 6 | data: Uint8Array; 7 | } 8 | -------------------------------------------------------------------------------- /d.ts/src/player/player-engine-dedicated-thread-worker.d.ts: -------------------------------------------------------------------------------- 1 | declare let PlayerEngineWorker: (self: DedicatedWorkerGlobalScope) => void; 2 | export default PlayerEngineWorker; 3 | -------------------------------------------------------------------------------- /d.ts/src/demux/mp3.d.ts: -------------------------------------------------------------------------------- 1 | export declare class MP3Data { 2 | object_type: number; 3 | sample_rate: number; 4 | channel_count: number; 5 | data: Uint8Array; 6 | } 7 | -------------------------------------------------------------------------------- /d.ts/src/demux/pgs-data.d.ts: -------------------------------------------------------------------------------- 1 | export declare class PGSData { 2 | pid: number; 3 | stream_id: number; 4 | pts?: number; 5 | dts?: number; 6 | lang: string; 7 | data: Uint8Array; 8 | len: number; 9 | } 10 | -------------------------------------------------------------------------------- /types/test-flv.ts: -------------------------------------------------------------------------------- 1 | import mpegts from '../'; 2 | 3 | type LoaderStatusAlias = mpegts.LoaderStatus; 4 | type LoaderErrorsAlias = mpegts.LoaderErrors; 5 | 6 | interface MediaDataSourceExt extends mpegts.MediaDataSource { 7 | example: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/demux/pgs-data.ts: -------------------------------------------------------------------------------- 1 | // ISO/IEC 13818-1 PES packets containing private data (stream_type=0x06) 2 | export class PGSData { 3 | pid: number; 4 | stream_id: number; 5 | pts?: number; 6 | dts?: number; 7 | lang: string; 8 | data: Uint8Array; 9 | len: number; 10 | } 11 | 12 | -------------------------------------------------------------------------------- /d.ts/src/core/mse-events.d.ts: -------------------------------------------------------------------------------- 1 | declare enum MSEEvents { 2 | ERROR = "error", 3 | SOURCE_OPEN = "source_open", 4 | UPDATE_END = "update_end", 5 | BUFFER_FULL = "buffer_full", 6 | START_STREAMING = "start_streaming", 7 | END_STREAMING = "end_streaming" 8 | } 9 | export default MSEEvents; 10 | -------------------------------------------------------------------------------- /d.ts/src/demux/pes-private-data.d.ts: -------------------------------------------------------------------------------- 1 | export declare class PESPrivateData { 2 | pid: number; 3 | stream_id: number; 4 | pts?: number; 5 | dts?: number; 6 | nearest_pts?: number; 7 | data: Uint8Array; 8 | len: number; 9 | } 10 | export declare class PESPrivateDataDescriptor { 11 | pid: number; 12 | stream_type: number; 13 | descriptor: Uint8Array; 14 | } 15 | -------------------------------------------------------------------------------- /d.ts/src/player/live-latency-synchronizer.d.ts: -------------------------------------------------------------------------------- 1 | declare class LiveLatencySynchronizer { 2 | TAG: string; 3 | private _config; 4 | private _media_element; 5 | private e?; 6 | constructor(config: any, media_element: HTMLMediaElement); 7 | destroy(): void; 8 | private _onMediaTimeUpdate; 9 | private _getCurrentLatency; 10 | } 11 | export default LiveLatencySynchronizer; 12 | -------------------------------------------------------------------------------- /d.ts/src/player/live-latency-chaser.d.ts: -------------------------------------------------------------------------------- 1 | declare class LiveLatencyChaser { 2 | private _config; 3 | private _media_element; 4 | private _on_direct_seek; 5 | constructor(config: any, media_element: HTMLMediaElement, on_direct_seek: (target: number) => void); 6 | destroy(): void; 7 | notifyBufferedRangeUpdate(): void; 8 | private _chaseLiveLatency; 9 | } 10 | export default LiveLatencyChaser; 11 | -------------------------------------------------------------------------------- /d.ts/src/demux/av1.d.ts: -------------------------------------------------------------------------------- 1 | export declare class AV1OBUInMpegTsParser { 2 | private readonly TAG; 3 | private data_; 4 | private current_startcode_offset_; 5 | private eof_flag_; 6 | static _ebsp2rbsp(uint8array: Uint8Array): Uint8Array; 7 | constructor(data: Uint8Array); 8 | private findNextStartCodeOffset; 9 | readNextOBUPayload(): Uint8Array | null; 10 | } 11 | export default AV1OBUInMpegTsParser; 12 | -------------------------------------------------------------------------------- /src/demux/pes-private-data.ts: -------------------------------------------------------------------------------- 1 | // ISO/IEC 13818-1 PES packets containing private data (stream_type=0x06) 2 | export class PESPrivateData { 3 | pid: number; 4 | stream_id: number; 5 | pts?: number; 6 | dts?: number; 7 | nearest_pts?: number; 8 | data: Uint8Array; 9 | len: number; 10 | } 11 | 12 | export class PESPrivateDataDescriptor { 13 | pid: number; 14 | stream_type: number; 15 | descriptor: Uint8Array; 16 | } 17 | -------------------------------------------------------------------------------- /d.ts/src/demux/klv.d.ts: -------------------------------------------------------------------------------- 1 | export declare class KLVData { 2 | pid: number; 3 | stream_id: number; 4 | pts?: number; 5 | dts?: number; 6 | access_units: AccessUnit[]; 7 | data: Uint8Array; 8 | len: number; 9 | } 10 | type AccessUnit = { 11 | service_id: number; 12 | sequence_number: number; 13 | flags: number; 14 | data: Uint8Array; 15 | }; 16 | export declare const klv_parse: (data: Uint8Array) => AccessUnit[]; 17 | export {}; 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "allowJs": true, 5 | "sourceMap": true, 6 | "module": "es6", 7 | "target": "es5", 8 | "experimentalDecorators": true, 9 | "declaration": true, 10 | "declarationDir": "./d.ts/", 11 | "lib": ["dom", "es5", "es2015.promise", "es2015.collection", "ESNext", "WebWorker"] 12 | }, 13 | "include": [ 14 | "./src/**/*", 15 | ] 16 | } -------------------------------------------------------------------------------- /d.ts/src/player/startup-stall-jumper.d.ts: -------------------------------------------------------------------------------- 1 | declare class StartupStallJumper { 2 | private readonly TAG; 3 | private _media_element; 4 | private _on_direct_seek; 5 | private _canplay_received; 6 | private e; 7 | constructor(media_element: HTMLMediaElement, on_direct_seek: (target: number) => void); 8 | destroy(): void; 9 | private _onMediaCanPlay; 10 | private _onMediaStalled; 11 | private _onMediaProgress; 12 | private _detectAndFixStuckPlayback; 13 | } 14 | export default StartupStallJumper; 15 | -------------------------------------------------------------------------------- /types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "lib": [ 5 | "es6", 6 | "dom" 7 | ], 8 | "noImplicitAny": true, 9 | "noImplicitThis": true, 10 | "strictNullChecks": true, 11 | "strictFunctionTypes": true, 12 | "baseUrl": "./", 13 | "typeRoots": [ 14 | "./" 15 | ], 16 | "types": [], 17 | "noEmit": true, 18 | "forceConsistentCasingInFileNames": true 19 | }, 20 | "files": [ 21 | "index.d.ts", 22 | "../d.ts/mpegts.d.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /d.ts/src/demux/smpte2038.d.ts: -------------------------------------------------------------------------------- 1 | export declare class SMPTE2038Data { 2 | pid: number; 3 | stream_id: number; 4 | pts?: number; 5 | dts?: number; 6 | nearest_pts?: number; 7 | ancillaries: AncillaryData[]; 8 | data: Uint8Array; 9 | len: number; 10 | } 11 | type AncillaryData = { 12 | yc_indicator: boolean; 13 | line_number: number; 14 | horizontal_offset: number; 15 | did: number; 16 | sdid: number; 17 | user_data: Uint8Array; 18 | description: string; 19 | information: any; 20 | }; 21 | export declare const smpte2038parse: (data: Uint8Array) => AncillaryData[]; 22 | export {}; 23 | -------------------------------------------------------------------------------- /d.ts/src/player/player-engine.d.ts: -------------------------------------------------------------------------------- 1 | import type MediaInfo from "../core/media-info"; 2 | export default interface PlayerEngine { 3 | destroy(): void; 4 | on(event: string, listener: (...args: any[]) => void): void; 5 | off(event: string, listener: (...args: any[]) => void): void; 6 | attachMediaElement(mediaElement: HTMLMediaElement): void; 7 | detachMediaElement(): void; 8 | load(): void; 9 | unload(): void; 10 | play(): Promise; 11 | pause(): void; 12 | seek(seconds: number): void; 13 | switchPrimaryAudio(): void; 14 | switchSecondaryAudio(): void; 15 | readonly mediaInfo: MediaInfo | undefined; 16 | readonly statisticsInfo: any | undefined; 17 | } 18 | -------------------------------------------------------------------------------- /d.ts/src/demux/mpeg4-audio.d.ts: -------------------------------------------------------------------------------- 1 | export declare enum MPEG4AudioObjectTypes { 2 | kNull = 0, 3 | kAACMain = 1, 4 | kAAC_LC = 2,// LC-AAC 5 | kAAC_SSR = 3, 6 | kAAC_LTP = 4, 7 | kAAC_SBR = 5,// HE-AAC 8 | kAAC_Scalable = 6, 9 | kLayer1 = 32, 10 | kLayer2 = 33, 11 | kLayer3 = 34 12 | } 13 | export declare enum MPEG4SamplingFrequencyIndex { 14 | k96000Hz = 0, 15 | k88200Hz = 1, 16 | k64000Hz = 2, 17 | k48000Hz = 3, 18 | k44100Hz = 4, 19 | k32000Hz = 5, 20 | k24000Hz = 6, 21 | k22050Hz = 7, 22 | k16000Hz = 8, 23 | k12000Hz = 9, 24 | k11025Hz = 10, 25 | k8000Hz = 11, 26 | k7350Hz = 12 27 | } 28 | export declare const MPEG4SamplingFrequencies: number[]; 29 | -------------------------------------------------------------------------------- /d.ts/src/player/loading-controller.d.ts: -------------------------------------------------------------------------------- 1 | declare class LoadingController { 2 | private readonly TAG; 3 | private _config; 4 | private _media_element; 5 | private _on_pause_transmuxer; 6 | private _on_resume_transmuxer; 7 | private _paused; 8 | private e?; 9 | constructor(config: any, media_element: HTMLMediaElement, on_pause_transmuxer: () => void, on_resume_transmuxer: () => void); 10 | destroy(): void; 11 | notifyBufferedPositionChanged(buffered_position?: number): void; 12 | private _onMediaTimeUpdate; 13 | private _suspendTransmuxerIfNeeded; 14 | private _suspendTransmuxerIfBufferedPositionExceeded; 15 | suspendTransmuxer(): void; 16 | private _resumeTransmuxerIfNeeded; 17 | resumeTransmuxer(): void; 18 | } 19 | export default LoadingController; 20 | -------------------------------------------------------------------------------- /src/demux/mpeg4-audio.ts: -------------------------------------------------------------------------------- 1 | export enum MPEG4AudioObjectTypes { 2 | kNull = 0, 3 | kAACMain, 4 | kAAC_LC, // LC-AAC 5 | kAAC_SSR, 6 | kAAC_LTP, 7 | kAAC_SBR, // HE-AAC 8 | kAAC_Scalable, 9 | 10 | kLayer1 = 32, 11 | kLayer2, 12 | kLayer3, // MP3 13 | } 14 | 15 | export enum MPEG4SamplingFrequencyIndex { 16 | k96000Hz = 0, 17 | k88200Hz, 18 | k64000Hz, 19 | k48000Hz, 20 | k44100Hz, 21 | k32000Hz, 22 | k24000Hz, 23 | k22050Hz, 24 | k16000Hz, 25 | k12000Hz, 26 | k11025Hz, 27 | k8000Hz, 28 | k7350Hz, 29 | } 30 | 31 | export const MPEG4SamplingFrequencies = [ 32 | 96000, 33 | 88200, 34 | 64000, 35 | 48000, 36 | 44100, 37 | 32000, 38 | 24000, 39 | 22050, 40 | 16000, 41 | 12000, 42 | 11025, 43 | 8000, 44 | 7350, 45 | ]; 46 | -------------------------------------------------------------------------------- /d.ts/src/player/seeking-handler.d.ts: -------------------------------------------------------------------------------- 1 | declare class SeekingHandler { 2 | private readonly TAG; 3 | private _config; 4 | private _media_element; 5 | private _always_seek_keyframe; 6 | private _on_unbuffered_seek; 7 | private _request_set_current_time; 8 | private _seek_request_record_clocktime?; 9 | private _idr_sample_list; 10 | private e?; 11 | constructor(config: any, media_element: HTMLMediaElement, on_unbuffered_seek: (milliseconds: number) => void); 12 | destroy(): void; 13 | seek(seconds: number): void; 14 | directSeek(seconds: number): void; 15 | appendSyncPoints(syncpoints: any[]): void; 16 | private _onMediaSeeking; 17 | private _pollAndApplyUnbufferedSeek; 18 | private _isPositionBuffered; 19 | private _getNearestKeyframe; 20 | private static _getClockTime; 21 | } 22 | export default SeekingHandler; 23 | -------------------------------------------------------------------------------- /src/demux/demux-errors.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Bilibili. All Rights Reserved. 3 | * 4 | * @author zheng qian 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | const DemuxErrors = { 20 | OK: 'OK', 21 | FORMAT_ERROR: 'FormatError', 22 | FORMAT_UNSUPPORTED: 'FormatUnsupported', 23 | CODEC_UNSUPPORTED: 'CodecUnsupported' 24 | }; 25 | 26 | export default DemuxErrors; -------------------------------------------------------------------------------- /d.ts/src/player/player-events.d.ts: -------------------------------------------------------------------------------- 1 | declare enum PlayerEvents { 2 | ERROR = "error", 3 | LOADING_COMPLETE = "loading_complete", 4 | RECOVERED_EARLY_EOF = "recovered_early_eof", 5 | MEDIA_INFO = "media_info", 6 | METADATA_ARRIVED = "metadata_arrived", 7 | SCRIPTDATA_ARRIVED = "scriptdata_arrived", 8 | TIMED_ID3_METADATA_ARRIVED = "timed_id3_metadata_arrived", 9 | PGS_SUBTITLE_ARRIVED = "pgs_subtitle_arrived", 10 | SYNCHRONOUS_KLV_METADATA_ARRIVED = "synchronous_klv_metadata_arrived", 11 | ASYNCHRONOUS_KLV_METADATA_ARRIVED = "asynchronous_klv_metadata_arrived", 12 | SMPTE2038_METADATA_ARRIVED = "smpte2038_metadata_arrived", 13 | SCTE35_METADATA_ARRIVED = "scte35_metadata_arrived", 14 | PES_PRIVATE_DATA_DESCRIPTOR = "pes_private_data_descriptor", 15 | PES_PRIVATE_DATA_ARRIVED = "pes_private_data_arrived", 16 | STATISTICS_INFO = "statistics_info", 17 | DESTROYING = "destroying" 18 | } 19 | export default PlayerEvents; 20 | -------------------------------------------------------------------------------- /src/core/mse-events.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Bilibili. All Rights Reserved. 3 | * 4 | * @author zheng qian 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | enum MSEEvents { 20 | ERROR = 'error', 21 | SOURCE_OPEN = 'source_open', 22 | UPDATE_END = 'update_end', 23 | BUFFER_FULL = 'buffer_full', 24 | START_STREAMING = 'start_streaming', 25 | END_STREAMING = 'end_streaming', 26 | }; 27 | 28 | export default MSEEvents; 29 | -------------------------------------------------------------------------------- /d.ts/src/demux/patpmt.d.ts: -------------------------------------------------------------------------------- 1 | interface ProgramPMTPIDMap { 2 | [program: number]: number; 3 | } 4 | export declare class PAT { 5 | version_number: number; 6 | network_pid: number; 7 | program_pmt_pid: ProgramPMTPIDMap; 8 | } 9 | export declare enum StreamType { 10 | kMPEG1Audio = 3, 11 | kMPEG2Audio = 4, 12 | kPESPrivateData = 6, 13 | kADTSAAC = 15, 14 | kID3 = 21, 15 | kH264 = 27, 16 | kH265 = 36 17 | } 18 | interface PIDStreamTypeMap { 19 | [pid: number]: StreamType; 20 | } 21 | export declare class PMT { 22 | program_number: number; 23 | version_number: number; 24 | pcr_pid: number; 25 | pid_stream_type: PIDStreamTypeMap; 26 | common_pids: { 27 | h264: number | undefined; 28 | adts_aac: number | undefined; 29 | }; 30 | pes_private_data_pids: { 31 | [pid: number]: boolean; 32 | }; 33 | timed_id3_pids: { 34 | [pid: number]: boolean; 35 | }; 36 | } 37 | export interface ProgramPMTMap { 38 | [program: number]: PMT; 39 | } 40 | export {}; 41 | -------------------------------------------------------------------------------- /src/demux/klv.ts: -------------------------------------------------------------------------------- 1 | export class KLVData { 2 | pid: number; 3 | stream_id: number; 4 | pts?: number; 5 | dts?: number; 6 | access_units: AccessUnit[]; 7 | data: Uint8Array; 8 | len: number; 9 | } 10 | 11 | type AccessUnit = { 12 | service_id: number; 13 | sequence_number: number; 14 | flags: number; 15 | data: Uint8Array; 16 | } 17 | 18 | export const klv_parse = (data: Uint8Array) => { 19 | let result: AccessUnit[] = []; 20 | 21 | let offset = 0; 22 | while (offset + 5 < data.byteLength) { 23 | let service_id = data[offset + 0]; 24 | let sequence_number = data[offset + 1]; 25 | let flags = data[offset + 2]; 26 | let au_size = (data[offset + 3] << 8) | (data[offset + 4] << 0); 27 | let au_data = data.slice(offset + 5, offset + 5 + au_size); 28 | 29 | result.push({ 30 | service_id, 31 | sequence_number, 32 | flags, 33 | data: au_data 34 | }); 35 | 36 | offset += 5 + au_size; 37 | } 38 | 39 | return result; 40 | } -------------------------------------------------------------------------------- /d.ts/src/core/transmuxing-events.d.ts: -------------------------------------------------------------------------------- 1 | declare enum TransmuxingEvents { 2 | IO_ERROR = "io_error", 3 | DEMUX_ERROR = "demux_error", 4 | INIT_SEGMENT = "init_segment", 5 | MEDIA_SEGMENT = "media_segment", 6 | LOADING_COMPLETE = "loading_complete", 7 | RECOVERED_EARLY_EOF = "recovered_early_eof", 8 | MEDIA_INFO = "media_info", 9 | METADATA_ARRIVED = "metadata_arrived", 10 | SCRIPTDATA_ARRIVED = "scriptdata_arrived", 11 | TIMED_ID3_METADATA_ARRIVED = "timed_id3_metadata_arrived", 12 | PGS_SUBTITLE_ARRIVED = "pgs_subtitle_arrived", 13 | SYNCHRONOUS_KLV_METADATA_ARRIVED = "synchronous_klv_metadata_arrived", 14 | ASYNCHRONOUS_KLV_METADATA_ARRIVED = "asynchronous_klv_metadata_arrived", 15 | SMPTE2038_METADATA_ARRIVED = "smpte2038_metadata_arrived", 16 | SCTE35_METADATA_ARRIVED = "scte35_metadata_arrived", 17 | PES_PRIVATE_DATA_DESCRIPTOR = "pes_private_data_descriptor", 18 | PES_PRIVATE_DATA_ARRIVED = "pes_private_data_arrived", 19 | STATISTICS_INFO = "statistics_info", 20 | RECOMMEND_SEEKPOINT = "recommend_seekpoint" 21 | } 22 | export default TransmuxingEvents; 23 | -------------------------------------------------------------------------------- /d.ts/src/player/mse-player.d.ts: -------------------------------------------------------------------------------- 1 | import MediaInfo from '../core/media-info'; 2 | declare class MSEPlayer { 3 | private readonly TAG; 4 | private _type; 5 | private _media_element; 6 | private _player_engine; 7 | constructor(mediaDataSource: any, config?: any); 8 | destroy(): void; 9 | on(event: string, listener: (...args: any[]) => void): void; 10 | off(event: string, listener: (...args: any[]) => void): void; 11 | attachMediaElement(mediaElement: HTMLMediaElement): void; 12 | detachMediaElement(): void; 13 | load(): void; 14 | unload(): void; 15 | play(): Promise; 16 | pause(): void; 17 | switchPrimaryAudio(): void; 18 | switchSecondaryAudio(): void; 19 | get type(): string; 20 | get buffered(): TimeRanges; 21 | get duration(): number; 22 | get volume(): number; 23 | set volume(value: number); 24 | get muted(): boolean; 25 | set muted(muted: boolean); 26 | get currentTime(): number; 27 | set currentTime(seconds: number); 28 | get mediaInfo(): MediaInfo; 29 | get statisticsInfo(): any; 30 | } 31 | export default MSEPlayer; 32 | -------------------------------------------------------------------------------- /d.ts/src/demux/h264.d.ts: -------------------------------------------------------------------------------- 1 | export declare enum H264NaluType { 2 | kUnspecified = 0, 3 | kSliceNonIDR = 1, 4 | kSliceDPA = 2, 5 | kSliceDPB = 3, 6 | kSliceDPC = 4, 7 | kSliceIDR = 5, 8 | kSliceSEI = 6, 9 | kSliceSPS = 7, 10 | kSlicePPS = 8, 11 | kSliceAUD = 9, 12 | kEndOfSequence = 10, 13 | kEndOfStream = 11, 14 | kFiller = 12, 15 | kSPSExt = 13, 16 | kReserved0 = 14 17 | } 18 | export declare class H264NaluPayload { 19 | type: H264NaluType; 20 | data: Uint8Array; 21 | } 22 | export declare class H264NaluAVC1 { 23 | type: H264NaluType; 24 | data: Uint8Array; 25 | constructor(nalu: H264NaluPayload); 26 | } 27 | export declare class H264AnnexBParser { 28 | private readonly TAG; 29 | private data_; 30 | private current_startcode_offset_; 31 | private eof_flag_; 32 | constructor(data: Uint8Array); 33 | private findNextStartCodeOffset; 34 | readNextNaluPayload(): H264NaluPayload | null; 35 | } 36 | export declare class AVCDecoderConfigurationRecord { 37 | private data; 38 | constructor(sps: Uint8Array, pps: Uint8Array, sps_details: any); 39 | getData(): Uint8Array; 40 | } 41 | -------------------------------------------------------------------------------- /d.ts/src/player/player-engine-worker-cmd-def.d.ts: -------------------------------------------------------------------------------- 1 | export type WorkerCommandOp = 'logging_config' | 'init' | 'destroy' | 'initialize_mse' | 'shutdown_mse' | 'load' | 'unload' | 'unbuffered_seek' | 'timeupdate' | 'readystatechange' | 'pause_transmuxer' | 'resume_transmuxer' | 'switch_audio'; 2 | export type WorkerCommandPacket = { 3 | cmd: WorkerCommandOp; 4 | }; 5 | export type WorkerCommandPacketInit = WorkerCommandPacket & { 6 | cmd: 'init'; 7 | media_data_source: any; 8 | config: any; 9 | }; 10 | export type WorkerCommandPacketLoggingConfig = WorkerCommandPacket & { 11 | cmd: 'logging_config'; 12 | logging_config: any; 13 | }; 14 | export type WorkerCommandPacketUnbufferedSeek = WorkerCommandPacket & { 15 | cmd: 'unbuffered_seek'; 16 | milliseconds: number; 17 | }; 18 | export type WorkerCommandPacketTimeUpdate = WorkerCommandPacket & { 19 | cmd: 'timeupdate'; 20 | current_time: number; 21 | }; 22 | export type WorkerCommandPacketReadyStateChange = WorkerCommandPacket & { 23 | cmd: 'readystatechange'; 24 | ready_state: number; 25 | }; 26 | export type WorkerCommandPacketSwitchAudio = WorkerCommandPacket & { 27 | cmd: 'switch_audio'; 28 | audio_track: 'primary' | 'secondary'; 29 | }; 30 | -------------------------------------------------------------------------------- /src/player/player-engine.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 zheng qian. All Rights Reserved. 3 | * 4 | * @author zheng qian 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | import type MediaInfo from "../core/media-info"; 20 | 21 | export default interface PlayerEngine { 22 | destroy(): void; 23 | on(event: string, listener: (...args: any[]) => void): void; 24 | off(event: string, listener: (...args: any[]) => void): void; 25 | attachMediaElement(mediaElement: HTMLMediaElement): void; 26 | detachMediaElement(): void; 27 | load(): void; 28 | unload(): void; 29 | play(): Promise; 30 | pause(): void; 31 | seek(seconds: number): void; 32 | switchPrimaryAudio(): void; 33 | switchSecondaryAudio(): void; 34 | readonly mediaInfo: MediaInfo | undefined; 35 | readonly statisticsInfo: any | undefined; 36 | } 37 | -------------------------------------------------------------------------------- /src/player/player-errors.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Bilibili. All Rights Reserved. 3 | * 4 | * @author zheng qian 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | import {LoaderErrors} from '../io/loader.js'; 20 | import DemuxErrors from '../demux/demux-errors.js'; 21 | 22 | export const ErrorTypes = { 23 | NETWORK_ERROR: 'NetworkError', 24 | MEDIA_ERROR: 'MediaError', 25 | OTHER_ERROR: 'OtherError' 26 | }; 27 | 28 | export const ErrorDetails = { 29 | NETWORK_EXCEPTION: LoaderErrors.EXCEPTION, 30 | NETWORK_STATUS_CODE_INVALID: LoaderErrors.HTTP_STATUS_CODE_INVALID, 31 | NETWORK_TIMEOUT: LoaderErrors.CONNECTING_TIMEOUT, 32 | NETWORK_UNRECOVERABLE_EARLY_EOF: LoaderErrors.UNRECOVERABLE_EARLY_EOF, 33 | 34 | MEDIA_MSE_ERROR: 'MediaMSEError', 35 | 36 | MEDIA_FORMAT_ERROR: DemuxErrors.FORMAT_ERROR, 37 | MEDIA_FORMAT_UNSUPPORTED: DemuxErrors.FORMAT_UNSUPPORTED, 38 | MEDIA_CODEC_UNSUPPORTED: DemuxErrors.CODEC_UNSUPPORTED 39 | }; -------------------------------------------------------------------------------- /d.ts/src/demux/aac.d.ts: -------------------------------------------------------------------------------- 1 | import { MPEG4AudioObjectTypes, MPEG4SamplingFrequencyIndex } from "./mpeg4-audio"; 2 | export declare class AACFrame { 3 | audio_object_type: MPEG4AudioObjectTypes; 4 | sampling_freq_index: MPEG4SamplingFrequencyIndex; 5 | sampling_frequency: number; 6 | channel_config: number; 7 | data: Uint8Array; 8 | } 9 | export declare class LOASAACFrame extends AACFrame { 10 | other_data_present: boolean; 11 | } 12 | export declare class AACADTSParser { 13 | private readonly TAG; 14 | private data_; 15 | private current_syncword_offset_; 16 | private eof_flag_; 17 | private has_last_incomplete_data; 18 | constructor(data: Uint8Array); 19 | private findNextSyncwordOffset; 20 | readNextAACFrame(): AACFrame | null; 21 | hasIncompleteData(): boolean; 22 | getIncompleteData(): Uint8Array; 23 | } 24 | export declare class AACLOASParser { 25 | private readonly TAG; 26 | private data_; 27 | private current_syncword_offset_; 28 | private eof_flag_; 29 | private has_last_incomplete_data; 30 | constructor(data: Uint8Array); 31 | private findNextSyncwordOffset; 32 | private getLATMValue; 33 | readNextAACFrame(privious?: LOASAACFrame): LOASAACFrame | null; 34 | hasIncompleteData(): boolean; 35 | getIncompleteData(): Uint8Array; 36 | } 37 | export declare class AudioSpecificConfig { 38 | config: Array; 39 | sampling_rate: number; 40 | channel_count: number; 41 | codec_mimetype: string; 42 | original_codec_mimetype: string; 43 | constructor(frame: AACFrame); 44 | } 45 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const packagejson = require("./package.json"); 3 | const path = require('path'); 4 | const TerserPlugin = require('terser-webpack-plugin'); 5 | 6 | module.exports = { 7 | entry: './src/index.js', 8 | output: { 9 | filename: 'mpegts.js', 10 | path: path.resolve(__dirname, 'dist'), 11 | library: 'mpegts', 12 | libraryTarget: 'umd' 13 | }, 14 | 15 | devtool: 'source-map', 16 | 17 | resolve: { 18 | extensions: ['.ts', '.tsx', '.js', '.json'] 19 | }, 20 | 21 | plugins: [ 22 | new webpack.DefinePlugin({ 23 | __VERSION__: JSON.stringify(packagejson.version) 24 | }) 25 | ], 26 | 27 | node: { 28 | 'fs': 'empty', 29 | 'path': 'empty' 30 | }, 31 | 32 | optimization: { 33 | minimizer: [ 34 | new TerserPlugin({ 35 | sourceMap: true 36 | }) 37 | ] 38 | }, 39 | 40 | module: { 41 | rules: [ 42 | { 43 | test: /\.(ts|js)$/, 44 | use: 'ts-loader', 45 | exclude: /node-modules/ 46 | }, 47 | { 48 | enforce: 'pre', 49 | test: /\.js$/, 50 | use: 'source-map-loader' 51 | } 52 | ] 53 | }, 54 | 55 | devServer: { 56 | static: ['demo'], 57 | proxy: { 58 | '/dist': { 59 | target: 'http://localhost:8080', 60 | pathRewrite: {'^/dist' : ''} 61 | } 62 | } 63 | } 64 | }; 65 | -------------------------------------------------------------------------------- /src/io/range-seek-handler.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Bilibili. All Rights Reserved. 3 | * 4 | * @author zheng qian 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | class RangeSeekHandler { 20 | 21 | constructor(zeroStart) { 22 | this._zeroStart = zeroStart || false; 23 | } 24 | 25 | getConfig(url, range) { 26 | let headers = {}; 27 | 28 | if (range.from !== 0 || range.to !== -1) { 29 | let param; 30 | if (range.to !== -1) { 31 | param = `bytes=${range.from.toString()}-${range.to.toString()}`; 32 | } else { 33 | param = `bytes=${range.from.toString()}-`; 34 | } 35 | headers['Range'] = param; 36 | } else if (this._zeroStart) { 37 | headers['Range'] = 'bytes=0-'; 38 | } 39 | 40 | return { 41 | url: url, 42 | headers: headers 43 | }; 44 | } 45 | 46 | removeURLParameters(seekedURL) { 47 | return seekedURL; 48 | } 49 | 50 | } 51 | 52 | export default RangeSeekHandler; -------------------------------------------------------------------------------- /src/player/player-events.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Bilibili. All Rights Reserved. 3 | * 4 | * @author zheng qian 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | enum PlayerEvents { 20 | ERROR = 'error', 21 | LOADING_COMPLETE = 'loading_complete', 22 | RECOVERED_EARLY_EOF = 'recovered_early_eof', 23 | MEDIA_INFO = 'media_info', 24 | METADATA_ARRIVED = 'metadata_arrived', 25 | SCRIPTDATA_ARRIVED = 'scriptdata_arrived', 26 | TIMED_ID3_METADATA_ARRIVED = 'timed_id3_metadata_arrived', 27 | PGS_SUBTITLE_ARRIVED = 'pgs_subtitle_arrived', 28 | SYNCHRONOUS_KLV_METADATA_ARRIVED = 'synchronous_klv_metadata_arrived', 29 | ASYNCHRONOUS_KLV_METADATA_ARRIVED = 'asynchronous_klv_metadata_arrived', 30 | SMPTE2038_METADATA_ARRIVED = 'smpte2038_metadata_arrived', 31 | SCTE35_METADATA_ARRIVED = 'scte35_metadata_arrived', 32 | PES_PRIVATE_DATA_DESCRIPTOR = 'pes_private_data_descriptor', 33 | PES_PRIVATE_DATA_ARRIVED = 'pes_private_data_arrived', 34 | STATISTICS_INFO = 'statistics_info', 35 | DESTROYING = 'destroying' 36 | }; 37 | 38 | export default PlayerEvents; 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mpegts.js", 3 | "version": "1.8.0", 4 | "description": "HTML5 MPEG2-TS Stream Player", 5 | "main": "./dist/mpegts.js", 6 | "types": "./d.ts/mpegts.d.ts", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/xqq/mpegts.js" 10 | }, 11 | "keywords": [ 12 | "html5", 13 | "mpegts", 14 | "m2ts", 15 | "mse", 16 | "arib", 17 | "dvb" 18 | ], 19 | "scripts": { 20 | "build": "webpack --mode=production --progress", 21 | "build:debug": "webpack --mode=development --progress", 22 | "watch": "webpack --mode=development --progress --watch", 23 | "serve": "webpack serve --mode=development --progress" 24 | }, 25 | "dependencies": { 26 | "es6-promise": "^4.2.5" 27 | }, 28 | "devDependencies": { 29 | "@types/node": "^10.12.18", 30 | "@types/express-serve-static-core": "4.17.37", 31 | "@typescript-eslint/eslint-plugin": "^1.4.2", 32 | "@typescript-eslint/parser": "^1.4.2", 33 | "@webpack-cli/serve": "^1.7.0", 34 | "browser-sync": "^2.26.10", 35 | "eslint": "^5.15.0", 36 | "exports-loader": "^0.7.0", 37 | "file-loader": "^3.0.1", 38 | "source-map-loader": "^0.2.4", 39 | "ssri": ">=8.0.1", 40 | "terser-webpack-plugin": "^1.2.3", 41 | "ts-loader": "^8.4.0", 42 | "typescript": "^5.2.2", 43 | "webpack": "^4.47.0", 44 | "webpack-cli": "^4.4.0", 45 | "webpack-dev-server": "^4.9.3", 46 | "ws": ">=7.4.6", 47 | "xmlhttprequest-ssl": ">=1.6.2", 48 | "loader-utils": ">=1.4.1", 49 | "socket.io-parser": ">=3.3.3", 50 | "minimist": ">=1.2.6" 51 | }, 52 | "author": "zheng qian ", 53 | "license": "Apache-2.0" 54 | } 55 | -------------------------------------------------------------------------------- /src/core/transmuxing-events.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Bilibili. All Rights Reserved. 3 | * 4 | * @author zheng qian 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | enum TransmuxingEvents { 20 | IO_ERROR = 'io_error', 21 | DEMUX_ERROR = 'demux_error', 22 | INIT_SEGMENT = 'init_segment', 23 | MEDIA_SEGMENT = 'media_segment', 24 | LOADING_COMPLETE = 'loading_complete', 25 | RECOVERED_EARLY_EOF = 'recovered_early_eof', 26 | MEDIA_INFO = 'media_info', 27 | METADATA_ARRIVED = 'metadata_arrived', 28 | SCRIPTDATA_ARRIVED = 'scriptdata_arrived', 29 | TIMED_ID3_METADATA_ARRIVED = 'timed_id3_metadata_arrived', 30 | PGS_SUBTITLE_ARRIVED = 'pgs_subtitle_arrived', 31 | SYNCHRONOUS_KLV_METADATA_ARRIVED = 'synchronous_klv_metadata_arrived', 32 | ASYNCHRONOUS_KLV_METADATA_ARRIVED = 'asynchronous_klv_metadata_arrived', 33 | SMPTE2038_METADATA_ARRIVED = 'smpte2038_metadata_arrived', 34 | SCTE35_METADATA_ARRIVED = 'scte35_metadata_arrived', 35 | PES_PRIVATE_DATA_DESCRIPTOR = 'pes_private_data_descriptor', 36 | PES_PRIVATE_DATA_ARRIVED = 'pes_private_data_arrived', 37 | STATISTICS_INFO = 'statistics_info', 38 | RECOMMEND_SEEKPOINT = 'recommend_seekpoint' 39 | }; 40 | 41 | export default TransmuxingEvents; 42 | -------------------------------------------------------------------------------- /src/utils/exception.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Bilibili. All Rights Reserved. 3 | * 4 | * @author zheng qian 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | export class RuntimeException { 20 | 21 | constructor(message) { 22 | this._message = message; 23 | } 24 | 25 | get name() { 26 | return 'RuntimeException'; 27 | } 28 | 29 | get message() { 30 | return this._message; 31 | } 32 | 33 | toString() { 34 | return this.name + ': ' + this.message; 35 | } 36 | 37 | } 38 | 39 | export class IllegalStateException extends RuntimeException { 40 | 41 | constructor(message) { 42 | super(message); 43 | } 44 | 45 | get name() { 46 | return 'IllegalStateException'; 47 | } 48 | 49 | } 50 | 51 | export class InvalidArgumentException extends RuntimeException { 52 | 53 | constructor(message) { 54 | super(message); 55 | } 56 | 57 | get name() { 58 | return 'InvalidArgumentException'; 59 | } 60 | 61 | } 62 | 63 | export class NotImplementedException extends RuntimeException { 64 | 65 | constructor(message) { 66 | super(message); 67 | } 68 | 69 | get name() { 70 | return 'NotImplementedException'; 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /d.ts/src/player/player-engine-dedicated-thread.d.ts: -------------------------------------------------------------------------------- 1 | import type PlayerEngine from './player-engine'; 2 | import MediaInfo from '../core/media-info'; 3 | declare class PlayerEngineDedicatedThread implements PlayerEngine { 4 | private readonly TAG; 5 | private _emitter; 6 | private _media_data_source; 7 | private _config; 8 | private _media_element?; 9 | private _worker; 10 | private _worker_destroying; 11 | private _seeking_handler?; 12 | private _loading_controller?; 13 | private _startup_stall_jumper?; 14 | private _live_latency_chaser?; 15 | private _live_latency_synchronizer?; 16 | private _pending_seek_time?; 17 | private _media_info?; 18 | private _statistics_info?; 19 | private e?; 20 | private _prev_ready_state; 21 | static isSupported(): boolean; 22 | constructor(mediaDataSource: any, config: any); 23 | destroy(): void; 24 | on(event: string, listener: (...args: any[]) => void): void; 25 | off(event: string, listener: (...args: any[]) => void): void; 26 | attachMediaElement(mediaElement: HTMLMediaElement): void; 27 | detachMediaElement(): void; 28 | load(): void; 29 | unload(): void; 30 | play(): Promise; 31 | pause(): void; 32 | seek(seconds: number): void; 33 | switchPrimaryAudio(): void; 34 | switchSecondaryAudio(): void; 35 | get mediaInfo(): MediaInfo; 36 | get statisticsInfo(): any; 37 | _onLoggingConfigChanged(config: any): void; 38 | private _onMSEUpdateEnd; 39 | private _onMSEBufferFull; 40 | private _onMediaLoadedMetadata; 41 | private _onRequestDirectSeek; 42 | private _onRequiredUnbufferedSeek; 43 | private _onRequestPauseTransmuxer; 44 | private _onRequestResumeTransmuxer; 45 | private _onMediaTimeUpdate; 46 | private _onMediaReadyStateChange; 47 | private _onWorkerMessage; 48 | private _fillStatisticsInfo; 49 | } 50 | export default PlayerEngineDedicatedThread; 51 | -------------------------------------------------------------------------------- /d.ts/src/player/player-engine-main-thread.d.ts: -------------------------------------------------------------------------------- 1 | import type PlayerEngine from './player-engine'; 2 | import MediaInfo from '../core/media-info'; 3 | declare class PlayerEngineMainThread implements PlayerEngine { 4 | private readonly TAG; 5 | private _emitter; 6 | private _media_data_source; 7 | private _config; 8 | private _media_element?; 9 | private _mse_controller?; 10 | private _transmuxer?; 11 | private _pending_seek_time?; 12 | private _seeking_handler?; 13 | private _loading_controller?; 14 | private _startup_stall_jumper?; 15 | private _live_latency_chaser?; 16 | private _live_latency_synchronizer?; 17 | private _mse_source_opened; 18 | private _has_pending_load; 19 | private _loaded_metadata_received; 20 | private _media_info?; 21 | private _statistics_info?; 22 | private e?; 23 | constructor(mediaDataSource: any, config: any); 24 | destroy(): void; 25 | on(event: string, listener: (...args: any[]) => void): void; 26 | off(event: string, listener: (...args: any[]) => void): void; 27 | attachMediaElement(mediaElement: HTMLMediaElement): void; 28 | detachMediaElement(): void; 29 | load(): void; 30 | unload(): void; 31 | play(): Promise; 32 | pause(): void; 33 | seek(seconds: number): void; 34 | switchPrimaryAudio(): void; 35 | switchSecondaryAudio(): void; 36 | get mediaInfo(): MediaInfo; 37 | get statisticsInfo(): any; 38 | private _onMSESourceOpen; 39 | private _onMSEUpdateEnd; 40 | private _onMSEBufferFull; 41 | private _onMSEError; 42 | private _onMSEStartStreaming; 43 | private _onMSEEndStreaming; 44 | private _onMediaLoadedMetadata; 45 | private _onRequestDirectSeek; 46 | private _onRequiredUnbufferedSeek; 47 | private _onRequestPauseTransmuxer; 48 | private _onRequestResumeTransmuxer; 49 | private _fillStatisticsInfo; 50 | } 51 | export default PlayerEngineMainThread; 52 | -------------------------------------------------------------------------------- /docs/livestream.md: -------------------------------------------------------------------------------- 1 | 2 | Livestream playback 3 | =================== 4 | You need to provide a livestream URL in `MediaDataSource` and indicates `isLive: true`. 5 | 6 | Sample MPEG2-TS over HTTP source: 7 | 8 | ```js 9 | { 10 | // MPEG2-TS over HTTP 11 | "type": "mpegts", 12 | "isLive": true, 13 | "url": "http://127.0.0.1:8080/live/livestream.ts" 14 | } 15 | ``` 16 | 17 | Sample HTTP FLV source: 18 | 19 | ```js 20 | { 21 | // HTTP FLV 22 | "type": "flv", 23 | "isLive": true, 24 | "url": "http://127.0.0.1:8080/live/livestream.flv" 25 | } 26 | ``` 27 | 28 | Or a WebSocket source: 29 | 30 | ```js 31 | { 32 | // MPEG2-TS/FLV over WebSocket 33 | "type": "mse", 34 | "isLive": true, 35 | "url": "ws://127.0.0.1:9090/live/livestream.flv" 36 | } 37 | ``` 38 | 39 | ## HTTP MPEG2-TS/FLV live stream 40 | 41 | ### CORS 42 | You must configure `Access-Control-Allow-Origin` header correctly on your stream server. 43 | 44 | See [cors.md](../docs/cors.md) for details. 45 | 46 | ### Compatibility 47 | Due to IO restrictions, mpegts.js can support HTTP MPEG2-TS/FLV live stream on `Chrome 43+`, `FireFox 42+`, `Edge 15.15048+` and `Safari 10.1+` for now. 48 | 49 | HTTP MPEG2-TS/FLV live stream relies on stream IO, which has been introduced in [fetch][] and [stream][] spec. Now `FetchStreamLoader` works well on most of the modern browsers: 50 | 51 | - Chrome: `FetchStreamLoader` works well on Chrome 43+ 52 | - FireFox: FireFox has `fetch` support but `stream` is missing, `moz-chunked-arraybuffer` xhr extension is used 53 | - Edge: `fetch + stream` is broken on old version of Microsoft Edge, see [Fetch API with ReadableStream has bug with data pumping][]. Got fixed in Creator Update (RS2). 54 | - Safari: `FetchStreamLoader` works well since Safari 10.1 (macOS 10.12.4) 55 | 56 | [fetch]: https://fetch.spec.whatwg.org/ 57 | [stream]: https://streams.spec.whatwg.org/ 58 | [Fetch API with ReadableStream has bug with data pumping]: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/8196907/ 59 | [Safari Technology Preview]: https://developer.apple.com/safari/technology-preview/ 60 | -------------------------------------------------------------------------------- /src/player/player-engine-worker-cmd-def.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 zheng qian. All Rights Reserved. 3 | * 4 | * @author zheng qian 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | export type WorkerCommandOp = 20 | | 'logging_config' 21 | | 'init' 22 | | 'destroy' 23 | | 'initialize_mse' 24 | | 'shutdown_mse' 25 | | 'load' 26 | | 'unload' 27 | | 'unbuffered_seek' 28 | | 'timeupdate' 29 | | 'readystatechange' 30 | | 'pause_transmuxer' 31 | | 'resume_transmuxer' 32 | | 'switch_audio'; 33 | 34 | export type WorkerCommandPacket = { 35 | cmd: WorkerCommandOp, 36 | }; 37 | 38 | export type WorkerCommandPacketInit = WorkerCommandPacket & { 39 | cmd: 'init', 40 | media_data_source: any, 41 | config: any, 42 | }; 43 | 44 | export type WorkerCommandPacketLoggingConfig = WorkerCommandPacket & { 45 | cmd: 'logging_config', 46 | logging_config: any, 47 | }; 48 | 49 | export type WorkerCommandPacketUnbufferedSeek = WorkerCommandPacket & { 50 | cmd: 'unbuffered_seek', 51 | milliseconds: number, 52 | }; 53 | 54 | export type WorkerCommandPacketTimeUpdate = WorkerCommandPacket & { 55 | cmd: 'timeupdate', 56 | current_time: number, 57 | }; 58 | 59 | export type WorkerCommandPacketReadyStateChange = WorkerCommandPacket & { 60 | cmd: 'readystatechange', 61 | ready_state: number, 62 | }; 63 | 64 | export type WorkerCommandPacketSwitchAudio = WorkerCommandPacket & { 65 | cmd: 'switch_audio', 66 | audio_track: 'primary' | 'secondary', 67 | }; 68 | -------------------------------------------------------------------------------- /docs/multipart.md: -------------------------------------------------------------------------------- 1 | 2 | Multipart playback 3 | ================== 4 | When you create FlvPlayer instance, the `MediaDataSource` structure is passing through the constructor. 5 | 6 | You need to provide a playlist for `MediaDataSource` in following format: 7 | 8 | ```js 9 | { 10 | // Required 11 | "type": "flv", // Only flv type supports multipart playback 12 | 13 | // Optional 14 | "duration": 12345678, // total duration, in milliseconds 15 | "cors": true, 16 | "withCredentials": false, 17 | 18 | // Optional 19 | // true by default, do not indicate unless you have to deal with audio-only or video-only stream 20 | "hasAudio": true, 21 | "hasVideo": true, 22 | 23 | // Required 24 | "segments": [ 25 | { 26 | "duration": 1234, // in milliseconds 27 | "filesize": 5678, // in bytes 28 | "url": "http://cdn.flvplayback.com/segments-1.flv" 29 | }, 30 | { 31 | "duration": 2345, 32 | "filesize": 6789, 33 | "url": "http://cdn.flvplayback.com/segments-2.flv" 34 | }, 35 | { 36 | "duration": 4567, 37 | "filesize": 7890, 38 | "url": "http://cdn.flvplayback.com/segments-3.flv" 39 | } 40 | // more segments... 41 | ] 42 | } 43 | ``` 44 | 45 | You must provide **accurate** duration for each segment. 46 | 47 | ## Sample input 48 | ```json 49 | { 50 | "type": "flv", 51 | "duration": 1373161, 52 | "segments": [ 53 | { 54 | "duration": 333438, 55 | "filesize": 60369190, 56 | "url": "http://127.0.0.1/flv/7182741-1.flv" 57 | },{ 58 | "duration": 390828, 59 | "filesize": 75726439, 60 | "url": "http://127.0.0.1/flv/7182741-2.flv" 61 | },{ 62 | "duration": 434453, 63 | "filesize": 103453988, 64 | "url": "http://127.0.0.1/flv/7182741-3.flv" 65 | },{ 66 | "duration": 214442, 67 | "filesize": 44189200, 68 | "url": "http://127.0.0.1/flv/7182741-4.flv" 69 | } 70 | ] 71 | } 72 | ``` 73 | -------------------------------------------------------------------------------- /demo/demo.css: -------------------------------------------------------------------------------- 1 | .mainContainer { 2 | display: block; 3 | width: 100%; 4 | margin-left: auto; 5 | margin-right: auto; 6 | } 7 | @media screen and (min-width: 1152px) { 8 | .mainContainer { 9 | display: block; 10 | width: 1152px; 11 | margin-left: auto; 12 | margin-right: auto; 13 | } 14 | } 15 | 16 | .video-container { 17 | position: relative; 18 | margin-top: 8px; 19 | } 20 | 21 | .video-container:before { 22 | display: block; 23 | content: ""; 24 | width: 100%; 25 | padding-bottom: 56.25%; 26 | } 27 | 28 | .video-container > div { 29 | position: absolute; 30 | top: 0; 31 | left: 0; 32 | right: 0; 33 | bottom: 0; 34 | } 35 | 36 | .video-container video { 37 | width: 100%; 38 | height: 100%; 39 | } 40 | 41 | .urlInput { 42 | display: block; 43 | width: 100%; 44 | margin-left: auto; 45 | margin-right: auto; 46 | margin-top: 8px; 47 | margin-bottom: 8px; 48 | } 49 | 50 | .centeredVideo { 51 | display: block; 52 | width: 100%; 53 | height: 100%; 54 | margin-left: auto; 55 | margin-right: auto; 56 | margin-bottom: auto; 57 | } 58 | 59 | .controls { 60 | display: block; 61 | width: 100%; 62 | text-align: left; 63 | margin-left: auto; 64 | margin-right: auto; 65 | margin-top: 8px; 66 | margin-bottom: 10px; 67 | } 68 | 69 | .logcatBox { 70 | border-color: #CCCCCC; 71 | font-size: 11px; 72 | font-family: Menlo, Consolas, monospace; 73 | display: block; 74 | width: 100%; 75 | text-align: left; 76 | margin-left: auto; 77 | margin-right: auto; 78 | } 79 | 80 | .url-input , .options { 81 | font-size: 13px; 82 | } 83 | 84 | .url-input { 85 | display: flex; 86 | } 87 | 88 | .url-input label { 89 | flex: initial; 90 | } 91 | 92 | .url-input input { 93 | flex: auto; 94 | margin-left: 8px; 95 | } 96 | 97 | .url-input button { 98 | flex: initial; 99 | margin-left: 8px; 100 | } 101 | 102 | .options { 103 | margin-top: 5px; 104 | } 105 | 106 | .hidden { 107 | display: none; 108 | } 109 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Bilibili. All Rights Reserved. 3 | * 4 | * @author zheng qian 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | export const defaultConfig = { 20 | enableWorker: false, 21 | enableWorkerForMSE: false, 22 | enableStashBuffer: true, 23 | stashInitialSize: undefined, 24 | 25 | isLive: false, 26 | 27 | liveBufferLatencyChasing: false, 28 | liveBufferLatencyChasingOnPaused: false, 29 | liveBufferLatencyMaxLatency: 1.5, 30 | liveBufferLatencyMinRemain: 0.5, 31 | 32 | liveSync: false, 33 | liveSyncMaxLatency: 1.2, 34 | liveSyncTargetLatency: 0.8, 35 | liveSyncPlaybackRate: 1.2, 36 | liveSyncMinLatency: undefined, 37 | liveSyncMinPlaybackRate: 0.95, 38 | 39 | lazyLoad: true, 40 | lazyLoadMaxDuration: 3 * 60, 41 | lazyLoadRecoverDuration: 30, 42 | deferLoadAfterSourceOpen: true, 43 | 44 | // autoCleanupSourceBuffer: default as false, leave unspecified 45 | autoCleanupMaxBackwardDuration: 3 * 60, 46 | autoCleanupMinBackwardDuration: 2 * 60, 47 | 48 | statisticsInfoReportInterval: 600, 49 | 50 | fixAudioTimestampGap: true, 51 | 52 | accurateSeek: false, 53 | seekType: 'range', // [range, param, custom] 54 | seekParamStart: 'bstart', 55 | seekParamEnd: 'bend', 56 | rangeLoadZeroStart: false, 57 | customSeekHandler: undefined, 58 | reuseRedirectedURL: false, 59 | // referrerPolicy: leave as unspecified 60 | 61 | headers: undefined, 62 | customLoader: undefined 63 | }; 64 | 65 | export function createDefaultConfig() { 66 | return Object.assign({}, defaultConfig); 67 | } -------------------------------------------------------------------------------- /d.ts/src/demux/base-demuxer.d.ts: -------------------------------------------------------------------------------- 1 | import MediaInfo from '../core/media-info'; 2 | import { PESPrivateData, PESPrivateDataDescriptor } from './pes-private-data'; 3 | import { SMPTE2038Data } from './smpte2038'; 4 | import { SCTE35Data } from './scte35'; 5 | import { KLVData } from './klv'; 6 | import { PGSData } from './pgs-data'; 7 | type OnErrorCallback = (type: string, info: string) => void; 8 | type OnMediaInfoCallback = (mediaInfo: MediaInfo) => void; 9 | type OnMetaDataArrivedCallback = (metadata: any) => void; 10 | type OnTrackMetadataCallback = (type: string, metadata: any) => void; 11 | type OnDataAvailableCallback = (audioTrack: any, videoTrack: any, force?: boolean) => void; 12 | type OnTimedID3MetadataCallback = (timed_id3_data: PESPrivateData) => void; 13 | type onPGSSubitleDataCallback = (pgs_data: PGSData) => void; 14 | type OnSynchronousKLVMetadataCallback = (synchronous_klv_data: KLVData) => void; 15 | type OnAsynchronousKLVMetadataCallback = (asynchronous_klv_data: PESPrivateData) => void; 16 | type OnSMPTE2038MetadataCallback = (smpte2038_data: SMPTE2038Data) => void; 17 | type OnSCTE35MetadataCallback = (scte35_data: SCTE35Data) => void; 18 | type OnPESPrivateDataCallback = (private_data: PESPrivateData) => void; 19 | type OnPESPrivateDataDescriptorCallback = (private_data_descriptor: PESPrivateDataDescriptor) => void; 20 | export default abstract class BaseDemuxer { 21 | onError: OnErrorCallback; 22 | onMediaInfo: OnMediaInfoCallback; 23 | onMetaDataArrived: OnMetaDataArrivedCallback; 24 | onTrackMetadata: OnTrackMetadataCallback; 25 | onDataAvailable: OnDataAvailableCallback; 26 | onTimedID3Metadata: OnTimedID3MetadataCallback; 27 | onPGSSubtitleData: onPGSSubitleDataCallback; 28 | onSynchronousKLVMetadata: OnSynchronousKLVMetadataCallback; 29 | onAsynchronousKLVMetadata: OnAsynchronousKLVMetadataCallback; 30 | onSMPTE2038Metadata: OnSMPTE2038MetadataCallback; 31 | onSCTE35Metadata: OnSCTE35MetadataCallback; 32 | onPESPrivateData: OnPESPrivateDataCallback; 33 | onPESPrivateDataDescriptor: OnPESPrivateDataDescriptorCallback; 34 | constructor(); 35 | destroy(): void; 36 | abstract parseChunks(chunk: ArrayBuffer, byteStart: number): number; 37 | } 38 | export {}; 39 | -------------------------------------------------------------------------------- /d.ts/src/demux/ac3.d.ts: -------------------------------------------------------------------------------- 1 | export declare class AC3Frame { 2 | sampling_frequency: number; 3 | sampling_rate_code: number; 4 | bit_stream_identification: number; 5 | bit_stream_mode: number; 6 | low_frequency_effects_channel_on: number; 7 | frame_size_code: number; 8 | channel_count: number; 9 | channel_mode: number; 10 | data: Uint8Array; 11 | } 12 | export declare class AC3Parser { 13 | private readonly TAG; 14 | private data_; 15 | private current_syncword_offset_; 16 | private eof_flag_; 17 | private has_last_incomplete_data; 18 | constructor(data: Uint8Array); 19 | private findNextSyncwordOffset; 20 | readNextAC3Frame(): AC3Frame | null; 21 | hasIncompleteData(): boolean; 22 | getIncompleteData(): Uint8Array; 23 | } 24 | export declare class AC3Config { 25 | config: Array; 26 | sampling_rate: number; 27 | bit_stream_identification: number; 28 | bit_stream_mode: number; 29 | low_frequency_effects_channel_on: number; 30 | channel_count: number; 31 | channel_mode: number; 32 | codec_mimetype: string; 33 | original_codec_mimetype: string; 34 | constructor(frame: AC3Frame); 35 | } 36 | export declare class EAC3Frame { 37 | sampling_frequency: number; 38 | sampling_rate_code: number; 39 | bit_stream_identification: number; 40 | low_frequency_effects_channel_on: number; 41 | num_blks: number; 42 | frame_size: number; 43 | channel_count: number; 44 | channel_mode: number; 45 | data: Uint8Array; 46 | } 47 | export declare class EAC3Parser { 48 | private readonly TAG; 49 | private data_; 50 | private current_syncword_offset_; 51 | private eof_flag_; 52 | private has_last_incomplete_data; 53 | constructor(data: Uint8Array); 54 | private findNextSyncwordOffset; 55 | readNextEAC3Frame(): EAC3Frame | null; 56 | hasIncompleteData(): boolean; 57 | getIncompleteData(): Uint8Array; 58 | } 59 | export declare class EAC3Config { 60 | config: Array; 61 | sampling_rate: number; 62 | bit_stream_identification: number; 63 | num_blks: number; 64 | low_frequency_effects_channel_on: number; 65 | channel_count: number; 66 | channel_mode: number; 67 | codec_mimetype: string; 68 | original_codec_mimetype: string; 69 | constructor(frame: EAC3Frame); 70 | } 71 | -------------------------------------------------------------------------------- /src/utils/typedarray-equality.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 magicxqq. All Rights Reserved. 3 | * 4 | * @author magicxqq 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | function isAligned16(a: Uint8Array) : boolean { 20 | return a.byteOffset % 2 === 0 && a.byteLength % 2 === 0; 21 | } 22 | 23 | function isAligned32(a: Uint8Array) : boolean { 24 | return a.byteOffset % 4 === 0 && a.byteLength % 4 === 0; 25 | } 26 | 27 | function compareArray(a: Uint8Array | Uint16Array | Uint32Array, 28 | b: Uint8Array | Uint16Array | Uint32Array): boolean { 29 | for (let i = 0; i < a.length; i++) { 30 | if (a[i] !== b[i]) { 31 | return false; 32 | } 33 | } 34 | return true; 35 | } 36 | 37 | function equal8(a: Uint8Array, b: Uint8Array) : boolean { 38 | return compareArray(a, b); 39 | } 40 | 41 | function equal16(a: Uint8Array, b: Uint8Array) : boolean { 42 | let a16 = new Uint16Array(a.buffer, a.byteOffset, a.byteLength / 2); 43 | let b16 = new Uint16Array(b.buffer, b.byteOffset, b.byteLength / 2); 44 | return compareArray(a16, b16); 45 | } 46 | 47 | function equal32(a: Uint8Array, b: Uint8Array) : boolean { 48 | let a32 = new Uint32Array(a.buffer, a.byteOffset, a.byteLength / 4); 49 | let b32 = new Uint32Array(b.buffer, b.byteOffset, b.byteLength / 4); 50 | return compareArray(a32, b32); 51 | } 52 | 53 | function buffersAreEqual(a: Uint8Array, b: Uint8Array) : boolean { 54 | if (a.byteLength !== b.byteLength) { 55 | return false; 56 | } 57 | 58 | if (isAligned32(a) && isAligned32(b)) { 59 | return equal32(a, b); 60 | } 61 | 62 | if (isAligned16(a) && isAligned16(b)) { 63 | return equal16(a, b); 64 | } 65 | 66 | return equal8(a, b); 67 | } 68 | 69 | export default buffersAreEqual; 70 | -------------------------------------------------------------------------------- /d.ts/src/demux/h265.d.ts: -------------------------------------------------------------------------------- 1 | export declare enum H265NaluType { 2 | kSliceIDR_W_RADL = 19, 3 | kSliceIDR_N_LP = 20, 4 | kSliceCRA_NUT = 21, 5 | kSliceVPS = 32, 6 | kSliceSPS = 33, 7 | kSlicePPS = 34, 8 | kSliceAUD = 35 9 | } 10 | export declare class H265NaluPayload { 11 | type: H265NaluType; 12 | data: Uint8Array; 13 | } 14 | export declare class H265NaluHVC1 { 15 | type: H265NaluType; 16 | data: Uint8Array; 17 | constructor(nalu: H265NaluPayload); 18 | } 19 | export declare class H265AnnexBParser { 20 | private readonly TAG; 21 | private data_; 22 | private current_startcode_offset_; 23 | private eof_flag_; 24 | constructor(data: Uint8Array); 25 | private findNextStartCodeOffset; 26 | readNextNaluPayload(): H265NaluPayload | null; 27 | } 28 | export type HEVCDecoderConfigurationRecordType = { 29 | configurationVersion: 1; 30 | } & VPSHEVCDecoderConfigurationRecordType & SPSHEVCDecoderConfigurationRecordType & PPSHEVCDecoderConfigurationRecordType; 31 | export type VPSHEVCDecoderConfigurationRecordType = { 32 | num_temporal_layers: number; 33 | temporal_id_nested: boolean; 34 | }; 35 | export type SPSHEVCDecoderConfigurationRecordType = { 36 | general_profile_space: number; 37 | general_tier_flag: number; 38 | general_level_idc: number; 39 | general_profile_idc: number; 40 | general_profile_compatibility_flags_1: number; 41 | general_profile_compatibility_flags_2: number; 42 | general_profile_compatibility_flags_3: number; 43 | general_profile_compatibility_flags_4: number; 44 | general_constraint_indicator_flags_1: number; 45 | general_constraint_indicator_flags_2: number; 46 | general_constraint_indicator_flags_3: number; 47 | general_constraint_indicator_flags_4: number; 48 | general_constraint_indicator_flags_5: number; 49 | general_constraint_indicator_flags_6: number; 50 | constant_frame_rate: number; 51 | min_spatial_segmentation_idc: number; 52 | chroma_format_idc: number; 53 | bit_depth_luma_minus8: number; 54 | bit_depth_chroma_minus8: number; 55 | }; 56 | export type PPSHEVCDecoderConfigurationRecordType = { 57 | parallelismType: number; 58 | }; 59 | export declare class HEVCDecoderConfigurationRecord { 60 | private data; 61 | constructor(vps: Uint8Array, sps: Uint8Array, pps: Uint8Array, detail: HEVCDecoderConfigurationRecordType); 62 | getData(): Uint8Array; 63 | } 64 | -------------------------------------------------------------------------------- /src/utils/polyfill.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Bilibili. All Rights Reserved. 3 | * 4 | * @author zheng qian 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | class Polyfill { 20 | 21 | static install() { 22 | // ES6 Object.setPrototypeOf 23 | Object.setPrototypeOf = Object.setPrototypeOf || function (obj, proto) { 24 | obj.__proto__ = proto; 25 | return obj; 26 | }; 27 | 28 | // ES6 Object.assign 29 | Object.assign = Object.assign || function (target) { 30 | if (target === undefined || target === null) { 31 | throw new TypeError('Cannot convert undefined or null to object'); 32 | } 33 | 34 | let output = Object(target); 35 | for (let i = 1; i < arguments.length; i++) { 36 | let source = arguments[i]; 37 | if (source !== undefined && source !== null) { 38 | for (let key in source) { 39 | if (source.hasOwnProperty(key)) { 40 | output[key] = source[key]; 41 | } 42 | } 43 | } 44 | } 45 | return output; 46 | }; 47 | 48 | // String.prototype.startsWith() 49 | if (!String.prototype.startsWith) { 50 | Object.defineProperty(String.prototype, 'startsWith', { 51 | value: function (search, rawPos) { 52 | let pos = rawPos > 0 ? rawPos | 0 : 0; 53 | return this.substring(pos, pos + search.length) === search; 54 | } 55 | }); 56 | } 57 | 58 | // ES6 Promise (missing support in IE11) 59 | if (typeof self.Promise !== 'function') { 60 | require('es6-promise').polyfill(); 61 | } 62 | } 63 | 64 | } 65 | 66 | Polyfill.install(); 67 | 68 | export default Polyfill; -------------------------------------------------------------------------------- /d.ts/src/demux/pat-pmt-pes.d.ts: -------------------------------------------------------------------------------- 1 | interface ProgramToPMTPIDMap { 2 | [program: number]: number; 3 | } 4 | export declare class PAT { 5 | version_number: number; 6 | network_pid: number; 7 | program_pmt_pid: ProgramToPMTPIDMap; 8 | } 9 | export declare enum StreamType { 10 | kMPEG1Audio = 3, 11 | kMPEG2Audio = 4, 12 | kPESPrivateData = 6, 13 | kADTSAAC = 15, 14 | kLOASAAC = 17, 15 | kAC3 = 129, 16 | kEAC3 = 135, 17 | kMetadata = 21, 18 | kSCTE35 = 134, 19 | kPGS = 144, 20 | kH264 = 27, 21 | kH265 = 36 22 | } 23 | interface PIDToStreamTypeMap { 24 | [pid: number]: StreamType; 25 | } 26 | export declare class PMT { 27 | program_number: number; 28 | version_number: number; 29 | pcr_pid: number; 30 | pid_stream_type: PIDToStreamTypeMap; 31 | common_pids: { 32 | h264: number | undefined; 33 | h265: number | undefined; 34 | av1: number | undefined; 35 | adts_aac: number | undefined; 36 | loas_aac: number | undefined; 37 | opus: number | undefined; 38 | ac3: number | undefined; 39 | eac3: number | undefined; 40 | mp3: number | undefined; 41 | }; 42 | pes_private_data_pids: { 43 | [pid: number]: boolean; 44 | }; 45 | timed_id3_pids: { 46 | [pid: number]: boolean; 47 | }; 48 | pgs_pids: { 49 | [pid: number]: boolean; 50 | }; 51 | pgs_langs: { 52 | [pid: number]: string; 53 | }; 54 | synchronous_klv_pids: { 55 | [pid: number]: boolean; 56 | }; 57 | asynchronous_klv_pids: { 58 | [pid: number]: boolean; 59 | }; 60 | scte_35_pids: { 61 | [pid: number]: boolean; 62 | }; 63 | smpte2038_pids: { 64 | [oid: number]: boolean; 65 | }; 66 | } 67 | export interface ProgramToPMTMap { 68 | [program: number]: PMT; 69 | } 70 | export declare class PESData { 71 | pid: number; 72 | data: Uint8Array; 73 | stream_type: StreamType; 74 | file_position: number; 75 | random_access_indicator: number; 76 | } 77 | export declare class SectionData { 78 | pid: number; 79 | data: Uint8Array; 80 | file_position: number; 81 | random_access_indicator: number; 82 | } 83 | export declare class SliceQueue { 84 | slices: Uint8Array[]; 85 | total_length: number; 86 | expected_length: number; 87 | file_position: number; 88 | random_access_indicator: 0; 89 | } 90 | export interface PIDToSliceQueues { 91 | [pid: number]: SliceQueue; 92 | } 93 | export {}; 94 | -------------------------------------------------------------------------------- /src/player/live-latency-chaser.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 zheng qian. All Rights Reserved. 3 | * 4 | * @author zheng qian 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | // Live buffer latency chaser by directly adjusting HTMLMediaElement.currentTime (not recommended) 20 | class LiveLatencyChaser { 21 | 22 | private _config: any = null; 23 | private _media_element: HTMLMediaElement = null; 24 | private _on_direct_seek: (target: number) => void = null; 25 | 26 | public constructor(config: any, media_element: HTMLMediaElement, on_direct_seek: (target: number) => void) { 27 | this._config = config; 28 | this._media_element = media_element; 29 | this._on_direct_seek = on_direct_seek; 30 | } 31 | 32 | public destroy(): void { 33 | this._on_direct_seek = null; 34 | this._media_element = null; 35 | this._config = null; 36 | } 37 | 38 | public notifyBufferedRangeUpdate(): void { 39 | this._chaseLiveLatency(); 40 | } 41 | 42 | private _chaseLiveLatency(): void { 43 | const buffered: TimeRanges = this._media_element.buffered; 44 | const current_time: number = this._media_element.currentTime; 45 | 46 | const paused = this._media_element.paused; 47 | 48 | if (!this._config.isLive || 49 | !this._config.liveBufferLatencyChasing || 50 | buffered.length == 0 || 51 | (!this._config.liveBufferLatencyChasingOnPaused && paused)) { 52 | return; 53 | } 54 | 55 | const buffered_end = buffered.end(buffered.length - 1); 56 | if (buffered_end > this._config.liveBufferLatencyMaxLatency) { 57 | if (buffered_end - current_time > this._config.liveBufferLatencyMaxLatency) { 58 | let target_time = buffered_end - this._config.liveBufferLatencyMinRemain; 59 | this._on_direct_seek(target_time); 60 | } 61 | } 62 | } 63 | 64 | } 65 | 66 | export default LiveLatencyChaser; 67 | -------------------------------------------------------------------------------- /d.ts/src/player/player-engine-worker-msg-def.d.ts: -------------------------------------------------------------------------------- 1 | import MSEEvents from '../core/mse-events'; 2 | import PlayerEvents from './player-events'; 3 | import TransmuxingEvents from '../core/transmuxing-events'; 4 | export type WorkerMessageType = 'destroyed' | 'mse_init' | 'mse_event' | 'player_event' | 'transmuxing_event' | 'buffered_position_changed' | 'logcat_callback'; 5 | export type WorkerMessagePacket = { 6 | msg: WorkerMessageType; 7 | }; 8 | export type WorkerMessagePacketMSEInit = WorkerMessagePacket & { 9 | msg: 'mse_init'; 10 | handle: any; 11 | }; 12 | export type WorkerMessagePacketMSEEvent = WorkerMessagePacket & { 13 | msg: 'mse_event'; 14 | event: MSEEvents; 15 | }; 16 | export type WorkerMessagePacketPlayerEvent = WorkerMessagePacket & { 17 | msg: 'player_event'; 18 | event: PlayerEvents; 19 | }; 20 | export type WorkerMessagePacketPlayerEventError = WorkerMessagePacketPlayerEvent & { 21 | msg: 'player_event'; 22 | event: PlayerEvents.ERROR; 23 | error_type: string; 24 | error_detail: string; 25 | info: any; 26 | }; 27 | export type WorkerMessagePacketPlayerEventExtraData = WorkerMessagePacketPlayerEvent & { 28 | msg: 'player_event'; 29 | event: PlayerEvents.METADATA_ARRIVED | PlayerEvents.SCRIPTDATA_ARRIVED | PlayerEvents.TIMED_ID3_METADATA_ARRIVED | PlayerEvents.PGS_SUBTITLE_ARRIVED | PlayerEvents.SYNCHRONOUS_KLV_METADATA_ARRIVED | PlayerEvents.ASYNCHRONOUS_KLV_METADATA_ARRIVED | PlayerEvents.SMPTE2038_METADATA_ARRIVED | PlayerEvents.SCTE35_METADATA_ARRIVED | PlayerEvents.PES_PRIVATE_DATA_DESCRIPTOR | PlayerEvents.PES_PRIVATE_DATA_ARRIVED; 30 | extraData: any; 31 | }; 32 | export type WorkerMessagePacketTransmuxingEvent = WorkerMessagePacket & { 33 | msg: 'transmuxing_event'; 34 | event: TransmuxingEvents; 35 | }; 36 | export type WorkerMessagePacketTransmuxingEventInfo = WorkerMessagePacketTransmuxingEvent & { 37 | msg: 'transmuxing_event'; 38 | event: TransmuxingEvents.MEDIA_INFO | TransmuxingEvents.STATISTICS_INFO; 39 | info: any; 40 | }; 41 | export type WorkerMessagePacketTransmuxingEventRecommendSeekpoint = WorkerMessagePacketTransmuxingEvent & { 42 | msg: 'transmuxing_event'; 43 | event: TransmuxingEvents.RECOMMEND_SEEKPOINT; 44 | milliseconds: number; 45 | }; 46 | export type WorkerMessagePacketBufferedPositionChanged = WorkerMessagePacket & { 47 | msg: 'buffered_position_changed'; 48 | buffered_position_milliseconds: number; 49 | }; 50 | export type WorkerMessagePacketLogcatCallback = WorkerMessagePacket & { 51 | msg: 'logcat_callback'; 52 | type: string; 53 | logcat: string; 54 | }; 55 | -------------------------------------------------------------------------------- /docs/cors.md: -------------------------------------------------------------------------------- 1 | 2 | CORS Configuration 3 | ================== 4 | Anytime you want to play an MPEG2-TS/FLV stream from another `Origin`, the server must response with a CORS header: 5 | 6 | ``` 7 | Access-Control-Allow-Origin: | * 8 | ``` 9 | 10 | For example, if an html on your site `http://flvplayback.com` want's to play an MPEG2-TS/FLV from another `Origin` like `http://cdn.flvplayback.com`, the video server must response with the following CORS header: 11 | 12 | ``` 13 | Access-Control-Allow-Origin: http://flvplayback.com 14 | ``` 15 | 16 | Or a wildcard value `*` to allow any request origin: 17 | 18 | ``` 19 | Access-Control-Allow-Origin: * 20 | ``` 21 | 22 | ## Static MPEG2-TS/FLV file playback 23 | For static MPEG2-TS/FLV file playback, we recommend you to add: 24 | 25 | ``` 26 | Access-Control-Expose-Headers: Content-Length 27 | ``` 28 | 29 | Or you should provide accurate filesize in **MediaDataSource** object. 30 | 31 | ## CORS with 301/302 redirect 32 | If your video server response with a 3xx redirection, the redirection's response headers **must** contains `Access-Control-Allow-Origin`; 33 | 34 | Obviously the redirect target server should also response with CORS headers, but pay attention that the browser will send `Origin: null` in redirected request according to current CORS policy. 35 | 36 | It means that your actual edge server should response with: 37 | 38 | ``` 39 | Access-Control-Allow-Origin: null | * 40 | ``` 41 | 42 | Or you can determine by request header `Origin` dynamically. 43 | 44 | ## Preflight OPTIONS for Range seek 45 | When use Range seek for cross-origin MPEG2-TS/FLV file, `Range` header added by mpegts.js will cause a [Preflight OPTIONS][] request by the browser. 46 | 47 | The browser will send an `OPTIONS` request before actual `GET` request, with following additional headers according to CORS policy: 48 | 49 | ``` 50 | Access-Control-Request-Headers: range 51 | Access-Control-Request-Method: GET 52 | ``` 53 | 54 | This means your video server must response to OPTIONS request with following additional CORS headers: 55 | 56 | ``` 57 | Access-Control-Allow-Origin: | * 58 | Access-Control-Allow-Methods: GET, OPTIONS 59 | Access-Control-Allow-Headers: range 60 | ``` 61 | 62 | [Preflight OPTIONS]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Preflighted_requests 63 | 64 | ## Reference 65 | We strongly advise you to read [HTTP access control (CORS)][] and [CORS spec][] document carefully. 66 | 67 | [HTTP access control (CORS)]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS 68 | [CORS spec]: https://www.w3.org/TR/cors/ 69 | -------------------------------------------------------------------------------- /src/io/param-seek-handler.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Bilibili. All Rights Reserved. 3 | * 4 | * @author zheng qian 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | class ParamSeekHandler { 20 | 21 | constructor(paramStart, paramEnd) { 22 | this._startName = paramStart; 23 | this._endName = paramEnd; 24 | } 25 | 26 | getConfig(baseUrl, range) { 27 | let url = baseUrl; 28 | 29 | if (range.from !== 0 || range.to !== -1) { 30 | let needAnd = true; 31 | if (url.indexOf('?') === -1) { 32 | url += '?'; 33 | needAnd = false; 34 | } 35 | 36 | if (needAnd) { 37 | url += '&'; 38 | } 39 | 40 | url += `${this._startName}=${range.from.toString()}`; 41 | 42 | if (range.to !== -1) { 43 | url += `&${this._endName}=${range.to.toString()}`; 44 | } 45 | } 46 | 47 | return { 48 | url: url, 49 | headers: {} 50 | }; 51 | } 52 | 53 | removeURLParameters(seekedURL) { 54 | let baseURL = seekedURL.split('?')[0]; 55 | let params = undefined; 56 | 57 | let queryIndex = seekedURL.indexOf('?'); 58 | if (queryIndex !== -1) { 59 | params = seekedURL.substring(queryIndex + 1); 60 | } 61 | 62 | let resultParams = ''; 63 | 64 | if (params != undefined && params.length > 0) { 65 | let pairs = params.split('&'); 66 | 67 | for (let i = 0; i < pairs.length; i++) { 68 | let pair = pairs[i].split('='); 69 | let requireAnd = (i > 0); 70 | 71 | if (pair[0] !== this._startName && pair[0] !== this._endName) { 72 | if (requireAnd) { 73 | resultParams += '&'; 74 | } 75 | resultParams += pairs[i]; 76 | } 77 | } 78 | } 79 | 80 | return (resultParams.length === 0) ? baseURL : baseURL + '?' + resultParams; 81 | } 82 | 83 | } 84 | 85 | export default ParamSeekHandler; -------------------------------------------------------------------------------- /d.ts/src/demux/av1-parser.d.ts: -------------------------------------------------------------------------------- 1 | import ExpGolomb from './exp-golomb.js'; 2 | type OperatingPoint = { 3 | operating_point_idc: number; 4 | level: number; 5 | tier: number; 6 | decoder_model_present_for_this_op?: boolean; 7 | }; 8 | type SequenceHeaderDetails = { 9 | frame_id_numbers_present_flag: boolean; 10 | additional_frame_id_length_minus_1?: number; 11 | delta_frame_id_length_minus_2?: number; 12 | reduced_still_picture_header: boolean; 13 | decoder_model_info_present_flag: boolean; 14 | operating_points_cnt_minus_1?: number; 15 | operating_points: OperatingPoint[]; 16 | buffer_removal_time_length_minus_1: number; 17 | equal_picture_interval: boolean; 18 | seq_force_screen_content_tools: number; 19 | seq_force_integer_mv: number; 20 | enable_order_hint: boolean; 21 | order_hint_bits: number; 22 | enable_superres: boolean; 23 | frame_width_bit: number; 24 | frame_height_bit: number; 25 | max_frame_width: number; 26 | max_frame_height: number; 27 | }; 28 | type FrameResolutions = { 29 | UpscaledWidth: number; 30 | FrameWidth: number; 31 | FrameHeight: number; 32 | RenderWidth: number; 33 | RenderHeight: number; 34 | }; 35 | type AV1Metadata = { 36 | codec_mimetype: string; 37 | level: number; 38 | level_string: string; 39 | tier: number; 40 | profile_idc: number; 41 | profile_string: string; 42 | bit_depth: number; 43 | ref_frames: number; 44 | chroma_format: number; 45 | chroma_format_string: string; 46 | sequence_header: SequenceHeaderDetails; 47 | sequence_header_data: Uint8Array; 48 | keyframe?: boolean; 49 | frame_rate: { 50 | fixed: boolean; 51 | fps: number; 52 | fps_den: number; 53 | fps_num: number; 54 | }; 55 | sar_ratio?: { 56 | width: number; 57 | height: number; 58 | }; 59 | codec_size?: { 60 | width: number; 61 | height: number; 62 | }; 63 | present_size?: { 64 | width: number; 65 | height: number; 66 | }; 67 | }; 68 | declare class AV1OBUParser { 69 | static parseOBUs(uint8array: Uint8Array, meta?: AV1Metadata | null): AV1Metadata; 70 | static parseSeuqneceHeader(uint8array: Uint8Array): Omit; 71 | static parseOBUFrameHeader(uint8array: Uint8Array, temporal_id: number, spatial_id: number, meta: AV1Metadata): AV1Metadata; 72 | static frameSizeAndRenderSize(gb: ExpGolomb, frame_size_override_flag: boolean, sequence_header: SequenceHeaderDetails): FrameResolutions; 73 | static getLevelString(level: number, tier: number): string; 74 | static getChromaFormat(mono_chrome: boolean, subsampling_x: number, subsampling_y: number): number; 75 | static getChromaFormatString(mono_chrome: boolean, subsampling_x: number, subsampling_y: number): string; 76 | } 77 | export default AV1OBUParser; 78 | -------------------------------------------------------------------------------- /src/demux/base-demuxer.ts: -------------------------------------------------------------------------------- 1 | import MediaInfo from '../core/media-info'; 2 | import { PESPrivateData, PESPrivateDataDescriptor } from './pes-private-data'; 3 | import { SMPTE2038Data } from './smpte2038'; 4 | import { SCTE35Data } from './scte35'; 5 | import { KLVData } from './klv'; 6 | import { PGSData } from './pgs-data'; 7 | 8 | type OnErrorCallback = (type: string, info: string) => void; 9 | type OnMediaInfoCallback = (mediaInfo: MediaInfo) => void; 10 | type OnMetaDataArrivedCallback = (metadata: any) => void; 11 | type OnTrackMetadataCallback = (type: string, metadata: any) => void; 12 | type OnDataAvailableCallback = (audioTrack: any, videoTrack: any, force?: boolean) => void; 13 | type OnTimedID3MetadataCallback = (timed_id3_data: PESPrivateData) => void; 14 | type onPGSSubitleDataCallback = (pgs_data: PGSData) => void; 15 | type OnSynchronousKLVMetadataCallback = (synchronous_klv_data: KLVData) => void; 16 | type OnAsynchronousKLVMetadataCallback = (asynchronous_klv_data: PESPrivateData) => void; 17 | type OnSMPTE2038MetadataCallback = (smpte2038_data: SMPTE2038Data) => void; 18 | type OnSCTE35MetadataCallback = (scte35_data: SCTE35Data) => void; 19 | type OnPESPrivateDataCallback = (private_data: PESPrivateData) => void; 20 | type OnPESPrivateDataDescriptorCallback = (private_data_descriptor: PESPrivateDataDescriptor) => void; 21 | 22 | export default abstract class BaseDemuxer { 23 | 24 | public onError: OnErrorCallback; 25 | public onMediaInfo: OnMediaInfoCallback; 26 | public onMetaDataArrived: OnMetaDataArrivedCallback; 27 | public onTrackMetadata: OnTrackMetadataCallback; 28 | public onDataAvailable: OnDataAvailableCallback; 29 | public onTimedID3Metadata: OnTimedID3MetadataCallback; 30 | public onPGSSubtitleData: onPGSSubitleDataCallback; 31 | public onSynchronousKLVMetadata: OnSynchronousKLVMetadataCallback 32 | public onAsynchronousKLVMetadata: OnAsynchronousKLVMetadataCallback; 33 | public onSMPTE2038Metadata: OnSMPTE2038MetadataCallback; 34 | public onSCTE35Metadata: OnSCTE35MetadataCallback; 35 | public onPESPrivateData: OnPESPrivateDataCallback; 36 | public onPESPrivateDataDescriptor: OnPESPrivateDataDescriptorCallback; 37 | 38 | public constructor() {} 39 | 40 | public destroy(): void { 41 | this.onError = null; 42 | this.onMediaInfo = null; 43 | this.onMetaDataArrived = null; 44 | this.onTrackMetadata = null; 45 | this.onDataAvailable = null; 46 | this.onTimedID3Metadata = null; 47 | this.onPGSSubtitleData = null; 48 | this.onSynchronousKLVMetadata = null; 49 | this.onAsynchronousKLVMetadata = null; 50 | this.onSMPTE2038Metadata = null; 51 | this.onSCTE35Metadata = null; 52 | this.onPESPrivateData = null; 53 | this.onPESPrivateDataDescriptor = null; 54 | } 55 | 56 | abstract parseChunks(chunk: ArrayBuffer, byteStart: number): number; 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/demux/smpte2038.ts: -------------------------------------------------------------------------------- 1 | import ExpGolomb from "./exp-golomb"; 2 | 3 | export class SMPTE2038Data { 4 | pid: number; 5 | stream_id: number; 6 | pts?: number; 7 | dts?: number; 8 | nearest_pts?: number; 9 | ancillaries: AncillaryData[]; 10 | data: Uint8Array; 11 | len: number; 12 | } 13 | 14 | type AncillaryData = { 15 | yc_indicator: boolean; 16 | line_number: number; 17 | horizontal_offset: number; 18 | did: number; 19 | sdid: number; 20 | user_data: Uint8Array; 21 | description: string; 22 | information: any; 23 | } 24 | 25 | 26 | export const smpte2038parse = (data: Uint8Array) => { 27 | let gb = new ExpGolomb(data); 28 | let readBits = 0; 29 | 30 | let ancillaries: AncillaryData[] = []; 31 | while (true) { 32 | let zero = gb.readBits(6); readBits += 6; 33 | if (zero !== 0) { break; } 34 | let YC_indicator = gb.readBool(); readBits += 1; 35 | let line_number = gb.readBits(11); readBits += 11; 36 | let horizontal_offset = gb.readBits(12); readBits += 12; 37 | let data_ID = gb.readBits(10) & 0xFF; readBits += 10; 38 | let data_SDID = gb.readBits(10) & 0xFF; readBits += 10; 39 | let data_count = gb.readBits(10) & 0xFF; readBits += 10; 40 | let user_data = new Uint8Array(data_count); 41 | for (let i = 0; i < data_count; i++) { 42 | let user_data_word = gb.readBits(10) & 0xFF; readBits += 10; 43 | user_data[i] = user_data_word; 44 | } 45 | let checksum_word = gb.readBits(10); readBits += 10; 46 | 47 | let description = 'User Defined'; 48 | let information: any = {}; 49 | if (data_ID === 0x41) { 50 | if (data_SDID === 0x07) { 51 | description = 'SCTE-104' 52 | } 53 | } else if (data_ID === 0x5F) { 54 | if (data_SDID === 0xDC) { 55 | description = 'ARIB STD-B37 (1SEG)'; 56 | } else if (data_SDID === 0xDD) { 57 | description = 'ARIB STD-B37 (ANALOG)'; 58 | } else if (data_SDID === 0xDE) { 59 | description = 'ARIB STD-B37 (SD)'; 60 | } else if (data_SDID === 0xDF) { 61 | description = 'ARIB STD-B37 (HD)'; 62 | } 63 | } else if (data_ID === 0x61) { 64 | if (data_SDID === 0x01) { 65 | description = 'EIA-708'; 66 | } else if (data_SDID === 0x02) { 67 | description = 'EIA-608'; 68 | } 69 | } 70 | 71 | ancillaries.push({ 72 | yc_indicator: YC_indicator, 73 | line_number, 74 | horizontal_offset, 75 | did: data_ID, 76 | sdid: data_SDID, 77 | user_data, 78 | description, 79 | information 80 | }); 81 | gb.readBits(8 - (readBits - Math.floor(readBits / 8)) % 8); 82 | readBits += (8 - (readBits - Math.floor(readBits / 8))) % 8; 83 | } 84 | 85 | gb.destroy(); 86 | gb = null; 87 | 88 | return ancillaries; 89 | } -------------------------------------------------------------------------------- /src/demux/pat-pmt-pes.ts: -------------------------------------------------------------------------------- 1 | interface ProgramToPMTPIDMap { 2 | [program: number]: number; 3 | } 4 | 5 | export class PAT { 6 | version_number: number; 7 | network_pid: number; 8 | // program_number -> pmt_pid 9 | program_pmt_pid: ProgramToPMTPIDMap = {}; 10 | } 11 | 12 | export enum StreamType { 13 | kMPEG1Audio = 0x03, 14 | kMPEG2Audio = 0x04, 15 | kPESPrivateData = 0x06, 16 | kADTSAAC = 0x0F, 17 | kLOASAAC = 0x11, 18 | kAC3 = 0x81, 19 | kEAC3 = 0x87, 20 | kMetadata = 0x15, 21 | kSCTE35 = 0x86, 22 | kPGS = 0x90, 23 | kH264 = 0x1b, 24 | kH265 = 0x24 25 | } 26 | 27 | interface PIDToStreamTypeMap { 28 | [pid: number]: StreamType; 29 | } 30 | 31 | export class PMT { 32 | program_number: number; 33 | version_number: number; 34 | pcr_pid: number; 35 | // pid -> stream_type 36 | pid_stream_type: PIDToStreamTypeMap = {}; 37 | 38 | common_pids: { 39 | h264: number | undefined, 40 | h265: number | undefined, 41 | av1: number | undefined, 42 | adts_aac: number | undefined, 43 | loas_aac: number | undefined, 44 | opus: number | undefined, 45 | ac3: number | undefined, 46 | eac3: number | undefined, 47 | mp3: number | undefined 48 | } = { 49 | h264: undefined, 50 | h265: undefined, 51 | av1: undefined, 52 | adts_aac: undefined, 53 | loas_aac: undefined, 54 | opus: undefined, 55 | ac3: undefined, 56 | eac3: undefined, 57 | mp3: undefined 58 | }; 59 | 60 | pes_private_data_pids: { 61 | [pid: number]: boolean 62 | } = {}; 63 | 64 | timed_id3_pids: { 65 | [pid: number]: boolean 66 | } = {}; 67 | 68 | pgs_pids: { 69 | [pid: number]: boolean; 70 | } = {}; 71 | pgs_langs: { 72 | [pid: number]: string; 73 | } = {}; 74 | 75 | synchronous_klv_pids: { 76 | [pid: number]: boolean 77 | } = {}; 78 | 79 | asynchronous_klv_pids: { 80 | [pid: number]: boolean 81 | } = {}; 82 | 83 | scte_35_pids: { 84 | [pid: number]: boolean 85 | } = {}; 86 | 87 | smpte2038_pids: { 88 | [oid: number]: boolean 89 | } = {}; 90 | } 91 | 92 | export interface ProgramToPMTMap { 93 | [program: number]: PMT; 94 | } 95 | 96 | export class PESData { 97 | pid: number; 98 | data: Uint8Array; 99 | stream_type: StreamType; 100 | file_position: number; 101 | random_access_indicator: number; 102 | } 103 | 104 | export class SectionData { 105 | pid: number; 106 | data: Uint8Array; 107 | file_position: number; 108 | random_access_indicator: number; 109 | } 110 | 111 | export class SliceQueue { 112 | slices: Uint8Array[] = []; 113 | total_length: number = 0; 114 | expected_length: number = 0; 115 | file_position: number = 0; 116 | random_access_indicator: 0; 117 | } 118 | 119 | export interface PIDToSliceQueues { 120 | [pid: number]: SliceQueue; 121 | } 122 | -------------------------------------------------------------------------------- /src/utils/utf8-conv.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Bilibili. All Rights Reserved. 3 | * 4 | * This file is derived from C++ project libWinTF8 (https://github.com/m13253/libWinTF8) 5 | * @author zheng qian 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | function checkContinuation(uint8array, start, checkLength) { 21 | let array = uint8array; 22 | if (start + checkLength < array.length) { 23 | while (checkLength--) { 24 | if ((array[++start] & 0xC0) !== 0x80) 25 | return false; 26 | } 27 | return true; 28 | } else { 29 | return false; 30 | } 31 | } 32 | 33 | function decodeUTF8(uint8array) { 34 | let out = []; 35 | let input = uint8array; 36 | let i = 0; 37 | let length = uint8array.length; 38 | 39 | while (i < length) { 40 | if (input[i] < 0x80) { 41 | out.push(String.fromCharCode(input[i])); 42 | ++i; 43 | continue; 44 | } else if (input[i] < 0xC0) { 45 | // fallthrough 46 | } else if (input[i] < 0xE0) { 47 | if (checkContinuation(input, i, 1)) { 48 | let ucs4 = (input[i] & 0x1F) << 6 | (input[i + 1] & 0x3F); 49 | if (ucs4 >= 0x80) { 50 | out.push(String.fromCharCode(ucs4 & 0xFFFF)); 51 | i += 2; 52 | continue; 53 | } 54 | } 55 | } else if (input[i] < 0xF0) { 56 | if (checkContinuation(input, i, 2)) { 57 | let ucs4 = (input[i] & 0xF) << 12 | (input[i + 1] & 0x3F) << 6 | input[i + 2] & 0x3F; 58 | if (ucs4 >= 0x800 && (ucs4 & 0xF800) !== 0xD800) { 59 | out.push(String.fromCharCode(ucs4 & 0xFFFF)); 60 | i += 3; 61 | continue; 62 | } 63 | } 64 | } else if (input[i] < 0xF8) { 65 | if (checkContinuation(input, i, 3)) { 66 | let ucs4 = (input[i] & 0x7) << 18 | (input[i + 1] & 0x3F) << 12 67 | | (input[i + 2] & 0x3F) << 6 | (input[i + 3] & 0x3F); 68 | if (ucs4 > 0x10000 && ucs4 < 0x110000) { 69 | ucs4 -= 0x10000; 70 | out.push(String.fromCharCode((ucs4 >>> 10) | 0xD800)); 71 | out.push(String.fromCharCode((ucs4 & 0x3FF) | 0xDC00)); 72 | i += 4; 73 | continue; 74 | } 75 | } 76 | } 77 | out.push(String.fromCharCode(0xFFFD)); 78 | ++i; 79 | } 80 | 81 | return out.join(''); 82 | } 83 | 84 | export default decodeUTF8; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Node.js 3 | ################# 4 | 5 | node_modules 6 | npm-debug.log 7 | .node-version 8 | 9 | ################# 10 | ## Grunt 11 | ################# 12 | 13 | .grunt 14 | _SpecRunner.html 15 | reports 16 | jsdoc 17 | # dist 18 | build/temp 19 | coverage/ 20 | 21 | ################# 22 | ## Eclipse 23 | ################# 24 | 25 | *.pydevproject 26 | .project 27 | .metadata 28 | bin/ 29 | tmp/ 30 | .idea/ 31 | *.tmp 32 | *.bak 33 | *.swp 34 | *~.nib 35 | local.properties 36 | .classpath 37 | .settings/ 38 | .loadpath 39 | 40 | # External tool builders 41 | .externalToolBuilders/ 42 | 43 | # Locally stored "Eclipse launch configurations" 44 | *.launch 45 | 46 | # CDT-specific 47 | .cproject 48 | 49 | # PDT-specific 50 | .buildpath 51 | 52 | 53 | ################# 54 | ## Visual Studio 55 | ################# 56 | 57 | ## Ignore Visual Studio temporary files, build results, and 58 | ## files generated by popular Visual Studio add-ons. 59 | 60 | # User-specific files 61 | *.suo 62 | *.user 63 | *.sln.docstates 64 | 65 | # Build results 66 | [Dd]ebug/ 67 | [Rr]elease/ 68 | *_i.c 69 | *_p.c 70 | *.ilk 71 | *.meta 72 | *.obj 73 | *.pch 74 | *.pdb 75 | *.pgc 76 | *.pgd 77 | *.rsp 78 | *.sbr 79 | *.tlb 80 | *.tli 81 | *.tlh 82 | *.tmp 83 | *.vspscc 84 | .builds 85 | *.dotCover 86 | 87 | ## TODO: If you have NuGet Package Restore enabled, uncomment this 88 | #packages/ 89 | 90 | # Visual C++ cache files 91 | ipch/ 92 | *.aps 93 | *.ncb 94 | *.opensdf 95 | *.sdf 96 | 97 | # Visual Studio profiler 98 | *.psess 99 | *.vsp 100 | 101 | # ReSharper is a .NET coding add-in 102 | _ReSharper* 103 | 104 | # Installshield output folder 105 | [Ee]xpress 106 | 107 | # DocProject is a documentation generator add-in 108 | DocProject/buildhelp/ 109 | DocProject/Help/*.HxT 110 | DocProject/Help/*.HxC 111 | DocProject/Help/*.hhc 112 | DocProject/Help/*.hhk 113 | DocProject/Help/*.hhp 114 | DocProject/Help/Html2 115 | DocProject/Help/html 116 | 117 | # Click-Once directory 118 | publish 119 | 120 | # Others 121 | [Bb]in 122 | [Oo]bj 123 | sql 124 | TestResults 125 | *.Cache 126 | ClientBin 127 | stylecop.* 128 | ~$* 129 | *.dbmdl 130 | Generated_Code #added for RIA/Silverlight projects 131 | 132 | # Backup & report files from converting an old project file to a newer 133 | # Visual Studio version. Backup files are not needed, because we have git ;-) 134 | _UpgradeReport_Files/ 135 | Backup*/ 136 | UpgradeLog*.XML 137 | 138 | 139 | 140 | ############ 141 | ## Windows 142 | ############ 143 | 144 | # Windows image file caches 145 | Thumbs.db 146 | 147 | # Folder config file 148 | Desktop.ini 149 | 150 | 151 | ############# 152 | ## Python 153 | ############# 154 | 155 | *.py[co] 156 | 157 | # Packages 158 | *.egg 159 | *.egg-info 160 | eggs 161 | parts 162 | bin 163 | var 164 | sdist 165 | develop-eggs 166 | .installed.cfg 167 | 168 | # Installer logs 169 | pip-log.txt 170 | 171 | # Unit test / coverage reports 172 | .coverage 173 | .tox 174 | 175 | #Translations 176 | *.mo 177 | 178 | #Mr Developer 179 | .mr.developer.cfg 180 | 181 | # Mac crap 182 | .DS_Store 183 | 184 | # Visual Studio Code 185 | .vscode/ 186 | browse.VC.db 187 | -------------------------------------------------------------------------------- /src/io/speed-sampler.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Bilibili. All Rights Reserved. 3 | * 4 | * @author zheng qian 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | // Utility class to calculate realtime network I/O speed 20 | class SpeedSampler { 21 | 22 | constructor() { 23 | // milliseconds 24 | this._firstCheckpoint = 0; 25 | this._lastCheckpoint = 0; 26 | this._intervalBytes = 0; 27 | this._totalBytes = 0; 28 | this._lastSecondBytes = 0; 29 | 30 | // compatibility detection 31 | if (self.performance && self.performance.now) { 32 | this._now = self.performance.now.bind(self.performance); 33 | } else { 34 | this._now = Date.now; 35 | } 36 | } 37 | 38 | reset() { 39 | this._firstCheckpoint = this._lastCheckpoint = 0; 40 | this._totalBytes = this._intervalBytes = 0; 41 | this._lastSecondBytes = 0; 42 | } 43 | 44 | addBytes(bytes) { 45 | if (this._firstCheckpoint === 0) { 46 | this._firstCheckpoint = this._now(); 47 | this._lastCheckpoint = this._firstCheckpoint; 48 | this._intervalBytes += bytes; 49 | this._totalBytes += bytes; 50 | } else if (this._now() - this._lastCheckpoint < 1000) { 51 | this._intervalBytes += bytes; 52 | this._totalBytes += bytes; 53 | } else { // duration >= 1000 54 | this._lastSecondBytes = this._intervalBytes; 55 | this._intervalBytes = bytes; 56 | this._totalBytes += bytes; 57 | this._lastCheckpoint = this._now(); 58 | } 59 | } 60 | 61 | get currentKBps() { 62 | this.addBytes(0); 63 | 64 | let durationSeconds = (this._now() - this._lastCheckpoint) / 1000; 65 | if (durationSeconds == 0) durationSeconds = 1; 66 | return (this._intervalBytes / durationSeconds) / 1024; 67 | } 68 | 69 | get lastSecondKBps() { 70 | this.addBytes(0); 71 | 72 | if (this._lastSecondBytes !== 0) { 73 | return this._lastSecondBytes / 1024; 74 | } else { // lastSecondBytes === 0 75 | if (this._now() - this._lastCheckpoint >= 500) { 76 | // if time interval since last checkpoint has exceeded 500ms 77 | // the speed is nearly accurate 78 | return this.currentKBps; 79 | } else { 80 | // We don't know 81 | return 0; 82 | } 83 | } 84 | } 85 | 86 | get averageKBps() { 87 | let durationSeconds = (this._now() - this._firstCheckpoint) / 1000; 88 | return (this._totalBytes / durationSeconds) / 1024; 89 | } 90 | 91 | } 92 | 93 | export default SpeedSampler; -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | ## This file is modified from .gitignore from same folder 2 | ## Which allows dist folder and ignores unnecessary folders 3 | 4 | demo 5 | docs 6 | 7 | ################# 8 | ## Node.js 9 | ################# 10 | 11 | node_modules 12 | npm-debug.log 13 | 14 | ################# 15 | ## Grunt 16 | ################# 17 | 18 | .grunt 19 | _SpecRunner.html 20 | reports 21 | jsdoc 22 | build/temp 23 | coverage/ 24 | 25 | ################# 26 | ## Eclipse 27 | ################# 28 | 29 | *.pydevproject 30 | .project 31 | .metadata 32 | bin/ 33 | tmp/ 34 | .idea/ 35 | *.tmp 36 | *.bak 37 | *.swp 38 | *~.nib 39 | local.properties 40 | .classpath 41 | .settings/ 42 | .loadpath 43 | 44 | # External tool builders 45 | .externalToolBuilders/ 46 | 47 | # Locally stored "Eclipse launch configurations" 48 | *.launch 49 | 50 | # CDT-specific 51 | .cproject 52 | 53 | # PDT-specific 54 | .buildpath 55 | 56 | 57 | ################# 58 | ## Visual Studio 59 | ################# 60 | 61 | ## Ignore Visual Studio temporary files, build results, and 62 | ## files generated by popular Visual Studio add-ons. 63 | 64 | # User-specific files 65 | *.suo 66 | *.user 67 | *.sln.docstates 68 | 69 | # Build results 70 | [Dd]ebug/ 71 | [Rr]elease/ 72 | *_i.c 73 | *_p.c 74 | *.ilk 75 | *.meta 76 | *.obj 77 | *.pch 78 | *.pdb 79 | *.pgc 80 | *.pgd 81 | *.rsp 82 | *.sbr 83 | *.tlb 84 | *.tli 85 | *.tlh 86 | *.tmp 87 | *.vspscc 88 | .builds 89 | *.dotCover 90 | 91 | ## TODO: If you have NuGet Package Restore enabled, uncomment this 92 | #packages/ 93 | 94 | # Visual C++ cache files 95 | ipch/ 96 | *.aps 97 | *.ncb 98 | *.opensdf 99 | *.sdf 100 | 101 | # Visual Studio profiler 102 | *.psess 103 | *.vsp 104 | 105 | # ReSharper is a .NET coding add-in 106 | _ReSharper* 107 | 108 | # Installshield output folder 109 | [Ee]xpress 110 | 111 | # DocProject is a documentation generator add-in 112 | DocProject/buildhelp/ 113 | DocProject/Help/*.HxT 114 | DocProject/Help/*.HxC 115 | DocProject/Help/*.hhc 116 | DocProject/Help/*.hhk 117 | DocProject/Help/*.hhp 118 | DocProject/Help/Html2 119 | DocProject/Help/html 120 | 121 | # Click-Once directory 122 | publish 123 | 124 | # Others 125 | [Bb]in 126 | [Oo]bj 127 | sql 128 | TestResults 129 | *.Cache 130 | ClientBin 131 | stylecop.* 132 | ~$* 133 | *.dbmdl 134 | Generated_Code #added for RIA/Silverlight projects 135 | 136 | # Backup & report files from converting an old project file to a newer 137 | # Visual Studio version. Backup files are not needed, because we have git ;-) 138 | _UpgradeReport_Files/ 139 | Backup*/ 140 | UpgradeLog*.XML 141 | 142 | 143 | 144 | ############ 145 | ## Windows 146 | ############ 147 | 148 | # Windows image file caches 149 | Thumbs.db 150 | 151 | # Folder config file 152 | Desktop.ini 153 | 154 | 155 | ############# 156 | ## Python 157 | ############# 158 | 159 | *.py[co] 160 | 161 | # Packages 162 | *.egg 163 | *.egg-info 164 | eggs 165 | parts 166 | bin 167 | var 168 | sdist 169 | develop-eggs 170 | .installed.cfg 171 | 172 | # Installer logs 173 | pip-log.txt 174 | 175 | # Unit test / coverage reports 176 | .coverage 177 | .tox 178 | 179 | #Translations 180 | *.mo 181 | 182 | #Mr Developer 183 | .mr.developer.cfg 184 | 185 | # Mac crap 186 | .DS_Store 187 | 188 | # Visual Studio Code 189 | .vscode/ 190 | browse.VC.db 191 | -------------------------------------------------------------------------------- /d.ts/src/demux/ts-demuxer.d.ts: -------------------------------------------------------------------------------- 1 | import BaseDemuxer from './base-demuxer'; 2 | declare class TSDemuxer extends BaseDemuxer { 3 | private readonly TAG; 4 | private config_; 5 | private ts_packet_size_; 6 | private sync_offset_; 7 | private first_parse_; 8 | private media_info_; 9 | private timescale_; 10 | private duration_; 11 | private pat_; 12 | private current_program_; 13 | private current_pmt_pid_; 14 | private pmt_; 15 | private program_pmt_map_; 16 | private pes_slice_queues_; 17 | private section_slice_queues_; 18 | private video_metadata_; 19 | private audio_metadata_; 20 | private last_pcr_; 21 | private last_pcr_base_; 22 | private timestamp_offset_; 23 | private audio_last_sample_pts_; 24 | private aac_last_incomplete_data_; 25 | private has_video_; 26 | private has_audio_; 27 | private video_init_segment_dispatched_; 28 | private audio_init_segment_dispatched_; 29 | private video_metadata_changed_; 30 | private loas_previous_frame; 31 | private video_track_; 32 | private audio_track_; 33 | preferred_secondary_audio: boolean; 34 | constructor(probe_data: any, config: any); 35 | destroy(): void; 36 | static probe(buffer: ArrayBuffer): { 37 | needMoreData: boolean; 38 | match?: undefined; 39 | consumed?: undefined; 40 | ts_packet_size?: undefined; 41 | sync_offset?: undefined; 42 | } | { 43 | match: boolean; 44 | needMoreData?: undefined; 45 | consumed?: undefined; 46 | ts_packet_size?: undefined; 47 | sync_offset?: undefined; 48 | } | { 49 | match: boolean; 50 | consumed: number; 51 | ts_packet_size: number; 52 | sync_offset: number; 53 | needMoreData?: undefined; 54 | }; 55 | bindDataSource(loader: any): this; 56 | resetMediaInfo(): void; 57 | parseChunks(chunk: ArrayBuffer, byte_start: number): number; 58 | private handleSectionSlice; 59 | private handlePESSlice; 60 | private emitSectionSlices; 61 | private emitPESSlices; 62 | private clearSlices; 63 | private parseSection; 64 | private parsePES; 65 | private parsePAT; 66 | private parsePMT; 67 | private parseSCTE35; 68 | private parseAV1Payload; 69 | private parseH264Payload; 70 | private parseH265Payload; 71 | private detectVideoMetadataChange; 72 | private isInitSegmentDispatched; 73 | private dispatchVideoInitSegment; 74 | private dispatchVideoMediaSegment; 75 | private dispatchAudioMediaSegment; 76 | private dispatchAudioVideoMediaSegment; 77 | private parseADTSAACPayload; 78 | private parseLOASAACPayload; 79 | private parseAC3Payload; 80 | private parseEAC3Payload; 81 | private parseOpusPayload; 82 | private parseMP3Payload; 83 | private detectAudioMetadataChange; 84 | private dispatchAudioInitSegment; 85 | private dispatchPESPrivateDataDescriptor; 86 | private parsePESPrivateDataPayload; 87 | private parseTimedID3MetadataPayload; 88 | private parsePGSPayload; 89 | private parseSynchronousKLVMetadataPayload; 90 | private parseAsynchronousKLVMetadataPayload; 91 | private parseSMPTE2038MetadataPayload; 92 | private getNearestTimestampMilliseconds; 93 | private getPcrBase; 94 | private getTimestamp; 95 | } 96 | export default TSDemuxer; 97 | -------------------------------------------------------------------------------- /src/mpegts.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Bilibili. All Rights Reserved. 3 | * 4 | * @author zheng qian 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | import Polyfill from './utils/polyfill.js'; 20 | import Features from './core/features.js'; 21 | import {BaseLoader, LoaderStatus, LoaderErrors} from './io/loader.js'; 22 | import MSEPlayer from './player/mse-player'; 23 | import NativePlayer from './player/native-player.js'; 24 | import PlayerEvents from './player/player-events'; 25 | import {ErrorTypes, ErrorDetails} from './player/player-errors.js'; 26 | import LoggingControl from './utils/logging-control.js'; 27 | import {InvalidArgumentException} from './utils/exception.js'; 28 | 29 | // here are all the interfaces 30 | 31 | // install polyfills 32 | Polyfill.install(); 33 | 34 | 35 | // factory method 36 | function createPlayer(mediaDataSource, optionalConfig) { 37 | let mds = mediaDataSource; 38 | if (mds == null || typeof mds !== 'object') { 39 | throw new InvalidArgumentException('MediaDataSource must be an javascript object!'); 40 | } 41 | 42 | if (!mds.hasOwnProperty('type')) { 43 | throw new InvalidArgumentException('MediaDataSource must has type field to indicate video file type!'); 44 | } 45 | 46 | switch (mds.type) { 47 | case 'mse': 48 | case 'mpegts': 49 | case 'm2ts': 50 | case 'flv': 51 | return new MSEPlayer(mds, optionalConfig); 52 | default: 53 | return new NativePlayer(mds, optionalConfig); 54 | } 55 | } 56 | 57 | 58 | // feature detection 59 | function isSupported() { 60 | return Features.supportMSEH264Playback(); 61 | } 62 | 63 | function getFeatureList() { 64 | return Features.getFeatureList(); 65 | } 66 | 67 | function supportWorkerForMSEH265Playback() { 68 | return Features.supportWorkerForMSEH265Playback(); 69 | } 70 | 71 | 72 | // interfaces 73 | let mpegts = {}; 74 | 75 | mpegts.createPlayer = createPlayer; 76 | mpegts.isSupported = isSupported; 77 | mpegts.getFeatureList = getFeatureList; 78 | mpegts.supportWorkerForMSEH265Playback = supportWorkerForMSEH265Playback; 79 | 80 | mpegts.BaseLoader = BaseLoader; 81 | mpegts.LoaderStatus = LoaderStatus; 82 | mpegts.LoaderErrors = LoaderErrors; 83 | 84 | mpegts.Events = PlayerEvents; 85 | mpegts.ErrorTypes = ErrorTypes; 86 | mpegts.ErrorDetails = ErrorDetails; 87 | 88 | mpegts.MSEPlayer = MSEPlayer; 89 | mpegts.NativePlayer = NativePlayer; 90 | mpegts.LoggingControl = LoggingControl; 91 | 92 | Object.defineProperty(mpegts, 'version', { 93 | enumerable: true, 94 | get: function () { 95 | // replaced by webpack.DefinePlugin 96 | return __VERSION__; 97 | } 98 | }); 99 | 100 | export default mpegts; 101 | -------------------------------------------------------------------------------- /src/player/startup-stall-jumper.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 zheng qian. All Rights Reserved. 3 | * 4 | * @author zheng qian 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | import Log from '../utils/logger'; 20 | 21 | class StartupStallJumper { 22 | 23 | private readonly TAG: string = 'StartupStallJumper'; 24 | 25 | private _media_element: HTMLMediaElement = null; 26 | private _on_direct_seek: (target: number) => void = null; 27 | private _canplay_received: boolean = false; 28 | 29 | private e: any = null; 30 | 31 | public constructor(media_element: HTMLMediaElement, on_direct_seek: (target: number) => void) { 32 | this._media_element = media_element; 33 | this._on_direct_seek = on_direct_seek; 34 | 35 | this.e = { 36 | onMediaCanPlay: this._onMediaCanPlay.bind(this), 37 | onMediaStalled: this._onMediaStalled.bind(this), 38 | onMediaProgress: this._onMediaProgress.bind(this), 39 | }; 40 | 41 | this._media_element.addEventListener('canplay', this.e.onMediaCanPlay); 42 | this._media_element.addEventListener('stalled', this.e.onMediaStalled); 43 | this._media_element.addEventListener('progress', this.e.onMediaProgress); 44 | } 45 | 46 | public destroy(): void { 47 | this._media_element.removeEventListener('canplay', this.e.onMediaCanPlay); 48 | this._media_element.removeEventListener('stalled', this.e.onMediaStalled); 49 | this._media_element.removeEventListener('progress', this.e.onMediaProgress); 50 | this._media_element = null; 51 | this._on_direct_seek = null; 52 | } 53 | 54 | private _onMediaCanPlay(e: Event): void { 55 | this._canplay_received = true; 56 | // Remove canplay listener since it will be fired multiple times 57 | this._media_element.removeEventListener('canplay', this.e.onMediaCanPlay); 58 | } 59 | 60 | private _onMediaStalled(e: Event): void { 61 | this._detectAndFixStuckPlayback(true); 62 | } 63 | 64 | private _onMediaProgress(e: Event): void { 65 | this._detectAndFixStuckPlayback(); 66 | } 67 | 68 | private _detectAndFixStuckPlayback(is_stalled?: boolean): void { 69 | const media = this._media_element; 70 | const buffered = media.buffered; 71 | 72 | if (is_stalled || !this._canplay_received || media.readyState < 2) { // HAVE_CURRENT_DATA 73 | if (buffered.length > 0 && media.currentTime < buffered.start(0)) { 74 | Log.w(this.TAG, `Playback seems stuck at ${media.currentTime}, seek to ${buffered.start(0)}`); 75 | this._on_direct_seek(buffered.start(0)); 76 | this._media_element.removeEventListener('progress', this.e.onMediaProgress); 77 | } 78 | } else { 79 | // Playback doesn't stuck, remove progress event listener 80 | this._media_element.removeEventListener('progress', this.e.onMediaProgress); 81 | } 82 | } 83 | 84 | } 85 | 86 | export default StartupStallJumper; 87 | -------------------------------------------------------------------------------- /src/demux/av1.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 もにょてっく. All Rights Reserved. 3 | * 4 | * @author もにょ〜ん 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | import Log from "../utils/logger"; 20 | 21 | export class AV1OBUInMpegTsParser { 22 | 23 | private readonly TAG: string = "AV1OBUInMpegTsParser"; 24 | 25 | private data_: Uint8Array; 26 | private current_startcode_offset_: number = 0; 27 | private eof_flag_: boolean = false; 28 | 29 | static _ebsp2rbsp(uint8array: Uint8Array) { 30 | let src = uint8array; 31 | let src_length = src.byteLength; 32 | let dst = new Uint8Array(src_length); 33 | let dst_idx = 0; 34 | 35 | for (let i = 0; i < src_length; i++) { 36 | if (i >= 2) { 37 | // Unescape: Skip 0x03 after 00 00 38 | if (src[i] === 0x03 && src[i - 1] === 0x00 && src[i - 2] === 0x00) { 39 | continue; 40 | } 41 | } 42 | dst[dst_idx] = src[i]; 43 | dst_idx++; 44 | } 45 | 46 | return new Uint8Array(dst.buffer, 0, dst_idx); 47 | } 48 | 49 | public constructor(data: Uint8Array) { 50 | this.data_ = data; 51 | this.current_startcode_offset_ = this.findNextStartCodeOffset(0); 52 | if (this.eof_flag_) { 53 | Log.e(this.TAG, "Could not find AV1 startcode until payload end!"); 54 | } 55 | } 56 | 57 | private findNextStartCodeOffset(start_offset: number) { 58 | let i = start_offset; 59 | let data = this.data_; 60 | 61 | while (true) { 62 | if (i + 2 >= data.byteLength) { 63 | this.eof_flag_ = true; 64 | return data.byteLength; 65 | } 66 | 67 | // search 00 00 01 68 | let uint24 = (data[i + 0] << 16) 69 | | (data[i + 1] << 8) 70 | | (data[i + 2]); 71 | if (uint24 === 0x000001) { 72 | return i; 73 | } else { 74 | i++; 75 | } 76 | } 77 | } 78 | 79 | public readNextOBUPayload(): Uint8Array | null { 80 | let data = this.data_; 81 | let payload: Uint8Array | null = null; 82 | 83 | while (payload == null) { 84 | if (this.eof_flag_) { 85 | break; 86 | } 87 | // offset pointed to start code 88 | let startcode_offset = this.current_startcode_offset_; 89 | 90 | // nalu payload start offset 91 | let offset = startcode_offset + 3; 92 | let next_startcode_offset = this.findNextStartCodeOffset(offset); 93 | this.current_startcode_offset_ = next_startcode_offset; 94 | 95 | payload = AV1OBUInMpegTsParser._ebsp2rbsp(data.subarray(offset, next_startcode_offset)); 96 | } 97 | 98 | return payload; 99 | } 100 | 101 | } 102 | 103 | export default AV1OBUInMpegTsParser; -------------------------------------------------------------------------------- /src/player/player-engine-worker-msg-def.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 zheng qian. All Rights Reserved. 3 | * 4 | * @author zheng qian 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | import MSEEvents from '../core/mse-events'; 20 | import PlayerEvents from './player-events'; 21 | import TransmuxingEvents from '../core/transmuxing-events'; 22 | 23 | export type WorkerMessageType = 24 | | 'destroyed' 25 | | 'mse_init' 26 | | 'mse_event' 27 | | 'player_event' 28 | | 'transmuxing_event' 29 | | 'buffered_position_changed' 30 | | 'logcat_callback'; 31 | 32 | export type WorkerMessagePacket = { 33 | msg: WorkerMessageType, 34 | }; 35 | 36 | export type WorkerMessagePacketMSEInit = WorkerMessagePacket & { 37 | msg: 'mse_init', 38 | handle: any, 39 | }; 40 | 41 | export type WorkerMessagePacketMSEEvent = WorkerMessagePacket & { 42 | msg: 'mse_event', 43 | event: MSEEvents, 44 | }; 45 | 46 | export type WorkerMessagePacketPlayerEvent = WorkerMessagePacket & { 47 | msg: 'player_event', 48 | event: PlayerEvents, 49 | }; 50 | 51 | export type WorkerMessagePacketPlayerEventError = WorkerMessagePacketPlayerEvent & { 52 | msg: 'player_event', 53 | event: PlayerEvents.ERROR, 54 | error_type: string, 55 | error_detail: string, 56 | info: any, 57 | }; 58 | 59 | export type WorkerMessagePacketPlayerEventExtraData = WorkerMessagePacketPlayerEvent & { 60 | msg: 'player_event', 61 | event: 62 | | PlayerEvents.METADATA_ARRIVED 63 | | PlayerEvents.SCRIPTDATA_ARRIVED 64 | | PlayerEvents.TIMED_ID3_METADATA_ARRIVED 65 | | PlayerEvents.PGS_SUBTITLE_ARRIVED 66 | | PlayerEvents.SYNCHRONOUS_KLV_METADATA_ARRIVED 67 | | PlayerEvents.ASYNCHRONOUS_KLV_METADATA_ARRIVED 68 | | PlayerEvents.SMPTE2038_METADATA_ARRIVED 69 | | PlayerEvents.SCTE35_METADATA_ARRIVED 70 | | PlayerEvents.PES_PRIVATE_DATA_DESCRIPTOR 71 | | PlayerEvents.PES_PRIVATE_DATA_ARRIVED, 72 | extraData: any, 73 | }; 74 | 75 | export type WorkerMessagePacketTransmuxingEvent = WorkerMessagePacket & { 76 | msg: 'transmuxing_event', 77 | event: TransmuxingEvents, 78 | }; 79 | 80 | export type WorkerMessagePacketTransmuxingEventInfo = WorkerMessagePacketTransmuxingEvent & { 81 | msg: 'transmuxing_event', 82 | event: TransmuxingEvents.MEDIA_INFO | TransmuxingEvents.STATISTICS_INFO, 83 | info: any, 84 | }; 85 | 86 | export type WorkerMessagePacketTransmuxingEventRecommendSeekpoint = WorkerMessagePacketTransmuxingEvent & { 87 | msg: 'transmuxing_event', 88 | event: TransmuxingEvents.RECOMMEND_SEEKPOINT, 89 | milliseconds: number, 90 | }; 91 | 92 | export type WorkerMessagePacketBufferedPositionChanged = WorkerMessagePacket & { 93 | msg: 'buffered_position_changed', 94 | buffered_position_milliseconds: number, 95 | }; 96 | 97 | export type WorkerMessagePacketLogcatCallback = WorkerMessagePacket & { 98 | msg: 'logcat_callback', 99 | type: string, 100 | logcat: string, 101 | }; 102 | -------------------------------------------------------------------------------- /src/player/live-latency-synchronizer.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 zheng qian. All Rights Reserved. 3 | * 4 | * @author zheng qian 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | import Log from "../utils/logger"; 20 | 21 | // Live buffer latency synchronizer by increasing HTMLMediaElement.playbackRate 22 | class LiveLatencySynchronizer { 23 | TAG = 'LiveLatencySynchronizer'; 24 | 25 | private _config: any = null; 26 | private _media_element: HTMLMediaElement = null; 27 | 28 | private e?: any = null; 29 | 30 | public constructor(config: any, media_element: HTMLMediaElement) { 31 | this._config = config; 32 | this._media_element = media_element; 33 | 34 | this.e = { 35 | onMediaTimeUpdate: this._onMediaTimeUpdate.bind(this), 36 | }; 37 | 38 | this._media_element.addEventListener('timeupdate', this.e.onMediaTimeUpdate); 39 | } 40 | 41 | public destroy(): void { 42 | this._media_element.removeEventListener('timeupdate', this.e.onMediaTimeUpdate); 43 | this._media_element = null; 44 | this._config = null; 45 | } 46 | 47 | private _onMediaTimeUpdate(e: Event): void { 48 | if (!this._config.isLive || !this._config.liveSync) { 49 | return; 50 | } 51 | 52 | const latency = this._getCurrentLatency(); 53 | 54 | if (latency > this._config.liveSyncMaxLatency) { 55 | const playback_rate = Math.min(2, Math.max(1, this._config.liveSyncPlaybackRate)); 56 | if (this._media_element.playbackRate !== playback_rate) { 57 | Log.v(this.TAG, `playbackRate ${this._media_element.playbackRate} => ${playback_rate}`); 58 | this._media_element.playbackRate = playback_rate; 59 | } 60 | } else if (latency > this._config.liveSyncTargetLatency) { 61 | // do nothing, keep playbackRate 62 | } else if (this._config.liveSyncMinLatency != null && latency < this._config.liveSyncMinLatency) { 63 | const playback_rate = Math.min(1, Math.max(0.5, this._config.liveSyncMinPlaybackRate)); 64 | if (this._media_element.playbackRate !== playback_rate) { 65 | Log.v(this.TAG, `playbackRate ${this._media_element.playbackRate} => ${playback_rate}`); 66 | this._media_element.playbackRate = playback_rate; 67 | } 68 | } else if (this._media_element.playbackRate !== 1 && this._media_element.playbackRate !== 0) { 69 | Log.v(this.TAG, `playbackRate ${this._media_element.playbackRate} => 1`); 70 | this._media_element.playbackRate = 1; 71 | } 72 | } 73 | 74 | private _getCurrentLatency(): number { 75 | if (!this._media_element) { 76 | return 0; 77 | } 78 | 79 | const buffered = this._media_element.buffered; 80 | const current_time = this._media_element.currentTime; 81 | 82 | if (buffered.length == 0) { 83 | return 0; 84 | } 85 | 86 | const buffered_end = buffered.end(buffered.length - 1); 87 | return buffered_end - current_time; 88 | } 89 | 90 | } 91 | 92 | export default LiveLatencySynchronizer; 93 | -------------------------------------------------------------------------------- /src/utils/logger.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Bilibili. All Rights Reserved. 3 | * 4 | * @author zheng qian 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | import EventEmitter from 'events'; 20 | 21 | class Log { 22 | 23 | static e(tag, msg) { 24 | if (!tag || Log.FORCE_GLOBAL_TAG) 25 | tag = Log.GLOBAL_TAG; 26 | 27 | let str = `[${tag}] > ${msg}`; 28 | 29 | if (Log.ENABLE_CALLBACK) { 30 | Log.emitter.emit('log', 'error', str); 31 | } 32 | 33 | if (!Log.ENABLE_ERROR) { 34 | return; 35 | } 36 | 37 | if (console.error) { 38 | console.error(str); 39 | } else if (console.warn) { 40 | console.warn(str); 41 | } else { 42 | console.log(str); 43 | } 44 | } 45 | 46 | static i(tag, msg) { 47 | if (!tag || Log.FORCE_GLOBAL_TAG) 48 | tag = Log.GLOBAL_TAG; 49 | 50 | let str = `[${tag}] > ${msg}`; 51 | 52 | if (Log.ENABLE_CALLBACK) { 53 | Log.emitter.emit('log', 'info', str); 54 | } 55 | 56 | if (!Log.ENABLE_INFO) { 57 | return; 58 | } 59 | 60 | if (console.info) { 61 | console.info(str); 62 | } else { 63 | console.log(str); 64 | } 65 | } 66 | 67 | static w(tag, msg) { 68 | if (!tag || Log.FORCE_GLOBAL_TAG) 69 | tag = Log.GLOBAL_TAG; 70 | 71 | let str = `[${tag}] > ${msg}`; 72 | 73 | if (Log.ENABLE_CALLBACK) { 74 | Log.emitter.emit('log', 'warn', str); 75 | } 76 | 77 | if (!Log.ENABLE_WARN) { 78 | return; 79 | } 80 | 81 | if (console.warn) { 82 | console.warn(str); 83 | } else { 84 | console.log(str); 85 | } 86 | } 87 | 88 | static d(tag, msg) { 89 | if (!tag || Log.FORCE_GLOBAL_TAG) 90 | tag = Log.GLOBAL_TAG; 91 | 92 | let str = `[${tag}] > ${msg}`; 93 | 94 | if (Log.ENABLE_CALLBACK) { 95 | Log.emitter.emit('log', 'debug', str); 96 | } 97 | 98 | if (!Log.ENABLE_DEBUG) { 99 | return; 100 | } 101 | 102 | if (console.debug) { 103 | console.debug(str); 104 | } else { 105 | console.log(str); 106 | } 107 | } 108 | 109 | static v(tag, msg) { 110 | if (!tag || Log.FORCE_GLOBAL_TAG) 111 | tag = Log.GLOBAL_TAG; 112 | 113 | let str = `[${tag}] > ${msg}`; 114 | 115 | if (Log.ENABLE_CALLBACK) { 116 | Log.emitter.emit('log', 'verbose', str); 117 | } 118 | 119 | if (!Log.ENABLE_VERBOSE) { 120 | return; 121 | } 122 | 123 | console.log(str); 124 | } 125 | 126 | } 127 | 128 | Log.GLOBAL_TAG = 'mpegts.js'; 129 | Log.FORCE_GLOBAL_TAG = false; 130 | Log.ENABLE_ERROR = true; 131 | Log.ENABLE_INFO = true; 132 | Log.ENABLE_WARN = true; 133 | Log.ENABLE_DEBUG = true; 134 | Log.ENABLE_VERBOSE = true; 135 | 136 | Log.ENABLE_CALLBACK = false; 137 | 138 | Log.emitter = new EventEmitter(); 139 | 140 | export default Log; -------------------------------------------------------------------------------- /src/remux/aac-silent.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Bilibili. All Rights Reserved. 3 | * 4 | * This file is modified from dailymotion's hls.js library (hls.js/src/helper/aac.js) 5 | * @author zheng qian 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | class AAC { 21 | 22 | static getSilentFrame(codec, channelCount) { 23 | if (codec === 'mp4a.40.2') { 24 | // handle LC-AAC 25 | if (channelCount === 1) { 26 | return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x23, 0x80]); 27 | } else if (channelCount === 2) { 28 | return new Uint8Array([0x21, 0x00, 0x49, 0x90, 0x02, 0x19, 0x00, 0x23, 0x80]); 29 | } else if (channelCount === 3) { 30 | return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x20, 0x84, 0x01, 0x26, 0x40, 0x08, 0x64, 0x00, 0x8e]); 31 | } else if (channelCount === 4) { 32 | return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x20, 0x84, 0x01, 0x26, 0x40, 0x08, 0x64, 0x00, 0x80, 0x2c, 0x80, 0x08, 0x02, 0x38]); 33 | } else if (channelCount === 5) { 34 | return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x20, 0x84, 0x01, 0x26, 0x40, 0x08, 0x64, 0x00, 0x82, 0x30, 0x04, 0x99, 0x00, 0x21, 0x90, 0x02, 0x38]); 35 | } else if (channelCount === 6) { 36 | return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x20, 0x84, 0x01, 0x26, 0x40, 0x08, 0x64, 0x00, 0x82, 0x30, 0x04, 0x99, 0x00, 0x21, 0x90, 0x02, 0x00, 0xb2, 0x00, 0x20, 0x08, 0xe0]); 37 | } 38 | } else { 39 | // handle HE-AAC (mp4a.40.5 / mp4a.40.29) 40 | if (channelCount === 1) { 41 | // ffmpeg -y -f lavfi -i "aevalsrc=0:d=0.05" -c:a libfdk_aac -profile:a aac_he -b:a 4k output.aac && hexdump -v -e '16/1 "0x%x," "\n"' -v output.aac 42 | return new Uint8Array([0x1, 0x40, 0x22, 0x80, 0xa3, 0x4e, 0xe6, 0x80, 0xba, 0x8, 0x0, 0x0, 0x0, 0x1c, 0x6, 0xf1, 0xc1, 0xa, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5e]); 43 | } else if (channelCount === 2) { 44 | // ffmpeg -y -f lavfi -i "aevalsrc=0|0:d=0.05" -c:a libfdk_aac -profile:a aac_he_v2 -b:a 4k output.aac && hexdump -v -e '16/1 "0x%x," "\n"' -v output.aac 45 | return new Uint8Array([0x1, 0x40, 0x22, 0x80, 0xa3, 0x5e, 0xe6, 0x80, 0xba, 0x8, 0x0, 0x0, 0x0, 0x0, 0x95, 0x0, 0x6, 0xf1, 0xa1, 0xa, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5e]); 46 | } else if (channelCount === 3) { 47 | // ffmpeg -y -f lavfi -i "aevalsrc=0|0|0:d=0.05" -c:a libfdk_aac -profile:a aac_he_v2 -b:a 4k output.aac && hexdump -v -e '16/1 "0x%x," "\n"' -v output.aac 48 | return new Uint8Array([0x1, 0x40, 0x22, 0x80, 0xa3, 0x5e, 0xe6, 0x80, 0xba, 0x8, 0x0, 0x0, 0x0, 0x0, 0x95, 0x0, 0x6, 0xf1, 0xa1, 0xa, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5e]); 49 | } 50 | } 51 | return null; 52 | } 53 | 54 | } 55 | 56 | export default AAC; -------------------------------------------------------------------------------- /src/io/loader.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Bilibili. All Rights Reserved. 3 | * 4 | * @author zheng qian 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | import {NotImplementedException} from '../utils/exception.js'; 20 | 21 | export const LoaderStatus = { 22 | kIdle: 0, 23 | kConnecting: 1, 24 | kBuffering: 2, 25 | kError: 3, 26 | kComplete: 4 27 | }; 28 | 29 | export const LoaderErrors = { 30 | OK: 'OK', 31 | EXCEPTION: 'Exception', 32 | HTTP_STATUS_CODE_INVALID: 'HttpStatusCodeInvalid', 33 | CONNECTING_TIMEOUT: 'ConnectingTimeout', 34 | EARLY_EOF: 'EarlyEof', 35 | UNRECOVERABLE_EARLY_EOF: 'UnrecoverableEarlyEof' 36 | }; 37 | 38 | /* Loader has callbacks which have following prototypes: 39 | * function onContentLengthKnown(contentLength: number): void 40 | * function onURLRedirect(url: string): void 41 | * function onDataArrival(chunk: ArrayBuffer, byteStart: number, receivedLength: number): void 42 | * function onError(errorType: number, errorInfo: {code: number, msg: string}): void 43 | * function onComplete(rangeFrom: number, rangeTo: number): void 44 | */ 45 | export class BaseLoader { 46 | 47 | constructor(typeName) { 48 | this._type = typeName || 'undefined'; 49 | this._status = LoaderStatus.kIdle; 50 | this._needStash = false; 51 | // callbacks 52 | this._onContentLengthKnown = null; 53 | this._onURLRedirect = null; 54 | this._onDataArrival = null; 55 | this._onError = null; 56 | this._onComplete = null; 57 | } 58 | 59 | destroy() { 60 | this._status = LoaderStatus.kIdle; 61 | this._onContentLengthKnown = null; 62 | this._onURLRedirect = null; 63 | this._onDataArrival = null; 64 | this._onError = null; 65 | this._onComplete = null; 66 | } 67 | 68 | isWorking() { 69 | return this._status === LoaderStatus.kConnecting || this._status === LoaderStatus.kBuffering; 70 | } 71 | 72 | get type() { 73 | return this._type; 74 | } 75 | 76 | get status() { 77 | return this._status; 78 | } 79 | 80 | get needStashBuffer() { 81 | return this._needStash; 82 | } 83 | 84 | get onContentLengthKnown() { 85 | return this._onContentLengthKnown; 86 | } 87 | 88 | set onContentLengthKnown(callback) { 89 | this._onContentLengthKnown = callback; 90 | } 91 | 92 | get onURLRedirect() { 93 | return this._onURLRedirect; 94 | } 95 | 96 | set onURLRedirect(callback) { 97 | this._onURLRedirect = callback; 98 | } 99 | 100 | get onDataArrival() { 101 | return this._onDataArrival; 102 | } 103 | 104 | set onDataArrival(callback) { 105 | this._onDataArrival = callback; 106 | } 107 | 108 | get onError() { 109 | return this._onError; 110 | } 111 | 112 | set onError(callback) { 113 | this._onError = callback; 114 | } 115 | 116 | get onComplete() { 117 | return this._onComplete; 118 | } 119 | 120 | set onComplete(callback) { 121 | this._onComplete = callback; 122 | } 123 | 124 | // pure virtual 125 | open(dataSource, range) { 126 | throw new NotImplementedException('Unimplemented abstract function!'); 127 | } 128 | 129 | abort() { 130 | throw new NotImplementedException('Unimplemented abstract function!'); 131 | } 132 | 133 | 134 | } -------------------------------------------------------------------------------- /src/demux/exp-golomb.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Bilibili. All Rights Reserved. 3 | * 4 | * @author zheng qian 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | import {IllegalStateException, InvalidArgumentException} from '../utils/exception.js'; 20 | 21 | // Exponential-Golomb buffer decoder 22 | class ExpGolomb { 23 | 24 | constructor(uint8array) { 25 | this.TAG = 'ExpGolomb'; 26 | 27 | this._buffer = uint8array; 28 | this._buffer_index = 0; 29 | this._total_bytes = uint8array.byteLength; 30 | this._total_bits = uint8array.byteLength * 8; 31 | this._current_word = 0; 32 | this._current_word_bits_left = 0; 33 | } 34 | 35 | destroy() { 36 | this._buffer = null; 37 | } 38 | 39 | _fillCurrentWord() { 40 | let buffer_bytes_left = this._total_bytes - this._buffer_index; 41 | if (buffer_bytes_left <= 0) 42 | throw new IllegalStateException('ExpGolomb: _fillCurrentWord() but no bytes available'); 43 | 44 | let bytes_read = Math.min(4, buffer_bytes_left); 45 | let word = new Uint8Array(4); 46 | word.set(this._buffer.subarray(this._buffer_index, this._buffer_index + bytes_read)); 47 | this._current_word = new DataView(word.buffer).getUint32(0, false); 48 | 49 | this._buffer_index += bytes_read; 50 | this._current_word_bits_left = bytes_read * 8; 51 | } 52 | 53 | readBits(bits) { 54 | if (bits > 32) 55 | throw new InvalidArgumentException('ExpGolomb: readBits() bits exceeded max 32bits!'); 56 | 57 | if (bits <= this._current_word_bits_left) { 58 | let result = this._current_word >>> (32 - bits); 59 | this._current_word <<= bits; 60 | this._current_word_bits_left -= bits; 61 | return result; 62 | } 63 | 64 | let result = this._current_word_bits_left ? this._current_word : 0; 65 | result = result >>> (32 - this._current_word_bits_left); 66 | let bits_need_left = bits - this._current_word_bits_left; 67 | 68 | this._fillCurrentWord(); 69 | let bits_read_next = Math.min(bits_need_left, this._current_word_bits_left); 70 | 71 | let result2 = this._current_word >>> (32 - bits_read_next); 72 | this._current_word <<= bits_read_next; 73 | this._current_word_bits_left -= bits_read_next; 74 | 75 | result = (result << bits_read_next) | result2; 76 | return result; 77 | } 78 | 79 | readBool() { 80 | return this.readBits(1) === 1; 81 | } 82 | 83 | readByte() { 84 | return this.readBits(8); 85 | } 86 | 87 | _skipLeadingZero() { 88 | let zero_count; 89 | for (zero_count = 0; zero_count < this._current_word_bits_left; zero_count++) { 90 | if (0 !== (this._current_word & (0x80000000 >>> zero_count))) { 91 | this._current_word <<= zero_count; 92 | this._current_word_bits_left -= zero_count; 93 | return zero_count; 94 | } 95 | } 96 | this._fillCurrentWord(); 97 | return zero_count + this._skipLeadingZero(); 98 | } 99 | 100 | readUEG() { // unsigned exponential golomb 101 | let leading_zeros = this._skipLeadingZero(); 102 | return this.readBits(leading_zeros + 1) - 1; 103 | } 104 | 105 | readSEG() { // signed exponential golomb 106 | let value = this.readUEG(); 107 | if (value & 0x01) { 108 | return (value + 1) >>> 1; 109 | } else { 110 | return -1 * (value >>> 1); 111 | } 112 | } 113 | 114 | } 115 | 116 | export default ExpGolomb; -------------------------------------------------------------------------------- /src/utils/browser.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Bilibili. All Rights Reserved. 3 | * 4 | * @author zheng qian 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | let Browser = {}; 20 | 21 | function detect() { 22 | // modified from jquery-browser-plugin 23 | 24 | let ua = self.navigator.userAgent.toLowerCase(); 25 | 26 | let match = /(edge)\/([\w.]+)/.exec(ua) || 27 | /(opr)[\/]([\w.]+)/.exec(ua) || 28 | /(chrome)[ \/]([\w.]+)/.exec(ua) || 29 | /(iemobile)[\/]([\w.]+)/.exec(ua) || 30 | /(version)(applewebkit)[ \/]([\w.]+).*(safari)[ \/]([\w.]+)/.exec(ua) || 31 | /(webkit)[ \/]([\w.]+).*(version)[ \/]([\w.]+).*(safari)[ \/]([\w.]+)/.exec(ua) || 32 | /(webkit)[ \/]([\w.]+)/.exec(ua) || 33 | /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) || 34 | /(msie) ([\w.]+)/.exec(ua) || 35 | ua.indexOf('trident') >= 0 && /(rv)(?::| )([\w.]+)/.exec(ua) || 36 | ua.indexOf('compatible') < 0 && /(firefox)[ \/]([\w.]+)/.exec(ua) || 37 | []; 38 | 39 | let platform_match = /(ipad)/.exec(ua) || 40 | /(ipod)/.exec(ua) || 41 | /(windows phone)/.exec(ua) || 42 | /(iphone)/.exec(ua) || 43 | /(kindle)/.exec(ua) || 44 | /(android)/.exec(ua) || 45 | /(windows)/.exec(ua) || 46 | /(mac)/.exec(ua) || 47 | /(linux)/.exec(ua) || 48 | /(cros)/.exec(ua) || 49 | []; 50 | 51 | let matched = { 52 | browser: match[5] || match[3] || match[1] || '', 53 | version: match[2] || match[4] || '0', 54 | majorVersion: match[4] || match[2] || '0', 55 | platform: platform_match[0] || '' 56 | }; 57 | 58 | let browser = {}; 59 | if (matched.browser) { 60 | browser[matched.browser] = true; 61 | 62 | let versionArray = matched.majorVersion.split('.'); 63 | browser.version = { 64 | major: parseInt(matched.majorVersion, 10), 65 | string: matched.version 66 | }; 67 | if (versionArray.length > 1) { 68 | browser.version.minor = parseInt(versionArray[1], 10); 69 | } 70 | if (versionArray.length > 2) { 71 | browser.version.build = parseInt(versionArray[2], 10); 72 | } 73 | } 74 | 75 | if (matched.platform) { 76 | browser[matched.platform] = true; 77 | } 78 | 79 | if (browser.chrome || browser.opr || browser.safari) { 80 | browser.webkit = true; 81 | } 82 | 83 | // MSIE. IE11 has 'rv' identifer 84 | if (browser.rv || browser.iemobile) { 85 | if (browser.rv) { 86 | delete browser.rv; 87 | } 88 | let msie = 'msie'; 89 | matched.browser = msie; 90 | browser[msie] = true; 91 | } 92 | 93 | // Microsoft Edge 94 | if (browser.edge) { 95 | delete browser.edge; 96 | let msedge = 'msedge'; 97 | matched.browser = msedge; 98 | browser[msedge] = true; 99 | } 100 | 101 | // Opera 15+ 102 | if (browser.opr) { 103 | let opera = 'opera'; 104 | matched.browser = opera; 105 | browser[opera] = true; 106 | } 107 | 108 | // Stock android browsers are marked as Safari 109 | if (browser.safari && browser.android) { 110 | let android = 'android'; 111 | matched.browser = android; 112 | browser[android] = true; 113 | } 114 | 115 | browser.name = matched.browser; 116 | browser.platform = matched.platform; 117 | 118 | for (let key in Browser) { 119 | if (Browser.hasOwnProperty(key)) { 120 | delete Browser[key]; 121 | } 122 | } 123 | Object.assign(Browser, browser); 124 | } 125 | 126 | detect(); 127 | 128 | export default Browser; -------------------------------------------------------------------------------- /src/core/media-info.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Bilibili. All Rights Reserved. 3 | * 4 | * @author zheng qian 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | class MediaInfo { 20 | 21 | constructor() { 22 | this.mimeType = null; 23 | this.duration = null; 24 | 25 | this.hasAudio = null; 26 | this.hasVideo = null; 27 | this.audioCodec = null; 28 | this.videoCodec = null; 29 | this.audioDataRate = null; 30 | this.videoDataRate = null; 31 | 32 | this.audioSampleRate = null; 33 | this.audioChannelCount = null; 34 | 35 | this.width = null; 36 | this.height = null; 37 | this.fps = null; 38 | this.profile = null; 39 | this.level = null; 40 | this.refFrames = null; 41 | this.chromaFormat = null; 42 | this.sarNum = null; 43 | this.sarDen = null; 44 | 45 | this.metadata = null; 46 | this.segments = null; // MediaInfo[] 47 | this.segmentCount = null; 48 | this.hasKeyframesIndex = null; 49 | this.keyframesIndex = null; 50 | } 51 | 52 | isComplete() { 53 | let audioInfoComplete = (this.hasAudio === false) || 54 | (this.hasAudio === true && 55 | this.audioCodec != null && 56 | this.audioSampleRate != null && 57 | this.audioChannelCount != null); 58 | 59 | let videoInfoComplete = (this.hasVideo === false) || 60 | (this.hasVideo === true && 61 | this.videoCodec != null && 62 | this.width != null && 63 | this.height != null && 64 | this.fps != null && 65 | this.profile != null && 66 | this.level != null && 67 | this.refFrames != null && 68 | this.chromaFormat != null && 69 | this.sarNum != null && 70 | this.sarDen != null); 71 | 72 | // keyframesIndex may not be present 73 | return this.mimeType != null && 74 | audioInfoComplete && 75 | videoInfoComplete; 76 | } 77 | 78 | isSeekable() { 79 | return this.hasKeyframesIndex === true; 80 | } 81 | 82 | getNearestKeyframe(milliseconds) { 83 | if (this.keyframesIndex == null) { 84 | return null; 85 | } 86 | 87 | let table = this.keyframesIndex; 88 | let keyframeIdx = this._search(table.times, milliseconds); 89 | 90 | return { 91 | index: keyframeIdx, 92 | milliseconds: table.times[keyframeIdx], 93 | fileposition: table.filepositions[keyframeIdx] 94 | }; 95 | } 96 | 97 | _search(list, value) { 98 | let idx = 0; 99 | 100 | let last = list.length - 1; 101 | let mid = 0; 102 | let lbound = 0; 103 | let ubound = last; 104 | 105 | if (value < list[0]) { 106 | idx = 0; 107 | lbound = ubound + 1; // skip search 108 | } 109 | 110 | while (lbound <= ubound) { 111 | mid = lbound + Math.floor((ubound - lbound) / 2); 112 | if (mid === last || (value >= list[mid] && value < list[mid + 1])) { 113 | idx = mid; 114 | break; 115 | } else if (list[mid] < value) { 116 | lbound = mid + 1; 117 | } else { 118 | ubound = mid - 1; 119 | } 120 | } 121 | 122 | return idx; 123 | } 124 | 125 | } 126 | 127 | export default MediaInfo; -------------------------------------------------------------------------------- /src/io/websocket-loader.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Bilibili. All Rights Reserved. 3 | * 4 | * @author zheng qian 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | import Log from '../utils/logger.js'; 20 | import {BaseLoader, LoaderStatus, LoaderErrors} from './loader.js'; 21 | import {RuntimeException} from '../utils/exception.js'; 22 | 23 | // For MPEG-TS/FLV over WebSocket live stream 24 | class WebSocketLoader extends BaseLoader { 25 | 26 | static isSupported() { 27 | try { 28 | return (typeof self.WebSocket !== 'undefined'); 29 | } catch (e) { 30 | return false; 31 | } 32 | } 33 | 34 | constructor() { 35 | super('websocket-loader'); 36 | this.TAG = 'WebSocketLoader'; 37 | 38 | this._needStash = true; 39 | 40 | this._ws = null; 41 | this._requestAbort = false; 42 | this._receivedLength = 0; 43 | } 44 | 45 | destroy() { 46 | if (this._ws) { 47 | this.abort(); 48 | } 49 | super.destroy(); 50 | } 51 | 52 | open(dataSource) { 53 | try { 54 | let ws = this._ws = new self.WebSocket(dataSource.url); 55 | ws.binaryType = 'arraybuffer'; 56 | ws.onopen = this._onWebSocketOpen.bind(this); 57 | ws.onclose = this._onWebSocketClose.bind(this); 58 | ws.onmessage = this._onWebSocketMessage.bind(this); 59 | ws.onerror = this._onWebSocketError.bind(this); 60 | 61 | this._status = LoaderStatus.kConnecting; 62 | } catch (e) { 63 | this._status = LoaderStatus.kError; 64 | 65 | let info = {code: e.code, msg: e.message}; 66 | 67 | if (this._onError) { 68 | this._onError(LoaderErrors.EXCEPTION, info); 69 | } else { 70 | throw new RuntimeException(info.msg); 71 | } 72 | } 73 | } 74 | 75 | abort() { 76 | let ws = this._ws; 77 | if (ws && (ws.readyState === 0 || ws.readyState === 1)) { // CONNECTING || OPEN 78 | this._requestAbort = true; 79 | ws.close(); 80 | } 81 | 82 | this._ws = null; 83 | this._status = LoaderStatus.kComplete; 84 | } 85 | 86 | _onWebSocketOpen(e) { 87 | this._status = LoaderStatus.kBuffering; 88 | } 89 | 90 | _onWebSocketClose(e) { 91 | if (this._requestAbort === true) { 92 | this._requestAbort = false; 93 | return; 94 | } 95 | 96 | this._status = LoaderStatus.kComplete; 97 | 98 | if (this._onComplete) { 99 | this._onComplete(0, this._receivedLength - 1); 100 | } 101 | } 102 | 103 | _onWebSocketMessage(e) { 104 | if (e.data instanceof ArrayBuffer) { 105 | this._dispatchArrayBuffer(e.data); 106 | } else if (e.data instanceof Blob) { 107 | let reader = new FileReader(); 108 | reader.onload = () => { 109 | this._dispatchArrayBuffer(reader.result); 110 | }; 111 | reader.readAsArrayBuffer(e.data); 112 | } else { 113 | this._status = LoaderStatus.kError; 114 | let info = {code: -1, msg: 'Unsupported WebSocket message type: ' + e.data.constructor.name}; 115 | 116 | if (this._onError) { 117 | this._onError(LoaderErrors.EXCEPTION, info); 118 | } else { 119 | throw new RuntimeException(info.msg); 120 | } 121 | } 122 | } 123 | 124 | _dispatchArrayBuffer(arraybuffer) { 125 | let chunk = arraybuffer; 126 | let byteStart = this._receivedLength; 127 | this._receivedLength += chunk.byteLength; 128 | 129 | if (this._onDataArrival) { 130 | this._onDataArrival(chunk, byteStart, this._receivedLength); 131 | } 132 | } 133 | 134 | _onWebSocketError(e) { 135 | this._status = LoaderStatus.kError; 136 | 137 | let info = { 138 | code: e.code, 139 | msg: e.message 140 | }; 141 | 142 | if (this._onError) { 143 | this._onError(LoaderErrors.EXCEPTION, info); 144 | } else { 145 | throw new RuntimeException(info.msg); 146 | } 147 | } 148 | 149 | } 150 | 151 | export default WebSocketLoader; -------------------------------------------------------------------------------- /src/core/features.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Bilibili. All Rights Reserved. 3 | * 4 | * @author zheng qian 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | import IOController from '../io/io-controller.js'; 20 | import {createDefaultConfig} from '../config.js'; 21 | import PlayerEngineDedicatedThread from '../player/player-engine-dedicated-thread'; 22 | 23 | class Features { 24 | 25 | static supportMSEH264Playback() { 26 | const avc_aac_mime_type = 'video/mp4; codecs="avc1.42E01E,mp4a.40.2"'; 27 | const support_w3c_mse = self.MediaSource && self.MediaSource.isTypeSupported(avc_aac_mime_type); 28 | const support_apple_mme = self.ManagedMediaSource && self.ManagedMediaSource.isTypeSupported(avc_aac_mime_type); 29 | return support_w3c_mse === true|| support_apple_mme === true; 30 | } 31 | 32 | static supportMSEH265Playback() { 33 | const hevc_mime_type = 'video/mp4; codecs="hvc1.1.6.L93.B0"'; 34 | const support_w3c_mse = self.MediaSource && self.MediaSource.isTypeSupported(hevc_mime_type); 35 | const support_apple_mme = self.ManagedMediaSource && self.ManagedMediaSource.isTypeSupported(hevc_mime_type); 36 | return support_w3c_mse === true || support_apple_mme === true; 37 | } 38 | 39 | static supportWorkerForMSEH265Playback() { 40 | return new Promise((resolve) => { 41 | if (!PlayerEngineDedicatedThread.isSupported()) { 42 | resolve(false); 43 | return; 44 | } 45 | const blobUrl = URL.createObjectURL(new Blob([` 46 | const hevc_mime_type = 'video/mp4; codecs="hvc1.1.6.L93.B0"'; 47 | const support_w3c_mse = self.MediaSource && self.MediaSource.isTypeSupported(hevc_mime_type); 48 | const support_apple_mme = self.ManagedMediaSource && self.ManagedMediaSource.isTypeSupported(hevc_mime_type); 49 | self.postMessage(support_w3c_mse === true || support_apple_mme === true); 50 | `], { type: 'application/javascript' })); 51 | const worker = new Worker(blobUrl); 52 | worker.addEventListener('message', (e) => resolve(e.data)); 53 | URL.revokeObjectURL(blobUrl); 54 | }); 55 | } 56 | 57 | static supportNetworkStreamIO() { 58 | let ioctl = new IOController({}, createDefaultConfig()); 59 | let loaderType = ioctl.loaderType; 60 | ioctl.destroy(); 61 | return loaderType == 'fetch-stream-loader' || loaderType == 'xhr-moz-chunked-loader'; 62 | } 63 | 64 | static getNetworkLoaderTypeName() { 65 | let ioctl = new IOController({}, createDefaultConfig()); 66 | let loaderType = ioctl.loaderType; 67 | ioctl.destroy(); 68 | return loaderType; 69 | } 70 | 71 | static supportNativeMediaPlayback(mimeType) { 72 | if (Features.videoElement == undefined) { 73 | Features.videoElement = window.document.createElement('video'); 74 | } 75 | let canPlay = Features.videoElement.canPlayType(mimeType); 76 | return canPlay === 'probably' || canPlay == 'maybe'; 77 | } 78 | 79 | static getFeatureList() { 80 | let features = { 81 | msePlayback: false, 82 | mseLivePlayback: false, 83 | mseH265Playback: false, 84 | networkStreamIO: false, 85 | networkLoaderName: '', 86 | nativeMP4H264Playback: false, 87 | nativeMP4H265Playback: false, 88 | nativeWebmVP8Playback: false, 89 | nativeWebmVP9Playback: false 90 | }; 91 | 92 | features.msePlayback = Features.supportMSEH264Playback(); 93 | features.networkStreamIO = Features.supportNetworkStreamIO(); 94 | features.networkLoaderName = Features.getNetworkLoaderTypeName(); 95 | features.mseLivePlayback = features.msePlayback && features.networkStreamIO; 96 | features.mseH265Playback = Features.supportMSEH265Playback(); 97 | features.nativeMP4H264Playback = Features.supportNativeMediaPlayback('video/mp4; codecs="avc1.42001E, mp4a.40.2"'); 98 | features.nativeMP4H265Playback = Features.supportNativeMediaPlayback('video/mp4; codecs="hvc1.1.6.L93.B0"'); 99 | features.nativeWebmVP8Playback = Features.supportNativeMediaPlayback('video/webm; codecs="vp8.0, vorbis"'); 100 | features.nativeWebmVP9Playback = Features.supportNativeMediaPlayback('video/webm; codecs="vp9"'); 101 | 102 | return features; 103 | } 104 | 105 | } 106 | 107 | export default Features; -------------------------------------------------------------------------------- /src/utils/logging-control.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Bilibili. All Rights Reserved. 3 | * 4 | * @author zheng qian 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | import EventEmitter from 'events'; 20 | import Log from './logger.js'; 21 | 22 | class LoggingControl { 23 | 24 | static get forceGlobalTag() { 25 | return Log.FORCE_GLOBAL_TAG; 26 | } 27 | 28 | static set forceGlobalTag(enable) { 29 | Log.FORCE_GLOBAL_TAG = enable; 30 | LoggingControl._notifyChange(); 31 | } 32 | 33 | static get globalTag() { 34 | return Log.GLOBAL_TAG; 35 | } 36 | 37 | static set globalTag(tag) { 38 | Log.GLOBAL_TAG = tag; 39 | LoggingControl._notifyChange(); 40 | } 41 | 42 | static get enableAll() { 43 | return Log.ENABLE_VERBOSE 44 | && Log.ENABLE_DEBUG 45 | && Log.ENABLE_INFO 46 | && Log.ENABLE_WARN 47 | && Log.ENABLE_ERROR; 48 | } 49 | 50 | static set enableAll(enable) { 51 | Log.ENABLE_VERBOSE = enable; 52 | Log.ENABLE_DEBUG = enable; 53 | Log.ENABLE_INFO = enable; 54 | Log.ENABLE_WARN = enable; 55 | Log.ENABLE_ERROR = enable; 56 | LoggingControl._notifyChange(); 57 | } 58 | 59 | static get enableDebug() { 60 | return Log.ENABLE_DEBUG; 61 | } 62 | 63 | static set enableDebug(enable) { 64 | Log.ENABLE_DEBUG = enable; 65 | LoggingControl._notifyChange(); 66 | } 67 | 68 | static get enableVerbose() { 69 | return Log.ENABLE_VERBOSE; 70 | } 71 | 72 | static set enableVerbose(enable) { 73 | Log.ENABLE_VERBOSE = enable; 74 | LoggingControl._notifyChange(); 75 | } 76 | 77 | static get enableInfo() { 78 | return Log.ENABLE_INFO; 79 | } 80 | 81 | static set enableInfo(enable) { 82 | Log.ENABLE_INFO = enable; 83 | LoggingControl._notifyChange(); 84 | } 85 | 86 | static get enableWarn() { 87 | return Log.ENABLE_WARN; 88 | } 89 | 90 | static set enableWarn(enable) { 91 | Log.ENABLE_WARN = enable; 92 | LoggingControl._notifyChange(); 93 | } 94 | 95 | static get enableError() { 96 | return Log.ENABLE_ERROR; 97 | } 98 | 99 | static set enableError(enable) { 100 | Log.ENABLE_ERROR = enable; 101 | LoggingControl._notifyChange(); 102 | } 103 | 104 | static getConfig() { 105 | return { 106 | globalTag: Log.GLOBAL_TAG, 107 | forceGlobalTag: Log.FORCE_GLOBAL_TAG, 108 | enableVerbose: Log.ENABLE_VERBOSE, 109 | enableDebug: Log.ENABLE_DEBUG, 110 | enableInfo: Log.ENABLE_INFO, 111 | enableWarn: Log.ENABLE_WARN, 112 | enableError: Log.ENABLE_ERROR, 113 | enableCallback: Log.ENABLE_CALLBACK 114 | }; 115 | } 116 | 117 | static applyConfig(config) { 118 | Log.GLOBAL_TAG = config.globalTag; 119 | Log.FORCE_GLOBAL_TAG = config.forceGlobalTag; 120 | Log.ENABLE_VERBOSE = config.enableVerbose; 121 | Log.ENABLE_DEBUG = config.enableDebug; 122 | Log.ENABLE_INFO = config.enableInfo; 123 | Log.ENABLE_WARN = config.enableWarn; 124 | Log.ENABLE_ERROR = config.enableError; 125 | Log.ENABLE_CALLBACK = config.enableCallback; 126 | } 127 | 128 | static _notifyChange() { 129 | let emitter = LoggingControl.emitter; 130 | 131 | if (emitter.listenerCount('change') > 0) { 132 | let config = LoggingControl.getConfig(); 133 | emitter.emit('change', config); 134 | } 135 | } 136 | 137 | static registerListener(listener) { 138 | LoggingControl.emitter.addListener('change', listener); 139 | } 140 | 141 | static removeListener(listener) { 142 | LoggingControl.emitter.removeListener('change', listener); 143 | } 144 | 145 | static addLogListener(listener) { 146 | Log.emitter.addListener('log', listener); 147 | if (Log.emitter.listenerCount('log') > 0) { 148 | Log.ENABLE_CALLBACK = true; 149 | LoggingControl._notifyChange(); 150 | } 151 | } 152 | 153 | static removeLogListener(listener) { 154 | Log.emitter.removeListener('log', listener); 155 | if (Log.emitter.listenerCount('log') === 0) { 156 | Log.ENABLE_CALLBACK = false; 157 | LoggingControl._notifyChange(); 158 | } 159 | } 160 | 161 | } 162 | 163 | LoggingControl.emitter = new EventEmitter(); 164 | 165 | export default LoggingControl; -------------------------------------------------------------------------------- /src/player/mse-player.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 zheng qian. All Rights Reserved. 3 | * 4 | * @author zheng qian 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | import Log from '../utils/logger'; 20 | import MediaInfo from '../core/media-info'; 21 | import PlayerEngine from './player-engine'; 22 | import PlayerEngineMainThread from './player-engine-main-thread'; 23 | import PlayerEngineDedicatedThread from './player-engine-dedicated-thread'; 24 | import {InvalidArgumentException} from '../utils/exception'; 25 | 26 | class MSEPlayer { 27 | 28 | private readonly TAG: string = 'MSEPlayer'; 29 | 30 | private _type: string = 'MSEPlayer'; 31 | 32 | private _media_element: HTMLMediaElement = null; 33 | private _player_engine: PlayerEngine = null; 34 | 35 | public constructor(mediaDataSource: any, config?: any) { 36 | const typeLowerCase: string = mediaDataSource.type.toLowerCase(); 37 | if (typeLowerCase !== 'mse' 38 | && typeLowerCase !== 'mpegts' 39 | && typeLowerCase !== 'm2ts' 40 | && typeLowerCase !== 'flv') { 41 | throw new InvalidArgumentException('MSEPlayer requires an mpegts/m2ts/flv MediaDataSource input!'); 42 | } 43 | 44 | if (config && config.enableWorkerForMSE && PlayerEngineDedicatedThread.isSupported()) { 45 | try { 46 | this._player_engine = new PlayerEngineDedicatedThread(mediaDataSource, config); 47 | } catch (error) { 48 | Log.e(this.TAG, 49 | 'Error while initializing PlayerEngineDedicatedThread, fallback to PlayerEngineMainThread'); 50 | this._player_engine = new PlayerEngineMainThread(mediaDataSource, config); 51 | } 52 | } else { 53 | this._player_engine = new PlayerEngineMainThread(mediaDataSource, config); 54 | } 55 | } 56 | 57 | public destroy(): void { 58 | this._player_engine.destroy(); 59 | this._player_engine = null; 60 | this._media_element = null; 61 | } 62 | 63 | public on(event: string, listener: (...args: any[]) => void): void { 64 | this._player_engine.on(event, listener); 65 | } 66 | 67 | public off(event: string, listener: (...args: any[]) => void): void { 68 | this._player_engine.off(event, listener); 69 | } 70 | 71 | public attachMediaElement(mediaElement: HTMLMediaElement): void { 72 | this._media_element = mediaElement; 73 | this._player_engine.attachMediaElement(mediaElement); 74 | } 75 | 76 | public detachMediaElement(): void { 77 | this._media_element = null; 78 | this._player_engine.detachMediaElement(); 79 | } 80 | 81 | public load(): void { 82 | this._player_engine.load(); 83 | } 84 | 85 | public unload(): void { 86 | this._player_engine.unload(); 87 | } 88 | 89 | public play(): Promise { 90 | return this._player_engine.play(); 91 | } 92 | 93 | public pause(): void { 94 | this._player_engine.pause(); 95 | } 96 | 97 | public switchPrimaryAudio(): void { 98 | this._player_engine.switchPrimaryAudio(); 99 | } 100 | 101 | public switchSecondaryAudio(): void { 102 | this._player_engine.switchSecondaryAudio(); 103 | } 104 | 105 | public get type(): string { 106 | return this._type; 107 | } 108 | 109 | public get buffered(): TimeRanges { 110 | return this._media_element.buffered; 111 | } 112 | 113 | public get duration(): number { 114 | return this._media_element.duration; 115 | } 116 | 117 | public get volume(): number { 118 | return this._media_element.volume; 119 | } 120 | 121 | public set volume(value) { 122 | this._media_element.volume = value; 123 | } 124 | 125 | public get muted(): boolean { 126 | return this._media_element.muted; 127 | } 128 | 129 | public set muted(muted) { 130 | this._media_element.muted = muted; 131 | } 132 | 133 | public get currentTime(): number { 134 | if (this._media_element) { 135 | return this._media_element.currentTime; 136 | } 137 | return 0; 138 | } 139 | 140 | public set currentTime(seconds: number) { 141 | this._player_engine.seek(seconds); 142 | } 143 | 144 | public get mediaInfo(): MediaInfo { 145 | return this._player_engine.mediaInfo; 146 | } 147 | 148 | public get statisticsInfo(): any { 149 | return this._player_engine.statisticsInfo; 150 | } 151 | 152 | } 153 | 154 | export default MSEPlayer; 155 | -------------------------------------------------------------------------------- /src/player/loading-controller.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 zheng qian. All Rights Reserved. 3 | * 4 | * @author zheng qian 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | import Log from '../utils/logger'; 20 | 21 | class LoadingController { 22 | 23 | private readonly TAG: string = 'LoadingController'; 24 | 25 | private _config: any = null; 26 | private _media_element: HTMLMediaElement = null; 27 | private _on_pause_transmuxer: () => void = null; 28 | private _on_resume_transmuxer: () => void = null; 29 | 30 | private _paused: boolean = false; 31 | 32 | private e?: any = null; 33 | 34 | public constructor( 35 | config: any, 36 | media_element: HTMLMediaElement, 37 | on_pause_transmuxer: () => void, 38 | on_resume_transmuxer: () => void 39 | ) { 40 | this._config = config; 41 | this._media_element = media_element; 42 | this._on_pause_transmuxer = on_pause_transmuxer; 43 | this._on_resume_transmuxer = on_resume_transmuxer; 44 | 45 | this.e = { 46 | onMediaTimeUpdate: this._onMediaTimeUpdate.bind(this), 47 | }; 48 | } 49 | 50 | public destroy(): void { 51 | this._media_element.removeEventListener('timeupdate', this.e.onMediaTimeUpdate); 52 | this.e = null; 53 | this._media_element = null; 54 | this._config = null; 55 | this._on_pause_transmuxer = null; 56 | this._on_resume_transmuxer = null; 57 | } 58 | 59 | // buffered_position: in seconds 60 | public notifyBufferedPositionChanged(buffered_position?: number): void { 61 | if (this._config.isLive || !this._config.lazyLoad) { 62 | return; 63 | } 64 | 65 | if (buffered_position == undefined) { 66 | this._suspendTransmuxerIfNeeded(); 67 | } else { 68 | this._suspendTransmuxerIfBufferedPositionExceeded(buffered_position); 69 | } 70 | } 71 | 72 | private _onMediaTimeUpdate(e: Event): void { 73 | if (this._paused) { 74 | this._resumeTransmuxerIfNeeded(); 75 | } 76 | } 77 | 78 | private _suspendTransmuxerIfNeeded() { 79 | const buffered: TimeRanges = this._media_element.buffered; 80 | const current_time: number = this._media_element.currentTime; 81 | let current_range_end = 0; 82 | 83 | for (let i = 0; i < buffered.length; i++) { 84 | const start = buffered.start(i); 85 | const end = buffered.end(i); 86 | if (start <= current_time && current_time < end) { 87 | current_range_end = end; 88 | break; 89 | } 90 | } 91 | if (current_range_end > 0) { 92 | this._suspendTransmuxerIfBufferedPositionExceeded(current_range_end); 93 | } 94 | } 95 | 96 | private _suspendTransmuxerIfBufferedPositionExceeded(buffered_end: number): void { 97 | const current_time = this._media_element.currentTime; 98 | if (buffered_end >= current_time + this._config.lazyLoadMaxDuration && !this._paused) { 99 | Log.v(this.TAG, 'Maximum buffering duration exceeded, suspend transmuxing task'); 100 | this.suspendTransmuxer(); 101 | this._media_element.addEventListener('timeupdate', this.e.onMediaTimeUpdate); 102 | } 103 | } 104 | 105 | public suspendTransmuxer(): void { 106 | this._paused = true; 107 | this._on_pause_transmuxer(); 108 | } 109 | 110 | private _resumeTransmuxerIfNeeded(): void { 111 | const buffered: TimeRanges = this._media_element.buffered; 112 | const current_time: number = this._media_element.currentTime; 113 | 114 | const recover_duration = this._config.lazyLoadRecoverDuration; 115 | let should_resume = false; 116 | 117 | for (let i = 0; i < buffered.length; i++) { 118 | const from = buffered.start(i); 119 | const to = buffered.end(i); 120 | if (current_time >= from && current_time < to) { 121 | if (current_time >= to - recover_duration) { 122 | should_resume = true; 123 | } 124 | break; 125 | } 126 | } 127 | 128 | if (should_resume) { 129 | Log.v(this.TAG, 'Continue loading from paused position'); 130 | this.resumeTransmuxer(); 131 | this._media_element.removeEventListener('timeupdate', this.e.onMediaTimeUpdate); 132 | } 133 | } 134 | 135 | public resumeTransmuxer(): void { 136 | this._paused = false; 137 | this._on_resume_transmuxer(); 138 | } 139 | 140 | } 141 | 142 | export default LoadingController; 143 | -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 | mpegts.js [![npm](https://img.shields.io/npm/v/mpegts.js.svg?style=flat)](https://www.npmjs.com/package/mpegts.js) 2 | ====== 3 | [日本語](README_ja.md) 4 | 5 | mpegts.js 是在 HTML5 上直接播放 MPEG2-TS 流的播放器,针对低延迟直播优化,可用于 DVB/ISDB 数字电视流或监控摄像头等的低延迟回放。 6 | 7 | mpegts.js 基于 [flv.js](https://github.com/bilibili/flv.js) 改造而来。 8 | 9 | ## Overview 10 | mpegts.js 通过在 JavaScript 中渐进化解析 MPEG2-TS 流并实时转封装为 ISO BMFF (Fragmented MP4),然后通过 [Media Source Extensions][] 把音视频数据喂入 HTML5 `