Showing preview only (386K chars total). Download the full file or copy to clipboard to get everything.
Repository: bilibili/flv.js
Branch: master
Commit: 42343088f226
Files: 61
Total size: 368.0 KB
Directory structure:
gitextract_l0qlpdgd/
├── .eslintignore
├── .eslintrc.json
├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── d.ts/
│ └── flv.d.ts
├── demo/
│ ├── demo.css
│ └── index.html
├── docs/
│ ├── api.md
│ ├── cors.md
│ ├── design.md
│ ├── livestream.md
│ └── multipart.md
├── package.json
├── src/
│ ├── config.js
│ ├── core/
│ │ ├── features.js
│ │ ├── media-info.js
│ │ ├── media-segment-info.js
│ │ ├── mse-controller.js
│ │ ├── mse-events.js
│ │ ├── transmuxer.js
│ │ ├── transmuxing-controller.js
│ │ ├── transmuxing-events.js
│ │ └── transmuxing-worker.js
│ ├── demux/
│ │ ├── amf-parser.js
│ │ ├── demux-errors.js
│ │ ├── exp-golomb.js
│ │ ├── flv-demuxer.js
│ │ └── sps-parser.js
│ ├── flv.js
│ ├── index.js
│ ├── io/
│ │ ├── fetch-stream-loader.js
│ │ ├── io-controller.js
│ │ ├── loader.js
│ │ ├── param-seek-handler.js
│ │ ├── range-seek-handler.js
│ │ ├── speed-sampler.js
│ │ ├── websocket-loader.js
│ │ ├── xhr-moz-chunked-loader.js
│ │ ├── xhr-msstream-loader.js
│ │ └── xhr-range-loader.js
│ ├── player/
│ │ ├── flv-player.js
│ │ ├── native-player.js
│ │ ├── player-errors.js
│ │ └── player-events.js
│ ├── remux/
│ │ ├── aac-silent.js
│ │ ├── mp4-generator.js
│ │ └── mp4-remuxer.js
│ └── utils/
│ ├── browser.js
│ ├── exception.js
│ ├── logger.js
│ ├── logging-control.js
│ ├── polyfill.js
│ └── utf8-conv.js
├── tsconfig.json
├── tslint.json
├── types/
│ ├── index.d.ts
│ ├── test-flv.ts
│ └── tsconfig.json
└── webpack.config.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .eslintignore
================================================
dist/
node_modules/
================================================
FILE: .eslintrc.json
================================================
{
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module",
"ecmaFeatures": {
"impliedStrict": true
}
},
"env": {
"es6": true,
"worker": true,
"node": true,
"browser": true
},
"rules": {
"keyword-spacing": [2, {
"overrides": {
"if": {"after": true},
"for": {"after": true},
"while": {"after": true},
"switch": {"after": true},
"catch": {"after": true}
}
}],
"key-spacing": [2, {"beforeColon": false, "afterColon": true, "mode": "strict"}],
"arrow-spacing": 2,
"comma-spacing": 2,
"comma-style": 2,
"indent": [2, 4, {"SwitchCase": 1}],
"no-var": 2,
"no-bitwise": 0,
"no-alert": 2,
"no-console": 0,
"no-debugger": 1,
"no-unused-vars": 0,
"no-mixed-spaces-and-tabs": 2,
"quotes": [2, "single", "avoid-escape"],
"semi": 2,
"semi-spacing": 2,
"space-before-blocks": 2,
"space-before-function-paren": [2, {"anonymous": "always", "named": "never"}],
"space-in-parens": [2, "never"],
"space-infix-ops": 2
}
}
================================================
FILE: .gitignore
================================================
#################
## Node.js
#################
node_modules
npm-debug.log
#################
## Grunt
#################
.grunt
_SpecRunner.html
reports
jsdoc
dist
build/temp
coverage/
#################
## Eclipse
#################
*.pydevproject
.project
.metadata
bin/
tmp/
.idea/
*.tmp
*.bak
*.swp
*~.nib
local.properties
.classpath
.settings/
.loadpath
# External tool builders
.externalToolBuilders/
# Locally stored "Eclipse launch configurations"
*.launch
# CDT-specific
.cproject
# PDT-specific
.buildpath
#################
## Visual Studio
#################
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# User-specific files
*.suo
*.user
*.sln.docstates
# Build results
[Dd]ebug/
[Rr]elease/
*_i.c
*_p.c
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.vspscc
.builds
*.dotCover
## TODO: If you have NuGet Package Restore enabled, uncomment this
#packages/
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opensdf
*.sdf
# Visual Studio profiler
*.psess
*.vsp
# ReSharper is a .NET coding add-in
_ReSharper*
# Installshield output folder
[Ee]xpress
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish
# Others
[Bb]in
[Oo]bj
sql
TestResults
*.Cache
ClientBin
stylecop.*
~$*
*.dbmdl
Generated_Code #added for RIA/Silverlight projects
# Backup & report files from converting an old project file to a newer
# Visual Studio version. Backup files are not needed, because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
############
## Windows
############
# Windows image file caches
Thumbs.db
# Folder config file
Desktop.ini
#############
## Python
#############
*.py[co]
# Packages
*.egg
*.egg-info
eggs
parts
bin
var
sdist
develop-eggs
.installed.cfg
# Installer logs
pip-log.txt
# Unit test / coverage reports
.coverage
.tox
#Translations
*.mo
#Mr Developer
.mr.developer.cfg
# Mac crap
.DS_Store
# Visual Studio Code
.vscode/
browse.VC.db
================================================
FILE: .npmignore
================================================
## This file is modified from .gitignore from same folder
## Which allows dist folder and ignores unnecessary folders
demo
docs
#################
## Node.js
#################
node_modules
npm-debug.log
#################
## Grunt
#################
.grunt
_SpecRunner.html
reports
jsdoc
build/temp
coverage/
#################
## Eclipse
#################
*.pydevproject
.project
.metadata
bin/
tmp/
.idea/
*.tmp
*.bak
*.swp
*~.nib
local.properties
.classpath
.settings/
.loadpath
# External tool builders
.externalToolBuilders/
# Locally stored "Eclipse launch configurations"
*.launch
# CDT-specific
.cproject
# PDT-specific
.buildpath
#################
## Visual Studio
#################
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# User-specific files
*.suo
*.user
*.sln.docstates
# Build results
[Dd]ebug/
[Rr]elease/
*_i.c
*_p.c
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.vspscc
.builds
*.dotCover
## TODO: If you have NuGet Package Restore enabled, uncomment this
#packages/
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opensdf
*.sdf
# Visual Studio profiler
*.psess
*.vsp
# ReSharper is a .NET coding add-in
_ReSharper*
# Installshield output folder
[Ee]xpress
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish
# Others
[Bb]in
[Oo]bj
sql
TestResults
*.Cache
ClientBin
stylecop.*
~$*
*.dbmdl
Generated_Code #added for RIA/Silverlight projects
# Backup & report files from converting an old project file to a newer
# Visual Studio version. Backup files are not needed, because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
############
## Windows
############
# Windows image file caches
Thumbs.db
# Folder config file
Desktop.ini
#############
## Python
#############
*.py[co]
# Packages
*.egg
*.egg-info
eggs
parts
bin
var
sdist
develop-eggs
.installed.cfg
# Installer logs
pip-log.txt
# Unit test / coverage reports
.coverage
.tox
#Translations
*.mo
#Mr Developer
.mr.developer.cfg
# Mac crap
.DS_Store
# Visual Studio Code
.vscode/
browse.VC.db
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.md
================================================
flv.js [](https://www.npmjs.com/package/flv.js)
======
An HTML5 Flash Video (FLV) Player written in pure JavaScript without Flash. LONG LIVE FLV!
This project relies on [Media Source Extensions][] to work.
**For FLV live stream playback, please consider [mpegts.js][] which is under active development.**
**This project will become rarely maintained.**
[mpegts.js]: https://github.com/xqq/mpegts.js
## Overview
flv.js works by transmuxing FLV file stream into ISO BMFF (Fragmented MP4) segments, followed by feeding mp4 segments into an HTML5 `<video>` element through [Media Source Extensions][] API.
[Media Source Extensions]: https://w3c.github.io/media-source/
## Demo
[http://bilibili.github.io/flv.js/demo/](http://bilibili.github.io/flv.js/demo/)
## Features
- FLV container with H.264 + AAC / MP3 codec playback
- Multipart segmented video playback
- HTTP FLV low latency live stream playback
- FLV over WebSocket live stream playback
- Compatible with Chrome, FireFox, Safari 10, IE11 and Edge
- Extremely low overhead, and hardware accelerated by your browser!
## Installation
```bash
npm install --save flv.js
```
## Build
```bash
npm ci # install dependencies / dev-dependences
npm run build:debug # debug version flv.js will be emitted to /dist
npm run build # minimized release version flv.min.js will be emitted to /dist
```
[cnpm](https://github.com/cnpm/cnpm) mirror is recommended if you are in Mainland China.
## CORS
If you use standalone video server for FLV stream, `Access-Control-Allow-Origin` header must be configured correctly on video server for cross-origin resource fetching.
See [cors.md](docs/cors.md) for more details.
## Getting Started
```html
<script src="flv.min.js"></script>
<video id="videoElement"></video>
<script>
if (flvjs.isSupported()) {
var videoElement = document.getElementById('videoElement');
var flvPlayer = flvjs.createPlayer({
type: 'flv',
url: 'http://example.com/flv/video.flv'
});
flvPlayer.attachMediaElement(videoElement);
flvPlayer.load();
flvPlayer.play();
}
</script>
```
## Limitations
- MP3 audio codec is currently not working on IE11 / Edge
- HTTP FLV live stream is not currently working on all browsers, see [livestream.md](docs/livestream.md)
## Multipart playback
You only have to provide a playlist for `MediaDataSource`. See [multipart.md](docs/multipart.md)
## Livestream playback
See [livestream.md](docs/livestream.md)
## API and Configuration
See [api.md](docs/api.md)
## Debug
```bash
npm ci # install dependencies / dev-dependences
npm run dev # watch file changes and build debug version on the fly
```
## Design
See [design.md](docs/design.md)
## License
```
Copyright (C) 2016 Bilibili. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
```
================================================
FILE: d.ts/flv.d.ts
================================================
/*
* Copyright (C) 2016 Bilibili. All Rights Reserved.
*
* @author zheng qian <xqq@xqq.im>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// flv.js TypeScript definition file
declare namespace FlvJs {
interface MediaSegment {
duration: number;
filesize?: number;
url: string;
}
interface MediaDataSource {
type: string;
isLive?: boolean;
cors?: boolean;
withCredentials?: boolean;
hasAudio?: boolean;
hasVideo?: boolean;
duration?: number;
filesize?: number;
url?: string;
segments?: MediaSegment[];
}
interface Config {
/**
* @desc Enable separated thread for transmuxing (unstable for now)
* @defaultvalue false
*/
enableWorker?: boolean;
/**
* @desc Enable IO stash buffer. Set to false if you need realtime (minimal latency) for live stream
* playback, but may stalled if there's network jittering.
* @defaultvalue true
*/
enableStashBuffer?: boolean;
/**
* @desc Indicates IO stash buffer initial size. Default is `384KB`. Indicate a suitable size can
* improve video load/seek time.
*/
stashInitialSize?: number;
/**
* @desc Same to `isLive` in **MediaDataSource**, ignored if has been set in MediaDataSource structure.
* @defaultvalue false
*/
isLive?: boolean;
/**
* @desc Abort the http connection if there's enough data for playback.
* @defaultvalue true
*/
lazyLoad?: boolean;
/**
* @desc Indicates how many seconds of data to be kept for `lazyLoad`.
* @defaultvalue 3 * 60
*/
lazyLoadMaxDuration?: number;
/**
* @desc Indicates the `lazyLoad` recover time boundary in seconds.
* @defaultvalue 30
*/
lazyLoadRecoverDuration?: number;
/**
* @desc Do load after MediaSource `sourceopen` event triggered. On Chrome, tabs which
* be opened in background may not trigger `sourceopen` event until switched to that tab.
* @defaultvalue true
*/
deferLoadAfterSourceOpen?: boolean;
/**
* @desc Do auto cleanup for SourceBuffer
* @defaultvalue false (from docs)
*/
autoCleanupSourceBuffer?: boolean;
/**
* @desc When backward buffer duration exceeded this value (in seconds), do auto cleanup for SourceBuffer
* @defaultvalue 3 * 60
*/
autoCleanupMaxBackwardDuration?: number;
/**
* @desc Indicates the duration in seconds to reserve for backward buffer when doing auto cleanup.
* @defaultvalue 2 * 60
*/
autoCleanupMinBackwardDuration?: number;
/**
* @defaultvalue 600
*/
statisticsInfoReportInterval?: number;
/**
* @desc Fill silent audio frames to avoid a/v unsync when detect large audio timestamp gap.
* @defaultvalue true
*/
fixAudioTimestampGap?: boolean;
/**
* @desc Accurate seek to any frame, not limited to video IDR frame, but may a bit slower.
* Available on Chrome > 50, FireFox and Safari.
* @defaultvalue false
*/
accurateSeek?: boolean;
/**
* @desc 'range' use range request to seek, or 'param' add params into url to indicate request range.
* @defaultvalue 'range'
*/
seekType?: 'range' | 'param' | 'custom';
/**
* @desc Indicates seek start parameter name for seekType = 'param'
* @defaultvalue 'bstart'
*/
seekParamStart?: string;
/**
* @desc Indicates seek end parameter name for seekType = 'param'
* @defaultvalue 'bend'
*/
seekParamEnd?: string;
/**
* @desc Send Range: bytes=0- for first time load if use Range seek
* @defaultvalue false
*/
rangeLoadZeroStart?: boolean;
/**
* @desc Indicates a custom seek handler
* @desc Should implement `SeekHandler` interface
*/
customSeekHandler?: CustomSeekHandlerConstructor;
/**
* @desc Reuse 301/302 redirected url for subsequence request like seek, reconnect, etc.
* @defaultvalue false
*/
reuseRedirectedURL?: boolean;
/**
* @desc Indicates the Referrer Policy when using FetchStreamLoader
* @defaultvalue 'no-referrer-when-downgrade' (from docs)
*/
referrerPolicy?: ReferrerPolicy;
/**
* @desc Indicates additional headers that will be added to request
*/
headers?: {
[k: string]: string
}
/**
* @desc Should implement `BaseLoader` interface
*/
customLoader?: CustomLoaderConstructor;
}
interface CustomSeekHandlerConstructor {
new(): SeekHandler;
}
interface SeekHandler {
getConfig(sourceURL: string, range: Range): SeekConfig;
removeURLParameters(url: string): string;
}
interface SeekConfig {
url: string;
headers: Headers | object;
}
interface BaseLoaderConstructor {
new(typeName: string): BaseLoader;
}
interface BaseLoader {
_status: number;
_needStash: boolean;
destroy(): void;
isWorking(): boolean;
readonly type: string;
readonly status: number;
readonly needStashBuffer: boolean;
onContentLengthKnown: (contentLength: number) => void;
onURLRedirect: (redirectedURL: string) => void;
onDataArrival: (chunk: ArrayBuffer, byteStart: number, receivedLength?: number) => void;
onError: (errorType: LoaderErrors, errorInfo: LoaderErrorMessage) => void;
onComplete: (rangeFrom: number, rangeTo: number) => void;
open(dataSource: MediaSegment, range: Range): void;
abort(): void;
}
interface CustomLoaderConstructor {
new(seekHandler: SeekHandler, config: Config): BaseLoader;
}
interface Range {
from: number;
to: number;
}
interface LoaderStatus {
readonly kIdle: 0;
readonly kConnecting: 1;
readonly kBuffering: 2;
readonly kError: 3;
readonly kComplete: 4;
}
interface LoaderErrors {
readonly OK: 'OK';
readonly EXCEPTION: 'Exception';
readonly HTTP_STATUS_CODE_INVALID: 'HttpStatusCodeInvalid';
readonly CONNECTING_TIMEOUT: 'ConnectingTimeout';
readonly EARLY_EOF: 'EarlyEof';
readonly UNRECOVERABLE_EARLY_EOF: 'UnrecoverableEarlyEof';
}
interface LoaderErrorMessage {
code: number;
msg: string;
}
interface FeatureList {
mseFlvPlayback: boolean;
mseLiveFlvPlayback: boolean;
networkStreamIO: boolean;
networkLoaderName: string;
nativeMP4H264Playback: boolean;
nativeWebmVP8Playback: boolean;
nativeWebmVP9Playback: boolean;
}
interface PlayerConstructor {
new (mediaDataSource: MediaDataSource, config?: Config): Player;
}
interface Player {
destroy(): void;
on(event: string, listener: (...args: any[]) => void): void;
off(event: string, listener: (...args: any[]) => void): void;
attachMediaElement(mediaElement: HTMLMediaElement): void;
detachMediaElement(): void;
load(): void;
unload(): void;
play(): Promise<void> | void;
pause(): void;
type: string;
buffered: TimeRanges;
duration: number;
volume: number;
muted: boolean;
currentTime: number;
/**
* @deprecated FlvPlayer/NativePlayer have its own `mediaInfo` field.
* @desc Keep it for backwards compatibility
* @since 1.4
*/
mediaInfo: NativePlayerMediaInfo | FlvPlayerMediaInfo;
/**
* @deprecated FlvPlayer/NativePlayer have its own `statisticsInfo` field.
* @desc Keep it for backwards compatibility
* @since 1.4
*/
statisticsInfo: NativePlayerStatisticsInfo | FlvPlayerStatisticsInfo;
}
interface NativePlayerStatisticsInfo {
playerType: 'NativePlayer';
url: string;
decodedFrames?: number;
droppedFrames?: number;
}
interface FlvPlayerReportStatisticsInfo {
url: string;
hasRedirect: boolean;
redirectedURL?: string;
speed: number; // KB/s
loaderType: string;
currentSegmentIndex: number;
totalSegmentCount: number;
}
interface FlvPlayerStatisticsInfo extends Partial<FlvPlayerReportStatisticsInfo> {
playerType: 'FlvPlayer';
decodedFrames?: number;
droppedFrames?: number;
}
interface NativePlayerMediaInfo {
mimeType: string;
duration?: number;
width?: number;
height?: number;
}
interface FlvPlayerMediaInfo extends NativePlayerMediaInfo {
audioCodec?: string;
videoCodec?: string;
audioDataRate?: number;
videoDataRate?: number;
hasAudio?: boolean;
hasVideo?: boolean;
chromaFormat?: string;
fps?: number;
[k: string]: any;
}
interface FlvPlayer extends Player {
mediaInfo: FlvPlayerMediaInfo;
statisticsInfo: FlvPlayerStatisticsInfo;
}
interface NativePlayer extends Player {
mediaInfo: NativePlayerMediaInfo;
statisticsInfo: NativePlayerStatisticsInfo;
}
interface LoggingControlConfig {
forceGlobalTag: boolean;
globalTag: string;
enableAll: boolean;
enableDebug: boolean;
enableVerbose: boolean;
enableInfo: boolean;
enableWarn: boolean;
enableError: boolean;
}
interface LoggingControl extends LoggingControlConfig {
getConfig(): LoggingControlConfig;
applyConfig(config: Partial<LoggingControlConfig>): void;
addLogListener(listener: (...args: any[]) => void): void;
removeLogListener(listener: (...args: any[]) => void): void;
}
interface Events {
ERROR: string;
LOADING_COMPLETE: string;
RECOVERED_EARLY_EOF: string;
MEDIA_INFO: string;
METADATA_ARRIVED: string;
SCRIPTDATA_ARRIVED: string;
STATISTICS_INFO: string;
}
interface ErrorTypes {
NETWORK_ERROR: string;
MEDIA_ERROR: string;
OTHER_ERROR: string;
}
interface ErrorDetails {
NETWORK_EXCEPTION: string;
NETWORK_STATUS_CODE_INVALID: string;
NETWORK_TIMEOUT: string;
NETWORK_UNRECOVERABLE_EARLY_EOF: string;
MEDIA_MSE_ERROR: string;
MEDIA_FORMAT_ERROR: string;
MEDIA_FORMAT_UNSUPPORTED: string;
MEDIA_CODEC_UNSUPPORTED: string;
}
}
declare var FlvJs: {
createPlayer(mediaDataSource: FlvJs.MediaDataSource, config?: FlvJs.Config): FlvJs.Player;
isSupported(): boolean;
getFeatureList(): FlvJs.FeatureList;
/**
* @deprecated Use `FlvJs.BaseLoaderConstructor` instead.
* Because it's not available on `flvjs` variable.
* @desc implement interface `BaseLoader`
* @since 1.4
*/
BaseLoader: FlvJs.BaseLoaderConstructor;
/**
* @deprecated Use `FlvJs.BaseLoaderConstructor` instead.
* Because it's not available on `flvjs` variable.
* @since 1.4
*/
LoaderStatus: FlvJs.LoaderStatus;
/**
* @deprecated Use `FlvJs.BaseLoaderConstructor` instead.
* Because it's not available on `flvjs` variable.
* @since 1.4
*/
LoaderErrors: FlvJs.LoaderErrors;
readonly version: string;
readonly Events: Readonly<FlvJs.Events>;
readonly ErrorTypes: Readonly<FlvJs.ErrorTypes>;
readonly ErrorDetails: Readonly<FlvJs.ErrorDetails>;
readonly FlvPlayer: FlvJs.PlayerConstructor;
readonly NativePlayer: FlvJs.PlayerConstructor;
readonly LoggingControl: FlvJs.LoggingControl;
};
export default FlvJs;
================================================
FILE: demo/demo.css
================================================
.mainContainer {
display: block;
width: 100%;
margin-left: auto;
margin-right: auto;
}
@media screen and (min-width: 1152px) {
.mainContainer {
display: block;
width: 1152px;
margin-left: auto;
margin-right: auto;
}
}
.video-container {
position: relative;
margin-top: 8px;
}
.video-container:before {
display: block;
content: "";
width: 100%;
padding-bottom: 56.25%;
}
.video-container > div {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.video-container video {
width: 100%;
height: 100%;
}
.urlInput {
display: block;
width: 100%;
margin-left: auto;
margin-right: auto;
margin-top: 8px;
margin-bottom: 8px;
}
.centeredVideo {
display: block;
width: 100%;
height: 100%;
margin-left: auto;
margin-right: auto;
margin-bottom: auto;
}
.controls {
display: block;
width: 100%;
text-align: left;
margin-left: auto;
margin-right: auto;
margin-top: 8px;
margin-bottom: 10px;
}
.logcatBox {
border-color: #CCCCCC;
font-size: 11px;
font-family: Menlo, Consolas, monospace;
display: block;
width: 100%;
text-align: left;
margin-left: auto;
margin-right: auto;
}
.url-input , .options {
font-size: 13px;
}
.url-input {
display: flex;
}
.url-input label {
flex: initial;
}
.url-input input {
flex: auto;
margin-left: 8px;
}
.url-input button {
flex: initial;
margin-left: 8px;
}
.options {
margin-top: 5px;
}
.hidden {
display: none;
}
================================================
FILE: demo/index.html
================================================
<!DOCTYPE html>
<html>
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<title>flv.js demo</title>
<link rel="stylesheet" type="text/css" href="demo.css" />
</head>
<body>
<div class="mainContainer">
<div>
<div id="streamURL">
<div class="url-input">
<label for="sURL">Stream URL:</label>
<input id="sURL" type="text" value="http://127.0.0.1/flv/7182741-1.flv" />
<button onclick="switch_mds()">Switch to MediaDataSource</button>
</div>
<div class="options">
<input type="checkbox" id="isLive" onchange="saveSettings()" />
<label for="isLive">isLive</label>
<input type="checkbox" id="withCredentials" onchange="saveSettings()" />
<label for="withCredentials">withCredentials</label>
<input type="checkbox" id="hasAudio" onchange="saveSettings()" checked />
<label for="hasAudio">hasAudio</label>
<input type="checkbox" id="hasVideo" onchange="saveSettings()" checked />
<label for="hasVideo">hasVideo</label>
</div>
</div>
<div id="mediaSourceURL" class="hidden">
<div class="url-input">
<label for="msURL">MediaDataSource JsonURL:</label>
<input id="msURL" type="text" value="http://127.0.0.1/flv/7182741.json" />
<button onclick="switch_url()">Switch to URL</button>
</div>
</div>
</div>
<div class="video-container">
<div>
<video name="videoElement" class="centeredVideo" controls autoplay>
Your browser is too old which doesn't support HTML5 video.
</video>
</div>
</div>
<div class="controls">
<button onclick="flv_load()">Load</button>
<button onclick="flv_start()">Start</button>
<button onclick="flv_pause()">Pause</button>
<button onclick="flv_destroy()">Destroy</button>
<input style="width:100px" type="text" name="seekpoint"/>
<button onclick="flv_seekto()">SeekTo</button>
</div>
<textarea name="logcatbox" class="logcatBox" rows="10" readonly></textarea>
</div>
<script src="../dist/flv.js"></script>
<script>
var checkBoxFields = ['isLive', 'withCredentials', 'hasAudio', 'hasVideo'];
var streamURL, mediaSourceURL;
function flv_load() {
console.log('isSupported: ' + flvjs.isSupported());
if (mediaSourceURL.className === '') {
var url = document.getElementById('msURL').value;
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onload = function (e) {
var mediaDataSource = JSON.parse(xhr.response);
flv_load_mds(mediaDataSource);
}
xhr.send();
} else {
var i;
var mediaDataSource = {
type: 'flv'
};
for (i = 0; i < checkBoxFields.length; i++) {
var field = checkBoxFields[i];
/** @type {HTMLInputElement} */
var checkbox = document.getElementById(field);
mediaDataSource[field] = checkbox.checked;
}
mediaDataSource['url'] = document.getElementById('sURL').value;
console.log('MediaDataSource', mediaDataSource);
flv_load_mds(mediaDataSource);
}
}
function flv_load_mds(mediaDataSource) {
var element = document.getElementsByName('videoElement')[0];
if (typeof player !== "undefined") {
if (player != null) {
player.unload();
player.detachMediaElement();
player.destroy();
player = null;
}
}
player = flvjs.createPlayer(mediaDataSource, {
enableWorker: false,
lazyLoadMaxDuration: 3 * 60,
seekType: 'range',
});
player.attachMediaElement(element);
player.load();
}
function flv_start() {
player.play();
}
function flv_pause() {
player.pause();
}
function flv_destroy() {
player.pause();
player.unload();
player.detachMediaElement();
player.destroy();
player = null;
}
function flv_seekto() {
var input = document.getElementsByName('seekpoint')[0];
player.currentTime = parseFloat(input.value);
}
function switch_url() {
streamURL.className = '';
mediaSourceURL.className = 'hidden';
saveSettings();
}
function switch_mds() {
streamURL.className = 'hidden';
mediaSourceURL.className = '';
saveSettings();
}
function ls_get(key, def) {
try {
var ret = localStorage.getItem('flvjs_demo.' + key);
if (ret === null) {
ret = def;
}
return ret;
} catch (e) {}
return def;
}
function ls_set(key, value) {
try {
localStorage.setItem('flvjs_demo.' + key, value);
} catch (e) {}
}
function saveSettings() {
if (mediaSourceURL.className === '') {
ls_set('inputMode', 'MediaDataSource');
} else {
ls_set('inputMode', 'StreamURL');
}
var i;
for (i = 0; i < checkBoxFields.length; i++) {
var field = checkBoxFields[i];
/** @type {HTMLInputElement} */
var checkbox = document.getElementById(field);
ls_set(field, checkbox.checked ? '1' : '0');
}
var msURL = document.getElementById('msURL');
var sURL = document.getElementById('sURL');
ls_set('msURL', msURL.value);
ls_set('sURL', sURL.value);
console.log('save');
}
function loadSettings() {
var i;
for (i = 0; i < checkBoxFields.length; i++) {
var field = checkBoxFields[i];
/** @type {HTMLInputElement} */
var checkbox = document.getElementById(field);
var c = ls_get(field, checkbox.checked ? '1' : '0');
checkbox.checked = c === '1' ? true : false;
}
var msURL = document.getElementById('msURL');
var sURL = document.getElementById('sURL');
msURL.value = ls_get('msURL', msURL.value);
sURL.value = ls_get('sURL', sURL.value);
if (ls_get('inputMode', 'StreamURL') === 'StreamURL') {
switch_url();
} else {
switch_mds();
}
}
function showVersion() {
var version = flvjs.version;
document.title = document.title + " (v" + version + ")";
}
var logcatbox = document.getElementsByName('logcatbox')[0];
flvjs.LoggingControl.addLogListener(function(type, str) {
logcatbox.value = logcatbox.value + str + '\n';
logcatbox.scrollTop = logcatbox.scrollHeight;
});
document.addEventListener('DOMContentLoaded', function () {
streamURL = document.getElementById('streamURL');
mediaSourceURL = document.getElementById('mediaSourceURL');
loadSettings();
showVersion();
flv_load();
});
</script>
</body>
</html>
================================================
FILE: docs/api.md
================================================
flv.js API
==========
This document use TypeScript-like definitions to describe interfaces.
## Interfaces
flv.js exports all the interfaces through `flvjs` object which exposed in global context `window`.
`flvjs` object can also be accessed by require or ES6 import.
Functions:
- [flvjs.createPlayer()](#flvjscreateplayer)
- [flvjs.isSupported()](#flvjsissupported)
- [flvjs.getFeatureList()](#flvjsgetfeaturelist)
Classes:
- [flvjs.FlvPlayer](#flvjsflvplayer)
- [flvjs.NativePlayer](#flvjsnativeplayer)
- [flvjs.LoggingControl](#flvjsloggingcontrol)
Enums:
- [flvjs.Events](#flvjsevents)
- [flvjs.ErrorTypes](#flvjserrortypes)
- [flvjs.ErrorDetails](#flvjserrordetails)
### flvjs.createPlayer()
```js
function createPlayer(mediaDataSource: MediaDataSource, config?: Config): Player;
```
Create a player instance according to `type` field indicated in `mediaDataSource`, with optional `config`.
### MediaDataSource
| Field | Type | Description |
| ------------------ | --------------------- | ---------------------------------------- |
| `type` | `string` | Indicates media type, `'flv'` or `'mp4'` |
| `isLive?` | `boolean` | Indicates whether the data source is a **live stream** |
| `cors?` | `boolean` | Indicates whether to enable CORS for http fetching |
| `withCredentials?` | `boolean` | Indicates whether to do http fetching with cookies |
| `hasAudio?` | `boolean` | Indicates whether the stream has audio track |
| `hasVideo?` | `boolean` | Indicates whether the stream has video track |
| `duration?` | `number` | Indicates total media duration, in **milliseconds** |
| `filesize?` | `number` | Indicates total file size of media file, in bytes |
| `url?` | `string` | Indicates media URL, can be starts with `'https(s)'` or `'ws(s)'` (WebSocket) |
| `segments?` | `Array<MediaSegment>` | Optional field for multipart playback, see **MediaSegment** |
If `segments` field exists, transmuxer will treat this `MediaDataSource` as a **multipart** source.
In multipart mode, `duration` `filesize` `url` field in `MediaDataSource` structure will be ignored.
### MediaSegment
| Field | Type | Description |
| ----------- | -------- | ---------------------------------------- |
| `duration` | `number` | Required field, indicates segment duration in **milliseconds** |
| `filesize?` | `number` | Optional field, indicates segment file size in bytes |
| `url` | `string` | Required field, indicates segment file URL |
### Config
| Field | Type | Default | Description |
| -------------------------------- | --------- | ---------------------------- | ---------------------------------------- |
| `enableWorker?` | `boolean` | `false` | Enable separated thread for transmuxing (unstable for now) |
| `enableStashBuffer?` | `boolean` | `true` | Enable IO stash buffer. Set to false if you need realtime (minimal latency) for live stream playback, but may stalled if there's network jittering. |
| `stashInitialSize?` | `number` | `384KB` | Indicates IO stash buffer initial size. Default is `384KB`. Indicate a suitable size can improve video load/seek time. |
| `isLive?` | `boolean` | `false` | Same to `isLive` in **MediaDataSource**, ignored if has been set in MediaDataSource structure. |
| `lazyLoad?` | `boolean` | `true` | Abort the http connection if there's enough data for playback. |
| `lazyLoadMaxDuration?` | `number` | `3 * 60` | Indicates how many seconds of data to be kept for `lazyLoad`. |
| `lazyLoadRecoverDuration?` | `number` | `30` | Indicates the `lazyLoad` recover time boundary in seconds. |
| `deferLoadAfterSourceOpen?` | `boolean` | `true` | Do load after MediaSource `sourceopen` event triggered. On Chrome, tabs which be opened in background may not trigger `sourceopen` event until switched to that tab. |
| `autoCleanupSourceBuffer` | `boolean` | `false` | Do auto cleanup for SourceBuffer |
| `autoCleanupMaxBackwardDuration` | `number` | `3 * 60` | When backward buffer duration exceeded this value (in seconds), do auto cleanup for SourceBuffer |
| `autoCleanupMinBackwardDuration` | `number` | `2 * 60` | Indicates the duration in seconds to reserve for backward buffer when doing auto cleanup. |
| `fixAudioTimestampGap` | `boolean` | `true` | Fill silent audio frames to avoid a/v unsync when detect large audio timestamp gap. |
| `accurateSeek?` | `boolean` | `false` | Accurate seek to any frame, not limited to video IDR frame, but may a bit slower. Available on `Chrome > 50`, `FireFox` and `Safari`. |
| `seekType?` | `string` | `'range'` | `'range'` use range request to seek, or `'param'` add params into url to indicate request range. |
| `seekParamStart?` | `string` | `'bstart'` | Indicates seek start parameter name for `seekType = 'param'` |
| `seekParamEnd?` | `string` | `'bend'` | Indicates seek end parameter name for `seekType = 'param'` |
| `rangeLoadZeroStart?` | `boolean` | `false` | Send `Range: bytes=0-` for first time load if use Range seek |
| `customSeekHandler?` | `object` | `undefined` | Indicates a custom seek handler |
| `reuseRedirectedURL?` | `boolean` | `false` | Reuse 301/302 redirected url for subsequence request like seek, reconnect, etc. |
| `referrerPolicy?` | `string` | `no-referrer-when-downgrade` | Indicates the [Referrer Policy][] when using FetchStreamLoader |
| `headers?` | `object` | `undefined` | Indicates additional headers that will be added to request |
[Referrer Policy]: https://w3c.github.io/webappsec-referrer-policy/#referrer-policy
### flvjs.isSupported()
```js
function isSupported(): boolean;
```
Return `true` if basic playback can works on your browser.
### flvjs.getFeatureList()
```js
function getFeatureList(): FeatureList;
```
Return a `FeatureList` object which has following details:
#### FeatureList
| Field | Type | Description |
| ----------------------- | --------- | ---------------------------------------- |
| `mseFlvPlayback` | `boolean` | Same to `flvjs.isSupported()`, indicates whether basic playback works on your browser. |
| `mseLiveFlvPlayback` | `boolean` | Indicates whether HTTP FLV live stream can works on your browser. |
| `networkStreamIO` | `boolean` | Indicates whether the network loader is streaming. |
| `networkLoaderName` | `string` | Indicates the network loader type name. |
| `nativeMP4H264Playback` | `boolean` | Indicates whether your browser support H.264 MP4 video file natively. |
| `nativeWebmVP8Playback` | `boolean` | Indicates whether your browser support WebM VP8 video file natively. |
| `nativeWebmVP9Playback` | `boolean` | Indicates whether your browser support WebM VP9 video file natively. |
### flvjs.FlvPlayer
```typescript
interface FlvPlayer extends Player {}
```
FLV player which implements the `Player` interface. Can be created by `new` operator directly.
### flvjs.NativePlayer
```typescript
interface NativePlayer extends Player {}
```
Player wrapper for browser's native player (HTMLVideoElement) without MediaSource src, which implements the `Player` interface. Useful for singlepart **MP4** file playback.
### interface Player (abstract)
```typescript
interface Player {
constructor(mediaDataSource: MediaDataSource, config?: Config): Player;
destroy(): void;
on(event: string, listener: Function): void;
off(event: string, listener: Function): void;
attachMediaElement(mediaElement: HTMLMediaElement): void;
detachMediaElement(): void;
load(): void;
unload(): void;
play(): Promise<void>;
pause(): void;
type: string;
buffered: TimeRanges;
duration: number;
volume: number;
muted: boolean;
currentTime: number;
mediaInfo: Object;
statisticsInfo: Object;
}
```
### flvjs.LoggingControl
A global interface which include several static getter/setter to set flv.js logcat verbose level.
```typescript
interface LoggingControl {
forceGlobalTag: boolean;
globalTag: string;
enableAll: boolean;
enableDebug: boolean;
enableVerbose: boolean;
enableInfo: boolean;
enableWarn: boolean;
enableError: boolean;
getConfig(): Object;
applyConfig(config: Object): void;
addLogListener(listener: Function): void;
removeLogListener(listener: Function): void;
}
```
### flvjs.Events
A series of constants that can be used with `Player.on()` / `Player.off()`. They require the prefix `flvjs.Events`.
| Event | Description |
| ------------------- | ---------------------------------------- |
| ERROR | An error occurred by any cause during the playback |
| LOADING_COMPLETE | The input MediaDataSource has been completely buffered to end |
| RECOVERED_EARLY_EOF | An unexpected network EOF occurred during buffering but automatically recovered |
| MEDIA_INFO | Provides technical information of the media like video/audio codec, bitrate, etc. |
| METADATA_ARRIVED | Provides metadata which FLV file(stream) can contain with an "onMetaData" marker. |
| SCRIPTDATA_ARRIVED | Provides scriptdata (OnCuePoint / OnTextData) which FLV file(stream) can contain. |
| STATISTICS_INFO | Provides playback statistics information like dropped frames, current speed, etc. |
### flvjs.ErrorTypes
The possible errors that can come up during playback. They require the prefix `flvjs.ErrorTypes`.
| Error | Description |
| ------------- | ---------------------------------------- |
| NETWORK_ERROR | Errors related to the network |
| MEDIA_ERROR | Errors related to the media (format error, decode issue, etc) |
| OTHER_ERROR | Any other unspecified error |
### flvjs.ErrorDetails
Provide more verbose explanation for Network and Media errors. They require the prefix `flvjs.ErrorDetails`.
| Error | Description |
| ------------------------------- | ---------------------------------------- |
| NETWORK_EXCEPTION | Related to any other issues with the network; contains a `message` |
| NETWORK_STATUS_CODE_INVALID | Related to an invalid HTTP status code, such as 403, 404, etc. |
| NETWORK_TIMEOUT | Related to timeout request issues |
| NETWORK_UNRECOVERABLE_EARLY_EOF | Related to unexpected network EOF which cannot be recovered |
| MEDIA_MSE_ERROR | Related to MediaSource's error such as decode issue |
| MEDIA_FORMAT_ERROR | Related to any invalid parameters in the media stream |
| MEDIA_FORMAT_UNSUPPORTED | The input MediaDataSource format is not supported by flv.js |
| MEDIA_CODEC_UNSUPPORTED | The media stream contains video/audio codec which is not supported |
================================================
FILE: docs/cors.md
================================================
CORS Configuration
==================
Anytime you want to play an FLV stream from another `Origin`, the server must response with a CORS header:
```
Access-Control-Allow-Origin: <your-origin> | *
```
For example, if an html on your site `http://flvplayback.com` want's to play an FLV from another `Origin` like `http://cdn.flvplayback.com`, the video server must response with the following CORS header:
```
Access-Control-Allow-Origin: http://flvplayback.com
```
Or a wildcard value `*` to allow any request origin:
```
Access-Control-Allow-Origin: *
```
## Static FLV file playback
For static FLV file playback, we recommend you to add:
```
Access-Control-Expose-Headers: Content-Length
```
Or you should provide accurate filesize in **MediaDataSource** object.
## CORS with 301/302 redirect
If your video server response with a 3xx redirection, the redirection's response headers **must** contains `Access-Control-Allow-Origin`;
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.
It means that your actual edge server should response with:
```
Access-Control-Allow-Origin: null | *
```
Or you can determine by request header `Origin` dynamically.
## Preflight OPTIONS for Range seek
When use Range seek for cross-origin FLV file, `Range` header added by flv.js will cause a [Preflight OPTIONS][] request by the browser.
The browser will send an `OPTIONS` request before actual `GET` request, with following additional headers according to CORS policy:
```
Access-Control-Request-Headers: range
Access-Control-Request-Method: GET
```
This means your video server must response to OPTIONS request with following additional CORS headers:
```
Access-Control-Allow-Origin: <your-origin> | *
Access-Control-Allow-Methods: GET, OPTIONS
Access-Control-Allow-Headers: range
```
[Preflight OPTIONS]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Preflighted_requests
## Reference
We strongly advise you to read [HTTP access control (CORS)][] and [CORS spec][] document carefully.
[HTTP access control (CORS)]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS
[CORS spec]: https://www.w3.org/TR/cors/
================================================
FILE: docs/design.md
================================================
flv.js design
======
Architecture overview:

================================================
FILE: docs/livestream.md
================================================
Livestream playback
===================
You need to provide a livestream URL in `MediaDataSource` and indicates `isLive: true`.
Sample HTTP FLV source:
```js
{
// HTTP FLV
"type": "flv",
"isLive": true,
"url": "http://127.0.0.1:8080/live/livestream.flv"
}
```
Or a WebSocket source:
```js
{
// FLV over WebSocket
"type": "flv",
"isLive": true,
"url": "ws://127.0.0.1:9090/live/livestream.flv"
}
```
## HTTP FLV live stream
### CORS
You must configure `Access-Control-Allow-Origin` header correctly on your stream server.
See [cors.md](../docs/cors.md) for details.
### Compatibility
Due to IO restrictions, flv.js can support HTTP FLV live stream on `Chrome 43+`, `FireFox 42+`, `Edge 15.15048+` and `Safari 10.1+` for now.
HTTP 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:
- Chrome: `FetchStreamLoader` works well on Chrome 43+
- FireFox: FireFox has `fetch` support but `stream` is missing, `moz-chunked-arraybuffer` xhr extension is used
- 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).
- Safari: `FetchStreamLoader` works well since Safari 10.1 (macOS 10.12.4)
[fetch]: https://fetch.spec.whatwg.org/
[stream]: https://streams.spec.whatwg.org/
[Fetch API with ReadableStream has bug with data pumping]: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/8196907/
[Safari Technology Preview]: https://developer.apple.com/safari/technology-preview/
================================================
FILE: docs/multipart.md
================================================
Multipart playback
==================
When you create FlvPlayer instance, the `MediaDataSource` structure is passing through the constructor.
You need to provide a playlist for `MediaDataSource` in following format:
```js
{
// Required
"type": "flv", // Only flv type supports multipart playback
// Optional
"duration": 12345678, // total duration, in milliseconds
"cors": true,
"withCredentials": false,
// Optional
// true by default, do not indicate unless you have to deal with audio-only or video-only stream
"hasAudio": true,
"hasVideo": true,
// Required
"segments": [
{
"duration": 1234, // in milliseconds
"filesize": 5678, // in bytes
"url": "http://cdn.flvplayback.com/segments-1.flv"
},
{
"duration": 2345,
"filesize": 6789,
"url": "http://cdn.flvplayback.com/segments-2.flv"
},
{
"duration": 4567,
"filesize": 7890,
"url": "http://cdn.flvplayback.com/segments-3.flv"
}
// more segments...
]
}
```
You must provide **accurate** duration for each segment.
## Sample input
```json
{
"type": "flv",
"duration": 1373161,
"segments": [
{
"duration": 333438,
"filesize": 60369190,
"url": "http://127.0.0.1/flv/7182741-1.flv"
},{
"duration": 390828,
"filesize": 75726439,
"url": "http://127.0.0.1/flv/7182741-2.flv"
},{
"duration": 434453,
"filesize": 103453988,
"url": "http://127.0.0.1/flv/7182741-3.flv"
},{
"duration": 214442,
"filesize": 44189200,
"url": "http://127.0.0.1/flv/7182741-4.flv"
}
]
}
```
================================================
FILE: package.json
================================================
{
"name": "flv.js",
"version": "1.6.2",
"description": "HTML5 FLV Player",
"main": "./dist/flv.js",
"types": "./d.ts/flv.d.ts",
"repository": {
"type": "git",
"url": "https://github.com/Bilibili/flv.js"
},
"keywords": [
"html5",
"flv",
"mse",
"javascript"
],
"scripts": {
"build": "webpack --mode=production --progress",
"build:debug": "webpack --mode=development --progress",
"dev": "webpack --mode=development --progress --watch",
"dtslint": "dtslint types"
},
"dependencies": {
"es6-promise": "^4.2.8",
"webworkify-webpack": "^2.1.5"
},
"devDependencies": {
"@types/node": "^16.3.3",
"@typescript-eslint/eslint-plugin": "^4.28.3",
"@typescript-eslint/parser": "^4.28.3",
"browser-sync": "^2.27.4",
"eslint": "^7.30.0",
"exports-loader": "^3.0.0",
"source-map-loader": "^3.0.0",
"terser-webpack-plugin": "^5.1.4",
"ts-loader": "^9.2.3",
"typescript": "^4.3.5",
"webpack": "^5.45.1",
"webpack-cli": "^4.7.2"
},
"author": "zheng qian <xqq@xqq.im>",
"license": "Apache-2.0"
}
================================================
FILE: src/config.js
================================================
/*
* Copyright (C) 2016 Bilibili. All Rights Reserved.
*
* @author zheng qian <xqq@xqq.im>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const defaultConfig = {
enableWorker: false,
enableStashBuffer: true,
stashInitialSize: undefined,
isLive: false,
lazyLoad: true,
lazyLoadMaxDuration: 3 * 60,
lazyLoadRecoverDuration: 30,
deferLoadAfterSourceOpen: true,
// autoCleanupSourceBuffer: default as false, leave unspecified
autoCleanupMaxBackwardDuration: 3 * 60,
autoCleanupMinBackwardDuration: 2 * 60,
statisticsInfoReportInterval: 600,
fixAudioTimestampGap: true,
accurateSeek: false,
seekType: 'range', // [range, param, custom]
seekParamStart: 'bstart',
seekParamEnd: 'bend',
rangeLoadZeroStart: false,
customSeekHandler: undefined,
reuseRedirectedURL: false,
// referrerPolicy: leave as unspecified
headers: undefined,
customLoader: undefined
};
export function createDefaultConfig() {
return Object.assign({}, defaultConfig);
}
================================================
FILE: src/core/features.js
================================================
/*
* Copyright (C) 2016 Bilibili. All Rights Reserved.
*
* @author zheng qian <xqq@xqq.im>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import IOController from '../io/io-controller.js';
import {createDefaultConfig} from '../config.js';
class Features {
static supportMSEH264Playback() {
return window.MediaSource &&
window.MediaSource.isTypeSupported('video/mp4; codecs="avc1.42E01E,mp4a.40.2"');
}
static supportNetworkStreamIO() {
let ioctl = new IOController({}, createDefaultConfig());
let loaderType = ioctl.loaderType;
ioctl.destroy();
return loaderType == 'fetch-stream-loader' || loaderType == 'xhr-moz-chunked-loader';
}
static getNetworkLoaderTypeName() {
let ioctl = new IOController({}, createDefaultConfig());
let loaderType = ioctl.loaderType;
ioctl.destroy();
return loaderType;
}
static supportNativeMediaPlayback(mimeType) {
if (Features.videoElement == undefined) {
Features.videoElement = window.document.createElement('video');
}
let canPlay = Features.videoElement.canPlayType(mimeType);
return canPlay === 'probably' || canPlay == 'maybe';
}
static getFeatureList() {
let features = {
mseFlvPlayback: false,
mseLiveFlvPlayback: false,
networkStreamIO: false,
networkLoaderName: '',
nativeMP4H264Playback: false,
nativeWebmVP8Playback: false,
nativeWebmVP9Playback: false
};
features.mseFlvPlayback = Features.supportMSEH264Playback();
features.networkStreamIO = Features.supportNetworkStreamIO();
features.networkLoaderName = Features.getNetworkLoaderTypeName();
features.mseLiveFlvPlayback = features.mseFlvPlayback && features.networkStreamIO;
features.nativeMP4H264Playback = Features.supportNativeMediaPlayback('video/mp4; codecs="avc1.42001E, mp4a.40.2"');
features.nativeWebmVP8Playback = Features.supportNativeMediaPlayback('video/webm; codecs="vp8.0, vorbis"');
features.nativeWebmVP9Playback = Features.supportNativeMediaPlayback('video/webm; codecs="vp9"');
return features;
}
}
export default Features;
================================================
FILE: src/core/media-info.js
================================================
/*
* Copyright (C) 2016 Bilibili. All Rights Reserved.
*
* @author zheng qian <xqq@xqq.im>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class MediaInfo {
constructor() {
this.mimeType = null;
this.duration = null;
this.hasAudio = null;
this.hasVideo = null;
this.audioCodec = null;
this.videoCodec = null;
this.audioDataRate = null;
this.videoDataRate = null;
this.audioSampleRate = null;
this.audioChannelCount = null;
this.width = null;
this.height = null;
this.fps = null;
this.profile = null;
this.level = null;
this.refFrames = null;
this.chromaFormat = null;
this.sarNum = null;
this.sarDen = null;
this.metadata = null;
this.segments = null; // MediaInfo[]
this.segmentCount = null;
this.hasKeyframesIndex = null;
this.keyframesIndex = null;
}
isComplete() {
let audioInfoComplete = (this.hasAudio === false) ||
(this.hasAudio === true &&
this.audioCodec != null &&
this.audioSampleRate != null &&
this.audioChannelCount != null);
let videoInfoComplete = (this.hasVideo === false) ||
(this.hasVideo === true &&
this.videoCodec != null &&
this.width != null &&
this.height != null &&
this.fps != null &&
this.profile != null &&
this.level != null &&
this.refFrames != null &&
this.chromaFormat != null &&
this.sarNum != null &&
this.sarDen != null);
// keyframesIndex may not be present
return this.mimeType != null &&
this.duration != null &&
this.metadata != null &&
this.hasKeyframesIndex != null &&
audioInfoComplete &&
videoInfoComplete;
}
isSeekable() {
return this.hasKeyframesIndex === true;
}
getNearestKeyframe(milliseconds) {
if (this.keyframesIndex == null) {
return null;
}
let table = this.keyframesIndex;
let keyframeIdx = this._search(table.times, milliseconds);
return {
index: keyframeIdx,
milliseconds: table.times[keyframeIdx],
fileposition: table.filepositions[keyframeIdx]
};
}
_search(list, value) {
let idx = 0;
let last = list.length - 1;
let mid = 0;
let lbound = 0;
let ubound = last;
if (value < list[0]) {
idx = 0;
lbound = ubound + 1; // skip search
}
while (lbound <= ubound) {
mid = lbound + Math.floor((ubound - lbound) / 2);
if (mid === last || (value >= list[mid] && value < list[mid + 1])) {
idx = mid;
break;
} else if (list[mid] < value) {
lbound = mid + 1;
} else {
ubound = mid - 1;
}
}
return idx;
}
}
export default MediaInfo;
================================================
FILE: src/core/media-segment-info.js
================================================
/*
* Copyright (C) 2016 Bilibili. All Rights Reserved.
*
* @author zheng qian <xqq@xqq.im>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Represents an media sample (audio / video)
export class SampleInfo {
constructor(dts, pts, duration, originalDts, isSync) {
this.dts = dts;
this.pts = pts;
this.duration = duration;
this.originalDts = originalDts;
this.isSyncPoint = isSync;
this.fileposition = null;
}
}
// Media Segment concept is defined in Media Source Extensions spec.
// Particularly in ISO BMFF format, an Media Segment contains a moof box followed by a mdat box.
export class MediaSegmentInfo {
constructor() {
this.beginDts = 0;
this.endDts = 0;
this.beginPts = 0;
this.endPts = 0;
this.originalBeginDts = 0;
this.originalEndDts = 0;
this.syncPoints = []; // SampleInfo[n], for video IDR frames only
this.firstSample = null; // SampleInfo
this.lastSample = null; // SampleInfo
}
appendSyncPoint(sampleInfo) { // also called Random Access Point
sampleInfo.isSyncPoint = true;
this.syncPoints.push(sampleInfo);
}
}
// Ordered list for recording video IDR frames, sorted by originalDts
export class IDRSampleList {
constructor() {
this._list = [];
}
clear() {
this._list = [];
}
appendArray(syncPoints) {
let list = this._list;
if (syncPoints.length === 0) {
return;
}
if (list.length > 0 && syncPoints[0].originalDts < list[list.length - 1].originalDts) {
this.clear();
}
Array.prototype.push.apply(list, syncPoints);
}
getLastSyncPointBeforeDts(dts) {
if (this._list.length == 0) {
return null;
}
let list = this._list;
let idx = 0;
let last = list.length - 1;
let mid = 0;
let lbound = 0;
let ubound = last;
if (dts < list[0].dts) {
idx = 0;
lbound = ubound + 1;
}
while (lbound <= ubound) {
mid = lbound + Math.floor((ubound - lbound) / 2);
if (mid === last || (dts >= list[mid].dts && dts < list[mid + 1].dts)) {
idx = mid;
break;
} else if (list[mid].dts < dts) {
lbound = mid + 1;
} else {
ubound = mid - 1;
}
}
return this._list[idx];
}
}
// Data structure for recording information of media segments in single track.
export class MediaSegmentInfoList {
constructor(type) {
this._type = type;
this._list = [];
this._lastAppendLocation = -1; // cached last insert location
}
get type() {
return this._type;
}
get length() {
return this._list.length;
}
isEmpty() {
return this._list.length === 0;
}
clear() {
this._list = [];
this._lastAppendLocation = -1;
}
_searchNearestSegmentBefore(originalBeginDts) {
let list = this._list;
if (list.length === 0) {
return -2;
}
let last = list.length - 1;
let mid = 0;
let lbound = 0;
let ubound = last;
let idx = 0;
if (originalBeginDts < list[0].originalBeginDts) {
idx = -1;
return idx;
}
while (lbound <= ubound) {
mid = lbound + Math.floor((ubound - lbound) / 2);
if (mid === last || (originalBeginDts > list[mid].lastSample.originalDts &&
(originalBeginDts < list[mid + 1].originalBeginDts))) {
idx = mid;
break;
} else if (list[mid].originalBeginDts < originalBeginDts) {
lbound = mid + 1;
} else {
ubound = mid - 1;
}
}
return idx;
}
_searchNearestSegmentAfter(originalBeginDts) {
return this._searchNearestSegmentBefore(originalBeginDts) + 1;
}
append(mediaSegmentInfo) {
let list = this._list;
let msi = mediaSegmentInfo;
let lastAppendIdx = this._lastAppendLocation;
let insertIdx = 0;
if (lastAppendIdx !== -1 && lastAppendIdx < list.length &&
msi.originalBeginDts >= list[lastAppendIdx].lastSample.originalDts &&
((lastAppendIdx === list.length - 1) ||
(lastAppendIdx < list.length - 1 &&
msi.originalBeginDts < list[lastAppendIdx + 1].originalBeginDts))) {
insertIdx = lastAppendIdx + 1; // use cached location idx
} else {
if (list.length > 0) {
insertIdx = this._searchNearestSegmentBefore(msi.originalBeginDts) + 1;
}
}
this._lastAppendLocation = insertIdx;
this._list.splice(insertIdx, 0, msi);
}
getLastSegmentBefore(originalBeginDts) {
let idx = this._searchNearestSegmentBefore(originalBeginDts);
if (idx >= 0) {
return this._list[idx];
} else { // -1
return null;
}
}
getLastSampleBefore(originalBeginDts) {
let segment = this.getLastSegmentBefore(originalBeginDts);
if (segment != null) {
return segment.lastSample;
} else {
return null;
}
}
getLastSyncPointBefore(originalBeginDts) {
let segmentIdx = this._searchNearestSegmentBefore(originalBeginDts);
let syncPoints = this._list[segmentIdx].syncPoints;
while (syncPoints.length === 0 && segmentIdx > 0) {
segmentIdx--;
syncPoints = this._list[segmentIdx].syncPoints;
}
if (syncPoints.length > 0) {
return syncPoints[syncPoints.length - 1];
} else {
return null;
}
}
}
================================================
FILE: src/core/mse-controller.js
================================================
/*
* Copyright (C) 2016 Bilibili. All Rights Reserved.
*
* @author zheng qian <xqq@xqq.im>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import EventEmitter from 'events';
import Log from '../utils/logger.js';
import Browser from '../utils/browser.js';
import MSEEvents from './mse-events.js';
import {SampleInfo, IDRSampleList} from './media-segment-info.js';
import {IllegalStateException} from '../utils/exception.js';
// Media Source Extensions controller
class MSEController {
constructor(config) {
this.TAG = 'MSEController';
this._config = config;
this._emitter = new EventEmitter();
if (this._config.isLive && this._config.autoCleanupSourceBuffer == undefined) {
// For live stream, do auto cleanup by default
this._config.autoCleanupSourceBuffer = true;
}
this.e = {
onSourceOpen: this._onSourceOpen.bind(this),
onSourceEnded: this._onSourceEnded.bind(this),
onSourceClose: this._onSourceClose.bind(this),
onSourceBufferError: this._onSourceBufferError.bind(this),
onSourceBufferUpdateEnd: this._onSourceBufferUpdateEnd.bind(this)
};
this._mediaSource = null;
this._mediaSourceObjectURL = null;
this._mediaElement = null;
this._isBufferFull = false;
this._hasPendingEos = false;
this._requireSetMediaDuration = false;
this._pendingMediaDuration = 0;
this._pendingSourceBufferInit = [];
this._mimeTypes = {
video: null,
audio: null
};
this._sourceBuffers = {
video: null,
audio: null
};
this._lastInitSegments = {
video: null,
audio: null
};
this._pendingSegments = {
video: [],
audio: []
};
this._pendingRemoveRanges = {
video: [],
audio: []
};
this._idrList = new IDRSampleList();
}
destroy() {
if (this._mediaElement || this._mediaSource) {
this.detachMediaElement();
}
this.e = null;
this._emitter.removeAllListeners();
this._emitter = null;
}
on(event, listener) {
this._emitter.addListener(event, listener);
}
off(event, listener) {
this._emitter.removeListener(event, listener);
}
attachMediaElement(mediaElement) {
if (this._mediaSource) {
throw new IllegalStateException('MediaSource has been attached to an HTMLMediaElement!');
}
let ms = this._mediaSource = new window.MediaSource();
ms.addEventListener('sourceopen', this.e.onSourceOpen);
ms.addEventListener('sourceended', this.e.onSourceEnded);
ms.addEventListener('sourceclose', this.e.onSourceClose);
this._mediaElement = mediaElement;
this._mediaSourceObjectURL = window.URL.createObjectURL(this._mediaSource);
mediaElement.src = this._mediaSourceObjectURL;
}
detachMediaElement() {
if (this._mediaSource) {
let ms = this._mediaSource;
for (let type in this._sourceBuffers) {
// pending segments should be discard
let ps = this._pendingSegments[type];
ps.splice(0, ps.length);
this._pendingSegments[type] = null;
this._pendingRemoveRanges[type] = null;
this._lastInitSegments[type] = null;
// remove all sourcebuffers
let sb = this._sourceBuffers[type];
if (sb) {
if (ms.readyState !== 'closed') {
// ms edge can throw an error: Unexpected call to method or property access
try {
ms.removeSourceBuffer(sb);
} catch (error) {
Log.e(this.TAG, error.message);
}
sb.removeEventListener('error', this.e.onSourceBufferError);
sb.removeEventListener('updateend', this.e.onSourceBufferUpdateEnd);
}
this._mimeTypes[type] = null;
this._sourceBuffers[type] = null;
}
}
if (ms.readyState === 'open') {
try {
ms.endOfStream();
} catch (error) {
Log.e(this.TAG, error.message);
}
}
ms.removeEventListener('sourceopen', this.e.onSourceOpen);
ms.removeEventListener('sourceended', this.e.onSourceEnded);
ms.removeEventListener('sourceclose', this.e.onSourceClose);
this._pendingSourceBufferInit = [];
this._isBufferFull = false;
this._idrList.clear();
this._mediaSource = null;
}
if (this._mediaElement) {
this._mediaElement.src = '';
this._mediaElement.removeAttribute('src');
this._mediaElement = null;
}
if (this._mediaSourceObjectURL) {
window.URL.revokeObjectURL(this._mediaSourceObjectURL);
this._mediaSourceObjectURL = null;
}
}
appendInitSegment(initSegment, deferred) {
if (!this._mediaSource || this._mediaSource.readyState !== 'open') {
// sourcebuffer creation requires mediaSource.readyState === 'open'
// so we defer the sourcebuffer creation, until sourceopen event triggered
this._pendingSourceBufferInit.push(initSegment);
// make sure that this InitSegment is in the front of pending segments queue
this._pendingSegments[initSegment.type].push(initSegment);
return;
}
let is = initSegment;
let mimeType = `${is.container}`;
if (is.codec && is.codec.length > 0) {
mimeType += `;codecs=${is.codec}`;
}
let firstInitSegment = false;
Log.v(this.TAG, 'Received Initialization Segment, mimeType: ' + mimeType);
this._lastInitSegments[is.type] = is;
if (mimeType !== this._mimeTypes[is.type]) {
if (!this._mimeTypes[is.type]) { // empty, first chance create sourcebuffer
firstInitSegment = true;
try {
let sb = this._sourceBuffers[is.type] = this._mediaSource.addSourceBuffer(mimeType);
sb.addEventListener('error', this.e.onSourceBufferError);
sb.addEventListener('updateend', this.e.onSourceBufferUpdateEnd);
} catch (error) {
Log.e(this.TAG, error.message);
this._emitter.emit(MSEEvents.ERROR, {code: error.code, msg: error.message});
return;
}
} else {
Log.v(this.TAG, `Notice: ${is.type} mimeType changed, origin: ${this._mimeTypes[is.type]}, target: ${mimeType}`);
}
this._mimeTypes[is.type] = mimeType;
}
if (!deferred) {
// deferred means this InitSegment has been pushed to pendingSegments queue
this._pendingSegments[is.type].push(is);
}
if (!firstInitSegment) { // append immediately only if init segment in subsequence
if (this._sourceBuffers[is.type] && !this._sourceBuffers[is.type].updating) {
this._doAppendSegments();
}
}
if (Browser.safari && is.container === 'audio/mpeg' && is.mediaDuration > 0) {
// 'audio/mpeg' track under Safari may cause MediaElement's duration to be NaN
// Manually correct MediaSource.duration to make progress bar seekable, and report right duration
this._requireSetMediaDuration = true;
this._pendingMediaDuration = is.mediaDuration / 1000; // in seconds
this._updateMediaSourceDuration();
}
}
appendMediaSegment(mediaSegment) {
let ms = mediaSegment;
this._pendingSegments[ms.type].push(ms);
if (this._config.autoCleanupSourceBuffer && this._needCleanupSourceBuffer()) {
this._doCleanupSourceBuffer();
}
let sb = this._sourceBuffers[ms.type];
if (sb && !sb.updating && !this._hasPendingRemoveRanges()) {
this._doAppendSegments();
}
}
seek(seconds) {
// remove all appended buffers
for (let type in this._sourceBuffers) {
if (!this._sourceBuffers[type]) {
continue;
}
// abort current buffer append algorithm
let sb = this._sourceBuffers[type];
if (this._mediaSource.readyState === 'open') {
try {
// If range removal algorithm is running, InvalidStateError will be throwed
// Ignore it.
sb.abort();
} catch (error) {
Log.e(this.TAG, error.message);
}
}
// IDRList should be clear
this._idrList.clear();
// pending segments should be discard
let ps = this._pendingSegments[type];
ps.splice(0, ps.length);
if (this._mediaSource.readyState === 'closed') {
// Parent MediaSource object has been detached from HTMLMediaElement
continue;
}
// record ranges to be remove from SourceBuffer
for (let i = 0; i < sb.buffered.length; i++) {
let start = sb.buffered.start(i);
let end = sb.buffered.end(i);
this._pendingRemoveRanges[type].push({start, end});
}
// if sb is not updating, let's remove ranges now!
if (!sb.updating) {
this._doRemoveRanges();
}
// Safari 10 may get InvalidStateError in the later appendBuffer() after SourceBuffer.remove() call
// Internal parser's state may be invalid at this time. Re-append last InitSegment to workaround.
// Related issue: https://bugs.webkit.org/show_bug.cgi?id=159230
if (Browser.safari) {
let lastInitSegment = this._lastInitSegments[type];
if (lastInitSegment) {
this._pendingSegments[type].push(lastInitSegment);
if (!sb.updating) {
this._doAppendSegments();
}
}
}
}
}
endOfStream() {
let ms = this._mediaSource;
let sb = this._sourceBuffers;
if (!ms || ms.readyState !== 'open') {
if (ms && ms.readyState === 'closed' && this._hasPendingSegments()) {
// If MediaSource hasn't turned into open state, and there're pending segments
// Mark pending endOfStream, defer call until all pending segments appended complete
this._hasPendingEos = true;
}
return;
}
if (sb.video && sb.video.updating || sb.audio && sb.audio.updating) {
// If any sourcebuffer is updating, defer endOfStream operation
// See _onSourceBufferUpdateEnd()
this._hasPendingEos = true;
} else {
this._hasPendingEos = false;
// Notify media data loading complete
// This is helpful for correcting total duration to match last media segment
// Otherwise MediaElement's ended event may not be triggered
ms.endOfStream();
}
}
getNearestKeyframe(dts) {
return this._idrList.getLastSyncPointBeforeDts(dts);
}
_needCleanupSourceBuffer() {
if (!this._config.autoCleanupSourceBuffer) {
return false;
}
let currentTime = this._mediaElement.currentTime;
for (let type in this._sourceBuffers) {
let sb = this._sourceBuffers[type];
if (sb) {
let buffered = sb.buffered;
if (buffered.length >= 1) {
if (currentTime - buffered.start(0) >= this._config.autoCleanupMaxBackwardDuration) {
return true;
}
}
}
}
return false;
}
_doCleanupSourceBuffer() {
let currentTime = this._mediaElement.currentTime;
for (let type in this._sourceBuffers) {
let sb = this._sourceBuffers[type];
if (sb) {
let buffered = sb.buffered;
let doRemove = false;
for (let i = 0; i < buffered.length; i++) {
let start = buffered.start(i);
let end = buffered.end(i);
if (start <= currentTime && currentTime < end + 3) { // padding 3 seconds
if (currentTime - start >= this._config.autoCleanupMaxBackwardDuration) {
doRemove = true;
let removeEnd = currentTime - this._config.autoCleanupMinBackwardDuration;
this._pendingRemoveRanges[type].push({start: start, end: removeEnd});
}
} else if (end < currentTime) {
doRemove = true;
this._pendingRemoveRanges[type].push({start: start, end: end});
}
}
if (doRemove && !sb.updating) {
this._doRemoveRanges();
}
}
}
}
_updateMediaSourceDuration() {
let sb = this._sourceBuffers;
if (this._mediaElement.readyState === 0 || this._mediaSource.readyState !== 'open') {
return;
}
if ((sb.video && sb.video.updating) || (sb.audio && sb.audio.updating)) {
return;
}
let current = this._mediaSource.duration;
let target = this._pendingMediaDuration;
if (target > 0 && (isNaN(current) || target > current)) {
Log.v(this.TAG, `Update MediaSource duration from ${current} to ${target}`);
this._mediaSource.duration = target;
}
this._requireSetMediaDuration = false;
this._pendingMediaDuration = 0;
}
_doRemoveRanges() {
for (let type in this._pendingRemoveRanges) {
if (!this._sourceBuffers[type] || this._sourceBuffers[type].updating) {
continue;
}
let sb = this._sourceBuffers[type];
let ranges = this._pendingRemoveRanges[type];
while (ranges.length && !sb.updating) {
let range = ranges.shift();
sb.remove(range.start, range.end);
}
}
}
_doAppendSegments() {
let pendingSegments = this._pendingSegments;
for (let type in pendingSegments) {
if (!this._sourceBuffers[type] || this._sourceBuffers[type].updating) {
continue;
}
if (pendingSegments[type].length > 0) {
let segment = pendingSegments[type].shift();
if (segment.timestampOffset) {
// For MPEG audio stream in MSE, if unbuffered-seeking occurred
// We need explicitly set timestampOffset to the desired point in timeline for mpeg SourceBuffer.
let currentOffset = this._sourceBuffers[type].timestampOffset;
let targetOffset = segment.timestampOffset / 1000; // in seconds
let delta = Math.abs(currentOffset - targetOffset);
if (delta > 0.1) { // If time delta > 100ms
Log.v(this.TAG, `Update MPEG audio timestampOffset from ${currentOffset} to ${targetOffset}`);
this._sourceBuffers[type].timestampOffset = targetOffset;
}
delete segment.timestampOffset;
}
if (!segment.data || segment.data.byteLength === 0) {
// Ignore empty buffer
continue;
}
try {
this._sourceBuffers[type].appendBuffer(segment.data);
this._isBufferFull = false;
if (type === 'video' && segment.hasOwnProperty('info')) {
this._idrList.appendArray(segment.info.syncPoints);
}
} catch (error) {
this._pendingSegments[type].unshift(segment);
if (error.code === 22) { // QuotaExceededError
/* Notice that FireFox may not throw QuotaExceededError if SourceBuffer is full
* Currently we can only do lazy-load to avoid SourceBuffer become scattered.
* SourceBuffer eviction policy may be changed in future version of FireFox.
*
* Related issues:
* https://bugzilla.mozilla.org/show_bug.cgi?id=1279885
* https://bugzilla.mozilla.org/show_bug.cgi?id=1280023
*/
// report buffer full, abort network IO
if (!this._isBufferFull) {
this._emitter.emit(MSEEvents.BUFFER_FULL);
}
this._isBufferFull = true;
} else {
Log.e(this.TAG, error.message);
this._emitter.emit(MSEEvents.ERROR, {code: error.code, msg: error.message});
}
}
}
}
}
_onSourceOpen() {
Log.v(this.TAG, 'MediaSource onSourceOpen');
this._mediaSource.removeEventListener('sourceopen', this.e.onSourceOpen);
// deferred sourcebuffer creation / initialization
if (this._pendingSourceBufferInit.length > 0) {
let pendings = this._pendingSourceBufferInit;
while (pendings.length) {
let segment = pendings.shift();
this.appendInitSegment(segment, true);
}
}
// there may be some pending media segments, append them
if (this._hasPendingSegments()) {
this._doAppendSegments();
}
this._emitter.emit(MSEEvents.SOURCE_OPEN);
}
_onSourceEnded() {
// fired on endOfStream
Log.v(this.TAG, 'MediaSource onSourceEnded');
}
_onSourceClose() {
// fired on detaching from media element
Log.v(this.TAG, 'MediaSource onSourceClose');
if (this._mediaSource && this.e != null) {
this._mediaSource.removeEventListener('sourceopen', this.e.onSourceOpen);
this._mediaSource.removeEventListener('sourceended', this.e.onSourceEnded);
this._mediaSource.removeEventListener('sourceclose', this.e.onSourceClose);
}
}
_hasPendingSegments() {
let ps = this._pendingSegments;
return ps.video.length > 0 || ps.audio.length > 0;
}
_hasPendingRemoveRanges() {
let prr = this._pendingRemoveRanges;
return prr.video.length > 0 || prr.audio.length > 0;
}
_onSourceBufferUpdateEnd() {
if (this._requireSetMediaDuration) {
this._updateMediaSourceDuration();
} else if (this._hasPendingRemoveRanges()) {
this._doRemoveRanges();
} else if (this._hasPendingSegments()) {
this._doAppendSegments();
} else if (this._hasPendingEos) {
this.endOfStream();
}
this._emitter.emit(MSEEvents.UPDATE_END);
}
_onSourceBufferError(e) {
Log.e(this.TAG, `SourceBuffer Error: ${e}`);
// this error might not always be fatal, just ignore it
}
}
export default MSEController;
================================================
FILE: src/core/mse-events.js
================================================
/*
* Copyright (C) 2016 Bilibili. All Rights Reserved.
*
* @author zheng qian <xqq@xqq.im>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const MSEEvents = {
ERROR: 'error',
SOURCE_OPEN: 'source_open',
UPDATE_END: 'update_end',
BUFFER_FULL: 'buffer_full'
};
export default MSEEvents;
================================================
FILE: src/core/transmuxer.js
================================================
/*
* Copyright (C) 2016 Bilibili. All Rights Reserved.
*
* @author zheng qian <xqq@xqq.im>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import EventEmitter from 'events';
import work from 'webworkify-webpack';
import Log from '../utils/logger.js';
import LoggingControl from '../utils/logging-control.js';
import TransmuxingController from './transmuxing-controller.js';
import TransmuxingEvents from './transmuxing-events.js';
import TransmuxingWorker from './transmuxing-worker.js';
import MediaInfo from './media-info.js';
class Transmuxer {
constructor(mediaDataSource, config) {
this.TAG = 'Transmuxer';
this._emitter = new EventEmitter();
if (config.enableWorker && typeof (Worker) !== 'undefined') {
try {
this._worker = work(require.resolve('./transmuxing-worker'));
this._workerDestroying = false;
this._worker.addEventListener('message', this._onWorkerMessage.bind(this));
this._worker.postMessage({cmd: 'init', param: [mediaDataSource, config]});
this.e = {
onLoggingConfigChanged: this._onLoggingConfigChanged.bind(this)
};
LoggingControl.registerListener(this.e.onLoggingConfigChanged);
this._worker.postMessage({cmd: 'logging_config', param: LoggingControl.getConfig()});
} catch (error) {
Log.e(this.TAG, 'Error while initialize transmuxing worker, fallback to inline transmuxing');
this._worker = null;
this._controller = new TransmuxingController(mediaDataSource, config);
}
} else {
this._controller = new TransmuxingController(mediaDataSource, config);
}
if (this._controller) {
let ctl = this._controller;
ctl.on(TransmuxingEvents.IO_ERROR, this._onIOError.bind(this));
ctl.on(TransmuxingEvents.DEMUX_ERROR, this._onDemuxError.bind(this));
ctl.on(TransmuxingEvents.INIT_SEGMENT, this._onInitSegment.bind(this));
ctl.on(TransmuxingEvents.MEDIA_SEGMENT, this._onMediaSegment.bind(this));
ctl.on(TransmuxingEvents.LOADING_COMPLETE, this._onLoadingComplete.bind(this));
ctl.on(TransmuxingEvents.RECOVERED_EARLY_EOF, this._onRecoveredEarlyEof.bind(this));
ctl.on(TransmuxingEvents.MEDIA_INFO, this._onMediaInfo.bind(this));
ctl.on(TransmuxingEvents.METADATA_ARRIVED, this._onMetaDataArrived.bind(this));
ctl.on(TransmuxingEvents.SCRIPTDATA_ARRIVED, this._onScriptDataArrived.bind(this));
ctl.on(TransmuxingEvents.STATISTICS_INFO, this._onStatisticsInfo.bind(this));
ctl.on(TransmuxingEvents.RECOMMEND_SEEKPOINT, this._onRecommendSeekpoint.bind(this));
}
}
destroy() {
if (this._worker) {
if (!this._workerDestroying) {
this._workerDestroying = true;
this._worker.postMessage({cmd: 'destroy'});
LoggingControl.removeListener(this.e.onLoggingConfigChanged);
this.e = null;
}
} else {
this._controller.destroy();
this._controller = null;
}
this._emitter.removeAllListeners();
this._emitter = null;
}
on(event, listener) {
this._emitter.addListener(event, listener);
}
off(event, listener) {
this._emitter.removeListener(event, listener);
}
hasWorker() {
return this._worker != null;
}
open() {
if (this._worker) {
this._worker.postMessage({cmd: 'start'});
} else {
this._controller.start();
}
}
close() {
if (this._worker) {
this._worker.postMessage({cmd: 'stop'});
} else {
this._controller.stop();
}
}
seek(milliseconds) {
if (this._worker) {
this._worker.postMessage({cmd: 'seek', param: milliseconds});
} else {
this._controller.seek(milliseconds);
}
}
pause() {
if (this._worker) {
this._worker.postMessage({cmd: 'pause'});
} else {
this._controller.pause();
}
}
resume() {
if (this._worker) {
this._worker.postMessage({cmd: 'resume'});
} else {
this._controller.resume();
}
}
_onInitSegment(type, initSegment) {
// do async invoke
Promise.resolve().then(() => {
this._emitter.emit(TransmuxingEvents.INIT_SEGMENT, type, initSegment);
});
}
_onMediaSegment(type, mediaSegment) {
Promise.resolve().then(() => {
this._emitter.emit(TransmuxingEvents.MEDIA_SEGMENT, type, mediaSegment);
});
}
_onLoadingComplete() {
Promise.resolve().then(() => {
this._emitter.emit(TransmuxingEvents.LOADING_COMPLETE);
});
}
_onRecoveredEarlyEof() {
Promise.resolve().then(() => {
this._emitter.emit(TransmuxingEvents.RECOVERED_EARLY_EOF);
});
}
_onMediaInfo(mediaInfo) {
Promise.resolve().then(() => {
this._emitter.emit(TransmuxingEvents.MEDIA_INFO, mediaInfo);
});
}
_onMetaDataArrived(metadata) {
Promise.resolve().then(() => {
this._emitter.emit(TransmuxingEvents.METADATA_ARRIVED, metadata);
});
}
_onScriptDataArrived(data) {
Promise.resolve().then(() => {
this._emitter.emit(TransmuxingEvents.SCRIPTDATA_ARRIVED, data);
});
}
_onStatisticsInfo(statisticsInfo) {
Promise.resolve().then(() => {
this._emitter.emit(TransmuxingEvents.STATISTICS_INFO, statisticsInfo);
});
}
_onIOError(type, info) {
Promise.resolve().then(() => {
this._emitter.emit(TransmuxingEvents.IO_ERROR, type, info);
});
}
_onDemuxError(type, info) {
Promise.resolve().then(() => {
this._emitter.emit(TransmuxingEvents.DEMUX_ERROR, type, info);
});
}
_onRecommendSeekpoint(milliseconds) {
Promise.resolve().then(() => {
this._emitter.emit(TransmuxingEvents.RECOMMEND_SEEKPOINT, milliseconds);
});
}
_onLoggingConfigChanged(config) {
if (this._worker) {
this._worker.postMessage({cmd: 'logging_config', param: config});
}
}
_onWorkerMessage(e) {
let message = e.data;
let data = message.data;
if (message.msg === 'destroyed' || this._workerDestroying) {
this._workerDestroying = false;
this._worker.terminate();
this._worker = null;
return;
}
switch (message.msg) {
case TransmuxingEvents.INIT_SEGMENT:
case TransmuxingEvents.MEDIA_SEGMENT:
this._emitter.emit(message.msg, data.type, data.data);
break;
case TransmuxingEvents.LOADING_COMPLETE:
case TransmuxingEvents.RECOVERED_EARLY_EOF:
this._emitter.emit(message.msg);
break;
case TransmuxingEvents.MEDIA_INFO:
Object.setPrototypeOf(data, MediaInfo.prototype);
this._emitter.emit(message.msg, data);
break;
case TransmuxingEvents.METADATA_ARRIVED:
case TransmuxingEvents.SCRIPTDATA_ARRIVED:
case TransmuxingEvents.STATISTICS_INFO:
this._emitter.emit(message.msg, data);
break;
case TransmuxingEvents.IO_ERROR:
case TransmuxingEvents.DEMUX_ERROR:
this._emitter.emit(message.msg, data.type, data.info);
break;
case TransmuxingEvents.RECOMMEND_SEEKPOINT:
this._emitter.emit(message.msg, data);
break;
case 'logcat_callback':
Log.emitter.emit('log', data.type, data.logcat);
break;
default:
break;
}
}
}
export default Transmuxer;
================================================
FILE: src/core/transmuxing-controller.js
================================================
/*
* Copyright (C) 2016 Bilibili. All Rights Reserved.
*
* @author zheng qian <xqq@xqq.im>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import EventEmitter from 'events';
import Log from '../utils/logger.js';
import Browser from '../utils/browser.js';
import MediaInfo from './media-info.js';
import FLVDemuxer from '../demux/flv-demuxer.js';
import MP4Remuxer from '../remux/mp4-remuxer.js';
import DemuxErrors from '../demux/demux-errors.js';
import IOController from '../io/io-controller.js';
import TransmuxingEvents from './transmuxing-events.js';
import {LoaderStatus, LoaderErrors} from '../io/loader.js';
// Transmuxing (IO, Demuxing, Remuxing) controller, with multipart support
class TransmuxingController {
constructor(mediaDataSource, config) {
this.TAG = 'TransmuxingController';
this._emitter = new EventEmitter();
this._config = config;
// treat single part media as multipart media, which has only one segment
if (!mediaDataSource.segments) {
mediaDataSource.segments = [{
duration: mediaDataSource.duration,
filesize: mediaDataSource.filesize,
url: mediaDataSource.url
}];
}
// fill in default IO params if not exists
if (typeof mediaDataSource.cors !== 'boolean') {
mediaDataSource.cors = true;
}
if (typeof mediaDataSource.withCredentials !== 'boolean') {
mediaDataSource.withCredentials = false;
}
this._mediaDataSource = mediaDataSource;
this._currentSegmentIndex = 0;
let totalDuration = 0;
this._mediaDataSource.segments.forEach((segment) => {
// timestampBase for each segment, and calculate total duration
segment.timestampBase = totalDuration;
totalDuration += segment.duration;
// params needed by IOController
segment.cors = mediaDataSource.cors;
segment.withCredentials = mediaDataSource.withCredentials;
// referrer policy control, if exist
if (config.referrerPolicy) {
segment.referrerPolicy = config.referrerPolicy;
}
});
if (!isNaN(totalDuration) && this._mediaDataSource.duration !== totalDuration) {
this._mediaDataSource.duration = totalDuration;
}
this._mediaInfo = null;
this._demuxer = null;
this._remuxer = null;
this._ioctl = null;
this._pendingSeekTime = null;
this._pendingResolveSeekPoint = null;
this._statisticsReporter = null;
}
destroy() {
this._mediaInfo = null;
this._mediaDataSource = null;
if (this._statisticsReporter) {
this._disableStatisticsReporter();
}
if (this._ioctl) {
this._ioctl.destroy();
this._ioctl = null;
}
if (this._demuxer) {
this._demuxer.destroy();
this._demuxer = null;
}
if (this._remuxer) {
this._remuxer.destroy();
this._remuxer = null;
}
this._emitter.removeAllListeners();
this._emitter = null;
}
on(event, listener) {
this._emitter.addListener(event, listener);
}
off(event, listener) {
this._emitter.removeListener(event, listener);
}
start() {
this._loadSegment(0);
this._enableStatisticsReporter();
}
_loadSegment(segmentIndex, optionalFrom) {
this._currentSegmentIndex = segmentIndex;
let dataSource = this._mediaDataSource.segments[segmentIndex];
let ioctl = this._ioctl = new IOController(dataSource, this._config, segmentIndex);
ioctl.onError = this._onIOException.bind(this);
ioctl.onSeeked = this._onIOSeeked.bind(this);
ioctl.onComplete = this._onIOComplete.bind(this);
ioctl.onRedirect = this._onIORedirect.bind(this);
ioctl.onRecoveredEarlyEof = this._onIORecoveredEarlyEof.bind(this);
if (optionalFrom) {
this._demuxer.bindDataSource(this._ioctl);
} else {
ioctl.onDataArrival = this._onInitChunkArrival.bind(this);
}
ioctl.open(optionalFrom);
}
stop() {
this._internalAbort();
this._disableStatisticsReporter();
}
_internalAbort() {
if (this._ioctl) {
this._ioctl.destroy();
this._ioctl = null;
}
}
pause() { // take a rest
if (this._ioctl && this._ioctl.isWorking()) {
this._ioctl.pause();
this._disableStatisticsReporter();
}
}
resume() {
if (this._ioctl && this._ioctl.isPaused()) {
this._ioctl.resume();
this._enableStatisticsReporter();
}
}
seek(milliseconds) {
if (this._mediaInfo == null || !this._mediaInfo.isSeekable()) {
return;
}
let targetSegmentIndex = this._searchSegmentIndexContains(milliseconds);
if (targetSegmentIndex === this._currentSegmentIndex) {
// intra-segment seeking
let segmentInfo = this._mediaInfo.segments[targetSegmentIndex];
if (segmentInfo == undefined) {
// current segment loading started, but mediainfo hasn't received yet
// wait for the metadata loaded, then seek to expected position
this._pendingSeekTime = milliseconds;
} else {
let keyframe = segmentInfo.getNearestKeyframe(milliseconds);
this._remuxer.seek(keyframe.milliseconds);
this._ioctl.seek(keyframe.fileposition);
// Will be resolved in _onRemuxerMediaSegmentArrival()
this._pendingResolveSeekPoint = keyframe.milliseconds;
}
} else {
// cross-segment seeking
let targetSegmentInfo = this._mediaInfo.segments[targetSegmentIndex];
if (targetSegmentInfo == undefined) {
// target segment hasn't been loaded. We need metadata then seek to expected time
this._pendingSeekTime = milliseconds;
this._internalAbort();
this._remuxer.seek();
this._remuxer.insertDiscontinuity();
this._loadSegment(targetSegmentIndex);
// Here we wait for the metadata loaded, then seek to expected position
} else {
// We have target segment's metadata, direct seek to target position
let keyframe = targetSegmentInfo.getNearestKeyframe(milliseconds);
this._internalAbort();
this._remuxer.seek(milliseconds);
this._remuxer.insertDiscontinuity();
this._demuxer.resetMediaInfo();
this._demuxer.timestampBase = this._mediaDataSource.segments[targetSegmentIndex].timestampBase;
this._loadSegment(targetSegmentIndex, keyframe.fileposition);
this._pendingResolveSeekPoint = keyframe.milliseconds;
this._reportSegmentMediaInfo(targetSegmentIndex);
}
}
this._enableStatisticsReporter();
}
_searchSegmentIndexContains(milliseconds) {
let segments = this._mediaDataSource.segments;
let idx = segments.length - 1;
for (let i = 0; i < segments.length; i++) {
if (milliseconds < segments[i].timestampBase) {
idx = i - 1;
break;
}
}
return idx;
}
_onInitChunkArrival(data, byteStart) {
let probeData = null;
let consumed = 0;
if (byteStart > 0) {
// IOController seeked immediately after opened, byteStart > 0 callback may received
this._demuxer.bindDataSource(this._ioctl);
this._demuxer.timestampBase = this._mediaDataSource.segments[this._currentSegmentIndex].timestampBase;
consumed = this._demuxer.parseChunks(data, byteStart);
} else if ((probeData = FLVDemuxer.probe(data)).match) {
// Always create new FLVDemuxer
this._demuxer = new FLVDemuxer(probeData, this._config);
if (!this._remuxer) {
this._remuxer = new MP4Remuxer(this._config);
}
let mds = this._mediaDataSource;
if (mds.duration != undefined && !isNaN(mds.duration)) {
this._demuxer.overridedDuration = mds.duration;
}
if (typeof mds.hasAudio === 'boolean') {
this._demuxer.overridedHasAudio = mds.hasAudio;
}
if (typeof mds.hasVideo === 'boolean') {
this._demuxer.overridedHasVideo = mds.hasVideo;
}
this._demuxer.timestampBase = mds.segments[this._currentSegmentIndex].timestampBase;
this._demuxer.onError = this._onDemuxException.bind(this);
this._demuxer.onMediaInfo = this._onMediaInfo.bind(this);
this._demuxer.onMetaDataArrived = this._onMetaDataArrived.bind(this);
this._demuxer.onScriptDataArrived = this._onScriptDataArrived.bind(this);
this._remuxer.bindDataSource(this._demuxer
.bindDataSource(this._ioctl
));
this._remuxer.onInitSegment = this._onRemuxerInitSegmentArrival.bind(this);
this._remuxer.onMediaSegment = this._onRemuxerMediaSegmentArrival.bind(this);
consumed = this._demuxer.parseChunks(data, byteStart);
} else {
probeData = null;
Log.e(this.TAG, 'Non-FLV, Unsupported media type!');
Promise.resolve().then(() => {
this._internalAbort();
});
this._emitter.emit(TransmuxingEvents.DEMUX_ERROR, DemuxErrors.FORMAT_UNSUPPORTED, 'Non-FLV, Unsupported media type');
consumed = 0;
}
return consumed;
}
_onMediaInfo(mediaInfo) {
if (this._mediaInfo == null) {
// Store first segment's mediainfo as global mediaInfo
this._mediaInfo = Object.assign({}, mediaInfo);
this._mediaInfo.keyframesIndex = null;
this._mediaInfo.segments = [];
this._mediaInfo.segmentCount = this._mediaDataSource.segments.length;
Object.setPrototypeOf(this._mediaInfo, MediaInfo.prototype);
}
let segmentInfo = Object.assign({}, mediaInfo);
Object.setPrototypeOf(segmentInfo, MediaInfo.prototype);
this._mediaInfo.segments[this._currentSegmentIndex] = segmentInfo;
// notify mediaInfo update
this._reportSegmentMediaInfo(this._currentSegmentIndex);
if (this._pendingSeekTime != null) {
Promise.resolve().then(() => {
let target = this._pendingSeekTime;
this._pendingSeekTime = null;
this.seek(target);
});
}
}
_onMetaDataArrived(metadata) {
this._emitter.emit(TransmuxingEvents.METADATA_ARRIVED, metadata);
}
_onScriptDataArrived(data) {
this._emitter.emit(TransmuxingEvents.SCRIPTDATA_ARRIVED, data);
}
_onIOSeeked() {
this._remuxer.insertDiscontinuity();
}
_onIOComplete(extraData) {
let segmentIndex = extraData;
let nextSegmentIndex = segmentIndex + 1;
if (nextSegmentIndex < this._mediaDataSource.segments.length) {
this._internalAbort();
this._remuxer.flushStashedSamples();
this._loadSegment(nextSegmentIndex);
} else {
this._remuxer.flushStashedSamples();
this._emitter.emit(TransmuxingEvents.LOADING_COMPLETE);
this._disableStatisticsReporter();
}
}
_onIORedirect(redirectedURL) {
let segmentIndex = this._ioctl.extraData;
this._mediaDataSource.segments[segmentIndex].redirectedURL = redirectedURL;
}
_onIORecoveredEarlyEof() {
this._emitter.emit(TransmuxingEvents.RECOVERED_EARLY_EOF);
}
_onIOException(type, info) {
Log.e(this.TAG, `IOException: type = ${type}, code = ${info.code}, msg = ${info.msg}`);
this._emitter.emit(TransmuxingEvents.IO_ERROR, type, info);
this._disableStatisticsReporter();
}
_onDemuxException(type, info) {
Log.e(this.TAG, `DemuxException: type = ${type}, info = ${info}`);
this._emitter.emit(TransmuxingEvents.DEMUX_ERROR, type, info);
}
_onRemuxerInitSegmentArrival(type, initSegment) {
this._emitter.emit(TransmuxingEvents.INIT_SEGMENT, type, initSegment);
}
_onRemuxerMediaSegmentArrival(type, mediaSegment) {
if (this._pendingSeekTime != null) {
// Media segments after new-segment cross-seeking should be dropped.
return;
}
this._emitter.emit(TransmuxingEvents.MEDIA_SEGMENT, type, mediaSegment);
// Resolve pending seekPoint
if (this._pendingResolveSeekPoint != null && type === 'video') {
let syncPoints = mediaSegment.info.syncPoints;
let seekpoint = this._pendingResolveSeekPoint;
this._pendingResolveSeekPoint = null;
// Safari: Pass PTS for recommend_seekpoint
if (Browser.safari && syncPoints.length > 0 && syncPoints[0].originalDts === seekpoint) {
seekpoint = syncPoints[0].pts;
}
// else: use original DTS (keyframe.milliseconds)
this._emitter.emit(TransmuxingEvents.RECOMMEND_SEEKPOINT, seekpoint);
}
}
_enableStatisticsReporter() {
if (this._statisticsReporter == null) {
this._statisticsReporter = self.setInterval(
this._reportStatisticsInfo.bind(this),
this._config.statisticsInfoReportInterval);
}
}
_disableStatisticsReporter() {
if (this._statisticsReporter) {
self.clearInterval(this._statisticsReporter);
this._statisticsReporter = null;
}
}
_reportSegmentMediaInfo(segmentIndex) {
let segmentInfo = this._mediaInfo.segments[segmentIndex];
let exportInfo = Object.assign({}, segmentInfo);
exportInfo.duration = this._mediaInfo.duration;
exportInfo.segmentCount = this._mediaInfo.segmentCount;
delete exportInfo.segments;
delete exportInfo.keyframesIndex;
this._emitter.emit(TransmuxingEvents.MEDIA_INFO, exportInfo);
}
_reportStatisticsInfo() {
let info = {};
info.url = this._ioctl.currentURL;
info.hasRedirect = this._ioctl.hasRedirect;
if (info.hasRedirect) {
info.redirectedURL = this._ioctl.currentRedirectedURL;
}
info.speed = this._ioctl.currentSpeed;
info.loaderType = this._ioctl.loaderType;
info.currentSegmentIndex = this._currentSegmentIndex;
info.totalSegmentCount = this._mediaDataSource.segments.length;
this._emitter.emit(TransmuxingEvents.STATISTICS_INFO, info);
}
}
export default TransmuxingController;
================================================
FILE: src/core/transmuxing-events.js
================================================
/*
* Copyright (C) 2016 Bilibili. All Rights Reserved.
*
* @author zheng qian <xqq@xqq.im>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const TransmuxingEvents = {
IO_ERROR: 'io_error',
DEMUX_ERROR: 'demux_error',
INIT_SEGMENT: 'init_segment',
MEDIA_SEGMENT: 'media_segment',
LOADING_COMPLETE: 'loading_complete',
RECOVERED_EARLY_EOF: 'recovered_early_eof',
MEDIA_INFO: 'media_info',
METADATA_ARRIVED: 'metadata_arrived',
SCRIPTDATA_ARRIVED: 'scriptdata_arrived',
STATISTICS_INFO: 'statistics_info',
RECOMMEND_SEEKPOINT: 'recommend_seekpoint'
};
export default TransmuxingEvents;
================================================
FILE: src/core/transmuxing-worker.js
================================================
/*
* Copyright (C) 2016 Bilibili. All Rights Reserved.
*
* @author zheng qian <xqq@xqq.im>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Log from '../utils/logger.js';
import LoggingControl from '../utils/logging-control.js';
import Polyfill from '../utils/polyfill.js';
import TransmuxingController from './transmuxing-controller.js';
import TransmuxingEvents from './transmuxing-events.js';
/* post message to worker:
data: {
cmd: string
param: any
}
receive message from worker:
data: {
msg: string,
data: any
}
*/
let TransmuxingWorker = function (self) {
let TAG = 'TransmuxingWorker';
let controller = null;
let logcatListener = onLogcatCallback.bind(this);
Polyfill.install();
self.addEventListener('message', function (e) {
switch (e.data.cmd) {
case 'init':
controller = new TransmuxingController(e.data.param[0], e.data.param[1]);
controller.on(TransmuxingEvents.IO_ERROR, onIOError.bind(this));
controller.on(TransmuxingEvents.DEMUX_ERROR, onDemuxError.bind(this));
controller.on(TransmuxingEvents.INIT_SEGMENT, onInitSegment.bind(this));
controller.on(TransmuxingEvents.MEDIA_SEGMENT, onMediaSegment.bind(this));
controller.on(TransmuxingEvents.LOADING_COMPLETE, onLoadingComplete.bind(this));
controller.on(TransmuxingEvents.RECOVERED_EARLY_EOF, onRecoveredEarlyEof.bind(this));
controller.on(TransmuxingEvents.MEDIA_INFO, onMediaInfo.bind(this));
controller.on(TransmuxingEvents.METADATA_ARRIVED, onMetaDataArrived.bind(this));
controller.on(TransmuxingEvents.SCRIPTDATA_ARRIVED, onScriptDataArrived.bind(this));
controller.on(TransmuxingEvents.STATISTICS_INFO, onStatisticsInfo.bind(this));
controller.on(TransmuxingEvents.RECOMMEND_SEEKPOINT, onRecommendSeekpoint.bind(this));
break;
case 'destroy':
if (controller) {
controller.destroy();
controller = null;
}
self.postMessage({msg: 'destroyed'});
break;
case 'start':
controller.start();
break;
case 'stop':
controller.stop();
break;
case 'seek':
controller.seek(e.data.param);
break;
case 'pause':
controller.pause();
break;
case 'resume':
controller.resume();
break;
case 'logging_config': {
let config = e.data.param;
LoggingControl.applyConfig(config);
if (config.enableCallback === true) {
LoggingControl.addLogListener(logcatListener);
} else {
LoggingControl.removeLogListener(logcatListener);
}
break;
}
}
});
function onInitSegment(type, initSegment) {
let obj = {
msg: TransmuxingEvents.INIT_SEGMENT,
data: {
type: type,
data: initSegment
}
};
self.postMessage(obj, [initSegment.data]); // data: ArrayBuffer
}
function onMediaSegment(type, mediaSegment) {
let obj = {
msg: TransmuxingEvents.MEDIA_SEGMENT,
data: {
type: type,
data: mediaSegment
}
};
self.postMessage(obj, [mediaSegment.data]); // data: ArrayBuffer
}
function onLoadingComplete() {
let obj = {
msg: TransmuxingEvents.LOADING_COMPLETE
};
self.postMessage(obj);
}
function onRecoveredEarlyEof() {
let obj = {
msg: TransmuxingEvents.RECOVERED_EARLY_EOF
};
self.postMessage(obj);
}
function onMediaInfo(mediaInfo) {
let obj = {
msg: TransmuxingEvents.MEDIA_INFO,
data: mediaInfo
};
self.postMessage(obj);
}
function onMetaDataArrived(metadata) {
let obj = {
msg: TransmuxingEvents.METADATA_ARRIVED,
data: metadata
};
self.postMessage(obj);
}
function onScriptDataArrived(data) {
let obj = {
msg: TransmuxingEvents.SCRIPTDATA_ARRIVED,
data: data
};
self.postMessage(obj);
}
function onStatisticsInfo(statInfo) {
let obj = {
msg: TransmuxingEvents.STATISTICS_INFO,
data: statInfo
};
self.postMessage(obj);
}
function onIOError(type, info) {
self.postMessage({
msg: TransmuxingEvents.IO_ERROR,
data: {
type: type,
info: info
}
});
}
function onDemuxError(type, info) {
self.postMessage({
msg: TransmuxingEvents.DEMUX_ERROR,
data: {
type: type,
info: info
}
});
}
function onRecommendSeekpoint(milliseconds) {
self.postMessage({
msg: TransmuxingEvents.RECOMMEND_SEEKPOINT,
data: milliseconds
});
}
function onLogcatCallback(type, str) {
self.postMessage({
msg: 'logcat_callback',
data: {
type: type,
logcat: str
}
});
}
};
export default TransmuxingWorker;
================================================
FILE: src/demux/amf-parser.js
================================================
/*
* Copyright (C) 2016 Bilibili. All Rights Reserved.
*
* @author zheng qian <xqq@xqq.im>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Log from '../utils/logger.js';
import decodeUTF8 from '../utils/utf8-conv.js';
import {IllegalStateException} from '../utils/exception.js';
let le = (function () {
let buf = new ArrayBuffer(2);
(new DataView(buf)).setInt16(0, 256, true); // little-endian write
return (new Int16Array(buf))[0] === 256; // platform-spec read, if equal then LE
})();
class AMF {
static parseScriptData(arrayBuffer, dataOffset, dataSize) {
let data = {};
try {
let name = AMF.parseValue(arrayBuffer, dataOffset, dataSize);
let value = AMF.parseValue(arrayBuffer, dataOffset + name.size, dataSize - name.size);
data[name.data] = value.data;
} catch (e) {
Log.e('AMF', e.toString());
}
return data;
}
static parseObject(arrayBuffer, dataOffset, dataSize) {
if (dataSize < 3) {
throw new IllegalStateException('Data not enough when parse ScriptDataObject');
}
let name = AMF.parseString(arrayBuffer, dataOffset, dataSize);
let value = AMF.parseValue(arrayBuffer, dataOffset + name.size, dataSize - name.size);
let isObjectEnd = value.objectEnd;
return {
data: {
name: name.data,
value: value.data
},
size: name.size + value.size,
objectEnd: isObjectEnd
};
}
static parseVariable(arrayBuffer, dataOffset, dataSize) {
return AMF.parseObject(arrayBuffer, dataOffset, dataSize);
}
static parseString(arrayBuffer, dataOffset, dataSize) {
if (dataSize < 2) {
throw new IllegalStateException('Data not enough when parse String');
}
let v = new DataView(arrayBuffer, dataOffset, dataSize);
let length = v.getUint16(0, !le);
let str;
if (length > 0) {
str = decodeUTF8(new Uint8Array(arrayBuffer, dataOffset + 2, length));
} else {
str = '';
}
return {
data: str,
size: 2 + length
};
}
static parseLongString(arrayBuffer, dataOffset, dataSize) {
if (dataSize < 4) {
throw new IllegalStateException('Data not enough when parse LongString');
}
let v = new DataView(arrayBuffer, dataOffset, dataSize);
let length = v.getUint32(0, !le);
let str;
if (length > 0) {
str = decodeUTF8(new Uint8Array(arrayBuffer, dataOffset + 4, length));
} else {
str = '';
}
return {
data: str,
size: 4 + length
};
}
static parseDate(arrayBuffer, dataOffset, dataSize) {
if (dataSize < 10) {
throw new IllegalStateException('Data size invalid when parse Date');
}
let v = new DataView(arrayBuffer, dataOffset, dataSize);
let timestamp = v.getFloat64(0, !le);
let localTimeOffset = v.getInt16(8, !le);
timestamp += localTimeOffset * 60 * 1000; // get UTC time
return {
data: new Date(timestamp),
size: 8 + 2
};
}
static parseValue(arrayBuffer, dataOffset, dataSize) {
if (dataSize < 1) {
throw new IllegalStateException('Data not enough when parse Value');
}
let v = new DataView(arrayBuffer, dataOffset, dataSize);
let offset = 1;
let type = v.getUint8(0);
let value;
let objectEnd = false;
try {
switch (type) {
case 0: // Number(Double) type
value = v.getFloat64(1, !le);
offset += 8;
break;
case 1: { // Boolean type
let b = v.getUint8(1);
value = b ? true : false;
offset += 1;
break;
}
case 2: { // String type
let amfstr = AMF.parseString(arrayBuffer, dataOffset + 1, dataSize - 1);
value = amfstr.data;
offset += amfstr.size;
break;
}
case 3: { // Object(s) type
value = {};
let terminal = 0; // workaround for malformed Objects which has missing ScriptDataObjectEnd
if ((v.getUint32(dataSize - 4, !le) & 0x00FFFFFF) === 9) {
terminal = 3;
}
while (offset < dataSize - 4) { // 4 === type(UI8) + ScriptDataObjectEnd(UI24)
let amfobj = AMF.parseObject(arrayBuffer, dataOffset + offset, dataSize - offset - terminal);
if (amfobj.objectEnd)
break;
value[amfobj.data.name] = amfobj.data.value;
offset += amfobj.size;
}
if (offset <= dataSize - 3) {
let marker = v.getUint32(offset - 1, !le) & 0x00FFFFFF;
if (marker === 9) {
offset += 3;
}
}
break;
}
case 8: { // ECMA array type (Mixed array)
value = {};
offset += 4; // ECMAArrayLength(UI32)
let terminal = 0; // workaround for malformed MixedArrays which has missing ScriptDataObjectEnd
if ((v.getUint32(dataSize - 4, !le) & 0x00FFFFFF) === 9) {
terminal = 3;
}
while (offset < dataSize - 8) { // 8 === type(UI8) + ECMAArrayLength(UI32) + ScriptDataVariableEnd(UI24)
let amfvar = AMF.parseVariable(arrayBuffer, dataOffset + offset, dataSize - offset - terminal);
if (amfvar.objectEnd)
break;
value[amfvar.data.name] = amfvar.data.value;
offset += amfvar.size;
}
if (offset <= dataSize - 3) {
let marker = v.getUint32(offset - 1, !le) & 0x00FFFFFF;
if (marker === 9) {
offset += 3;
}
}
break;
}
case 9: // ScriptDataObjectEnd
value = undefined;
offset = 1;
objectEnd = true;
break;
case 10: { // Strict array type
// ScriptDataValue[n]. NOTE: according to video_file_format_spec_v10_1.pdf
value = [];
let strictArrayLength = v.getUint32(1, !le);
offset += 4;
for (let i = 0; i < strictArrayLength; i++) {
let val = AMF.parseValue(arrayBuffer, dataOffset + offset, dataSize - offset);
value.push(val.data);
offset += val.size;
}
break;
}
case 11: { // Date type
let date = AMF.parseDate(arrayBuffer, dataOffset + 1, dataSize - 1);
value = date.data;
offset += date.size;
break;
}
case 12: { // Long string type
let amfLongStr = AMF.parseString(arrayBuffer, dataOffset + 1, dataSize - 1);
value = amfLongStr.data;
offset += amfLongStr.size;
break;
}
default:
// ignore and skip
offset = dataSize;
Log.w('AMF', 'Unsupported AMF value type ' + type);
}
} catch (e) {
Log.e('AMF', e.toString());
}
return {
data: value,
size: offset,
objectEnd: objectEnd
};
}
}
export default AMF;
================================================
FILE: src/demux/demux-errors.js
================================================
/*
* Copyright (C) 2016 Bilibili. All Rights Reserved.
*
* @author zheng qian <xqq@xqq.im>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const DemuxErrors = {
OK: 'OK',
FORMAT_ERROR: 'FormatError',
FORMAT_UNSUPPORTED: 'FormatUnsupported',
CODEC_UNSUPPORTED: 'CodecUnsupported'
};
export default DemuxErrors;
================================================
FILE: src/demux/exp-golomb.js
================================================
/*
* Copyright (C) 2016 Bilibili. All Rights Reserved.
*
* @author zheng qian <xqq@xqq.im>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {IllegalStateException, InvalidArgumentException} from '../utils/exception.js';
// Exponential-Golomb buffer decoder
class ExpGolomb {
constructor(uint8array) {
this.TAG = 'ExpGolomb';
this._buffer = uint8array;
this._buffer_index = 0;
this._total_bytes = uint8array.byteLength;
this._total_bits = uint8array.byteLength * 8;
this._current_word = 0;
this._current_word_bits_left = 0;
}
destroy() {
this._buffer = null;
}
_fillCurrentWord() {
let buffer_bytes_left = this._total_bytes - this._buffer_index;
if (buffer_bytes_left <= 0)
throw new IllegalStateException('ExpGolomb: _fillCurrentWord() but no bytes available');
let bytes_read = Math.min(4, buffer_bytes_left);
let word = new Uint8Array(4);
word.set(this._buffer.subarray(this._buffer_index, this._buffer_index + bytes_read));
this._current_word = new DataView(word.buffer).getUint32(0, false);
this._buffer_index += bytes_read;
this._current_word_bits_left = bytes_read * 8;
}
readBits(bits) {
if (bits > 32)
throw new InvalidArgumentException('ExpGolomb: readBits() bits exceeded max 32bits!');
if (bits <= this._current_word_bits_left) {
let result = this._current_word >>> (32 - bits);
this._current_word <<= bits;
this._current_word_bits_left -= bits;
return result;
}
let result = this._current_word_bits_left ? this._current_word : 0;
result = result >>> (32 - this._current_word_bits_left);
let bits_need_left = bits - this._current_word_bits_left;
this._fillCurrentWord();
let bits_read_next = Math.min(bits_need_left, this._current_word_bits_left);
let result2 = this._current_word >>> (32 - bits_read_next);
this._current_word <<= bits_read_next;
this._current_word_bits_left -= bits_read_next;
result = (result << bits_read_next) | result2;
return result;
}
readBool() {
return this.readBits(1) === 1;
}
readByte() {
return this.readBits(8);
}
_skipLeadingZero() {
let zero_count;
for (zero_count = 0; zero_count < this._current_word_bits_left; zero_count++) {
if (0 !== (this._current_word & (0x80000000 >>> zero_count))) {
this._current_word <<= zero_count;
this._current_word_bits_left -= zero_count;
return zero_count;
}
}
this._fillCurrentWord();
return zero_count + this._skipLeadingZero();
}
readUEG() { // unsigned exponential golomb
let leading_zeros = this._skipLeadingZero();
return this.readBits(leading_zeros + 1) - 1;
}
readSEG() { // signed exponential golomb
let value = this.readUEG();
if (value & 0x01) {
return (value + 1) >>> 1;
} else {
return -1 * (value >>> 1);
}
}
}
export default ExpGolomb;
================================================
FILE: src/demux/flv-demuxer.js
================================================
/*
* Copyright (C) 2016 Bilibili. All Rights Reserved.
*
* @author zheng qian <xqq@xqq.im>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Log from '../utils/logger.js';
import AMF from './amf-parser.js';
import SPSParser from './sps-parser.js';
import DemuxErrors from './demux-errors.js';
import MediaInfo from '../core/media-info.js';
import {IllegalStateException} from '../utils/exception.js';
function Swap16(src) {
return (((src >>> 8) & 0xFF) |
((src & 0xFF) << 8));
}
function Swap32(src) {
return (((src & 0xFF000000) >>> 24) |
((src & 0x00FF0000) >>> 8) |
((src & 0x0000FF00) << 8) |
((src & 0x000000FF) << 24));
}
function ReadBig32(array, index) {
return ((array[index] << 24) |
(array[index + 1] << 16) |
(array[index + 2] << 8) |
(array[index + 3]));
}
class FLVDemuxer {
constructor(probeData, config) {
this.TAG = 'FLVDemuxer';
this._config = config;
this._onError = null;
this._onMediaInfo = null;
this._onMetaDataArrived = null;
this._onScriptDataArrived = null;
this._onTrackMetadata = null;
this._onDataAvailable = null;
this._dataOffset = probeData.dataOffset;
this._firstParse = true;
this._dispatch = false;
this._hasAudio = probeData.hasAudioTrack;
this._hasVideo = probeData.hasVideoTrack;
this._hasAudioFlagOverrided = false;
this._hasVideoFlagOverrided = false;
this._audioInitialMetadataDispatched = false;
this._videoInitialMetadataDispatched = false;
this._mediaInfo = new MediaInfo();
this._mediaInfo.hasAudio = this._hasAudio;
this._mediaInfo.hasVideo = this._hasVideo;
this._metadata = null;
this._audioMetadata = null;
this._videoMetadata = null;
this._naluLengthSize = 4;
this._timestampBase = 0; // int32, in milliseconds
this._timescale = 1000;
this._duration = 0; // int32, in milliseconds
this._durationOverrided = false;
this._referenceFrameRate = {
fixed: true,
fps: 23.976,
fps_num: 23976,
fps_den: 1000
};
this._flvSoundRateTable = [5500, 11025, 22050, 44100, 48000];
this._mpegSamplingRates = [
96000, 88200, 64000, 48000, 44100, 32000,
24000, 22050, 16000, 12000, 11025, 8000, 7350
];
this._mpegAudioV10SampleRateTable = [44100, 48000, 32000, 0];
this._mpegAudioV20SampleRateTable = [22050, 24000, 16000, 0];
this._mpegAudioV25SampleRateTable = [11025, 12000, 8000, 0];
this._mpegAudioL1BitRateTable = [0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, -1];
this._mpegAudioL2BitRateTable = [0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, -1];
this._mpegAudioL3BitRateTable = [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, -1];
this._videoTrack = {type: 'video', id: 1, sequenceNumber: 0, samples: [], length: 0};
this._audioTrack = {type: 'audio', id: 2, sequenceNumber: 0, samples: [], length: 0};
this._littleEndian = (function () {
let buf = new ArrayBuffer(2);
(new DataView(buf)).setInt16(0, 256, true); // little-endian write
return (new Int16Array(buf))[0] === 256; // platform-spec read, if equal then LE
})();
}
destroy() {
this._mediaInfo = null;
this._metadata = null;
this._audioMetadata = null;
this._videoMetadata = null;
this._videoTrack = null;
this._audioTrack = null;
this._onError = null;
this._onMediaInfo = null;
this._onMetaDataArrived = null;
this._onScriptDataArrived = null;
this._onTrackMetadata = null;
this._onDataAvailable = null;
}
static probe(buffer) {
let data = new Uint8Array(buffer);
let mismatch = {match: false};
if (data[0] !== 0x46 || data[1] !== 0x4C || data[2] !== 0x56 || data[3] !== 0x01) {
return mismatch;
}
let hasAudio = ((data[4] & 4) >>> 2) !== 0;
let hasVideo = (data[4] & 1) !== 0;
let offset = ReadBig32(data, 5);
if (offset < 9) {
return mismatch;
}
return {
match: true,
consumed: offset,
dataOffset: offset,
hasAudioTrack: hasAudio,
hasVideoTrack: hasVideo
};
}
bindDataSource(loader) {
loader.onDataArrival = this.parseChunks.bind(this);
return this;
}
// prototype: function(type: string, metadata: any): void
get onTrackMetadata() {
return this._onTrackMetadata;
}
set onTrackMetadata(callback) {
this._onTrackMetadata = callback;
}
// prototype: function(mediaInfo: MediaInfo): void
get onMediaInfo() {
return this._onMediaInfo;
}
set onMediaInfo(callback) {
this._onMediaInfo = callback;
}
get onMetaDataArrived() {
return this._onMetaDataArrived;
}
set onMetaDataArrived(callback) {
this._onMetaDataArrived = callback;
}
get onScriptDataArrived() {
return this._onScriptDataArrived;
}
set onScriptDataArrived(callback) {
this._onScriptDataArrived = callback;
}
// prototype: function(type: number, info: string): void
get onError() {
return this._onError;
}
set onError(callback) {
this._onError = callback;
}
// prototype: function(videoTrack: any, audioTrack: any): void
get onDataAvailable() {
return this._onDataAvailable;
}
set onDataAvailable(callback) {
this._onDataAvailable = callback;
}
// timestamp base for output samples, must be in milliseconds
get timestampBase() {
return this._timestampBase;
}
set timestampBase(base) {
this._timestampBase = base;
}
get overridedDuration() {
return this._duration;
}
// Force-override media duration. Must be in milliseconds, int32
set overridedDuration(duration) {
this._durationOverrided = true;
this._duration = duration;
this._mediaInfo.duration = duration;
}
// Force-override audio track present flag, boolean
set overridedHasAudio(hasAudio) {
this._hasAudioFlagOverrided = true;
this._hasAudio = hasAudio;
this._mediaInfo.hasAudio = hasAudio;
}
// Force-override video track present flag, boolean
set overridedHasVideo(hasVideo) {
this._hasVideoFlagOverrided = true;
this._hasVideo = hasVideo;
this._mediaInfo.hasVideo = hasVideo;
}
resetMediaInfo() {
this._mediaInfo = new MediaInfo();
}
_isInitialMetadataDispatched() {
if (this._hasAudio && this._hasVideo) { // both audio & video
return this._audioInitialMetadataDispatched && this._videoInitialMetadataDispatched;
}
if (this._hasAudio && !this._hasVideo) { // audio only
return this._audioInitialMetadataDispatched;
}
if (!this._hasAudio && this._hasVideo) { // video only
return this._videoInitialMetadataDispatched;
}
return false;
}
// function parseChunks(chunk: ArrayBuffer, byteStart: number): number;
parseChunks(chunk, byteStart) {
if (!this._onError || !this._onMediaInfo || !this._onTrackMetadata || !this._onDataAvailable) {
throw new IllegalStateException('Flv: onError & onMediaInfo & onTrackMetadata & onDataAvailable callback must be specified');
}
let offset = 0;
let le = this._littleEndian;
if (byteStart === 0) { // buffer with FLV header
if (chunk.byteLength > 13) {
let probeData = FLVDemuxer.probe(chunk);
offset = probeData.dataOffset;
} else {
return 0;
}
}
if (this._firstParse) { // handle PreviousTagSize0 before Tag1
this._firstParse = false;
if (byteStart + offset !== this._dataOffset) {
Log.w(this.TAG, 'First time parsing but chunk byteStart invalid!');
}
let v = new DataView(chunk, offset);
let prevTagSize0 = v.getUint32(0, !le);
if (prevTagSize0 !== 0) {
Log.w(this.TAG, 'PrevTagSize0 !== 0 !!!');
}
offset += 4;
}
while (offset < chunk.byteLength) {
this._dispatch = true;
let v = new DataView(chunk, offset);
if (offset + 11 + 4 > chunk.byteLength) {
// data not enough for parsing an flv tag
break;
}
let tagType = v.getUint8(0);
let dataSize = v.getUint32(0, !le) & 0x00FFFFFF;
if (offset + 11 + dataSize + 4 > chunk.byteLength) {
// data not enough for parsing actual data body
break;
}
if (tagType !== 8 && tagType !== 9 && tagType !== 18) {
Log.w(this.TAG, `Unsupported tag type ${tagType}, skipped`);
// consume the whole tag (skip it)
offset += 11 + dataSize + 4;
continue;
}
let ts2 = v.getUint8(4);
let ts1 = v.getUint8(5);
let ts0 = v.getUint8(6);
let ts3 = v.getUint8(7);
let timestamp = ts0 | (ts1 << 8) | (ts2 << 16) | (ts3 << 24);
let streamId = v.getUint32(7, !le) & 0x00FFFFFF;
if (streamId !== 0) {
Log.w(this.TAG, 'Meet tag which has StreamID != 0!');
}
let dataOffset = offset + 11;
switch (tagType) {
case 8: // Audio
this._parseAudioData(chunk, dataOffset, dataSize, timestamp);
break;
case 9: // Video
this._parseVideoData(chunk, dataOffset, dataSize, timestamp, byteStart + offset);
break;
case 18: // ScriptDataObject
this._parseScriptData(chunk, dataOffset, dataSize);
break;
}
let prevTagSize = v.getUint32(11 + dataSize, !le);
if (prevTagSize !== 11 + dataSize) {
Log.w(this.TAG, `Invalid PrevTagSize ${prevTagSize}`);
}
offset += 11 + dataSize + 4; // tagBody + dataSize + prevTagSize
}
// dispatch parsed frames to consumer (typically, the remuxer)
if (this._isInitialMetadataDispatched()) {
if (this._dispatch && (this._audioTrack.length || this._videoTrack.length)) {
this._onDataAvailable(this._audioTrack, this._videoTrack);
}
}
return offset; // consumed bytes, just equals latest offset index
}
_parseScriptData(arrayBuffer, dataOffset, dataSize) {
let scriptData = AMF.parseScriptData(arrayBuffer, dataOffset, dataSize);
if (scriptData.hasOwnProperty('onMetaData')) {
if (scriptData.onMetaData == null || typeof scriptData.onMetaData !== 'object') {
Log.w(this.TAG, 'Invalid onMetaData structure!');
return;
}
if (this._metadata) {
Log.w(this.TAG, 'Found another onMetaData tag!');
}
this._metadata = scriptData;
let onMetaData = this._metadata.onMetaData;
if (this._onMetaDataArrived) {
this._onMetaDataArrived(Object.assign({}, onMetaData));
}
if (typeof onMetaData.hasAudio === 'boolean') { // hasAudio
if (this._hasAudioFlagOverrided === false) {
this._hasAudio = onMetaData.hasAudio;
this._mediaInfo.hasAudio = this._hasAudio;
}
}
if (typeof onMetaData.hasVideo === 'boolean') { // hasVideo
if (this._hasVideoFlagOverrided === false) {
this._hasVideo = onMetaData.hasVideo;
this._mediaInfo.hasVideo = this._hasVideo;
}
}
if (typeof onMetaData.audiodatarate === 'number') { // audiodatarate
this._mediaInfo.audioDataRate = onMetaData.audiodatarate;
}
if (typeof onMetaData.videodatarate === 'number') { // videodatarate
this._mediaInfo.videoDataRate = onMetaData.videodatarate;
}
if (typeof onMetaData.width === 'number') { // width
this._mediaInfo.width = onMetaData.width;
}
if (typeof onMetaData.height === 'number') { // height
this._mediaInfo.height = onMetaData.height;
}
if (typeof onMetaData.duration === 'number') { // duration
if (!this._durationOverrided) {
let duration = Math.floor(onMetaData.duration * this._timescale);
this._duration = duration;
this._mediaInfo.duration = duration;
}
} else {
this._mediaInfo.duration = 0;
}
if (typeof onMetaData.framerate === 'number') { // framerate
let fps_num = Math.floor(onMetaData.framerate * 1000);
if (fps_num > 0) {
let fps = fps_num / 1000;
this._referenceFrameRate.fixed = true;
this._referenceFrameRate.fps = fps;
this._referenceFrameRate.fps_num = fps_num;
this._referenceFrameRate.fps_den = 1000;
this._mediaInfo.fps = fps;
}
}
if (typeof onMetaData.keyframes === 'object') { // keyframes
this._mediaInfo.hasKeyframesIndex = true;
let keyframes = onMetaData.keyframes;
this._mediaInfo.keyframesIndex = this._parseKeyframesIndex(keyframes);
onMetaData.keyframes = null; // keyframes has been extracted, remove it
} else {
this._mediaInfo.hasKeyframesIndex = false;
}
this._dispatch = false;
this._mediaInfo.metadata = onMetaData;
Log.v(this.TAG, 'Parsed onMetaData');
if (this._mediaInfo.isComplete()) {
this._onMediaInfo(this._mediaInfo);
}
}
if (Object.keys(scriptData).length > 0) {
if (this._onScriptDataArrived) {
this._onScriptDataArrived(Object.assign({}, scriptData));
}
}
}
_parseKeyframesIndex(keyframes) {
let times = [];
let filepositions = [];
// ignore first keyframe which is actually AVC Sequence Header (AVCDecoderConfigurationRecord)
for (let i = 1; i < keyframes.times.length; i++) {
let time = this._timestampBase + Math.floor(keyframes.times[i] * 1000);
times.push(time);
filepositions.push(keyframes.filepositions[i]);
}
return {
times: times,
filepositions: filepositions
};
}
_parseAudioData(arrayBuffer, dataOffset, dataSize, tagTimestamp) {
if (dataSize <= 1) {
Log.w(this.TAG, 'Flv: Invalid audio packet, missing SoundData payload!');
return;
}
if (this._hasAudioFlagOverrided === true && this._hasAudio === false) {
// If hasAudio: false indicated explicitly in MediaDataSource,
// Ignore all the audio packets
return;
}
let le = this._littleEndian;
let v = new DataView(arrayBuffer, dataOffset, dataSize);
let soundSpec = v.getUint8(0);
let soundFormat = soundSpec >>> 4;
if (soundFormat !== 2 && soundFormat !== 10) { // MP3 or AAC
this._onError(DemuxErrors.CODEC_UNSUPPORTED, 'Flv: Unsupported audio codec idx: ' + soundFormat);
return;
}
let soundRate = 0;
let soundRateIndex = (soundSpec & 12) >>> 2;
if (soundRateIndex >= 0 && soundRateIndex <= 4) {
soundRate = this._flvSoundRateTable[soundRateIndex];
} else {
this._onError(DemuxErrors.FORMAT_ERROR, 'Flv: Invalid audio sample rate idx: ' + soundRateIndex);
return;
}
let soundSize = (soundSpec & 2) >>> 1; // unused
let soundType = (soundSpec & 1);
let meta = this._audioMetadata;
let track = this._audioTrack;
if (!meta) {
if (this._hasAudio === false && this._hasAudioFlagOverrided === false) {
this._hasAudio = true;
this._mediaInfo.hasAudio = true;
}
// initial metadata
meta = this._audioMetadata = {};
meta.type = 'audio';
meta.id = track.id;
meta.timescale = this._timescale;
meta.duration = this._duration;
meta.audioSampleRate = soundRate;
meta.channelCount = (soundType === 0 ? 1 : 2);
}
if (soundFormat === 10) { // AAC
let aacData = this._parseAACAudioData(arrayBuffer, dataOffset + 1, dataSize - 1);
if (aacData == undefined) {
return;
}
if (aacData.packetType === 0) { // AAC sequence header (AudioSpecificConfig)
if (meta.config) {
Log.w(this.TAG, 'Found another AudioSpecificConfig!');
}
let misc = aacData.data;
meta.audioSampleRate = misc.samplingRate;
meta.channelCount = misc.channelCount;
meta.codec = misc.codec;
meta.originalCodec = misc.originalCodec;
meta.config = misc.config;
// The decode result of an aac sample is 1024 PCM samples
meta.refSampleDuration = 1024 / meta.audioSampleRate * meta.timescale;
Log.v(this.TAG, 'Parsed AudioSpecificConfig');
if (this._isInitialMetadataDispatched()) {
// Non-initial metadata, force dispatch (or flush) parsed frames to remuxer
if (this._dispatch && (this._audioTrack.length || this._videoTrack.length)) {
this._onDataAvailable(this._audioTrack, this._videoTrack);
}
} else {
this._audioInitialMetadataDispatched = true;
}
// then notify new metadata
this._dispatch = false;
this._onTrackMetadata('audio', meta);
let mi = this._mediaInfo;
mi.audioCodec = meta.originalCodec;
mi.audioSampleRate = meta.audioSampleRate;
mi.audioChannelCount = meta.channelCount;
if (mi.hasVideo) {
if (mi.videoCodec != null) {
mi.mimeType = 'video/x-flv; codecs="' + mi.videoCodec + ',' + mi.audioCodec + '"';
}
} else {
mi.mimeType = 'video/x-flv; codecs="' + mi.audioCodec + '"';
}
if (mi.isComplete()) {
this._onMediaInfo(mi);
}
} else if (aacData.packetType === 1) { // AAC raw frame data
let dts = this._timestampBase + tagTimestamp;
let aacSample = {unit: aacData.data, length: aacData.data.byteLength, dts: dts, pts: dts};
track.samples.push(aacSample);
track.length += aacData.data.length;
} else {
Log.e(this.TAG, `Flv: Unsupported AAC data type ${aacData.packetType}`);
}
} else if (soundFormat === 2) { // MP3
if (!meta.codec) {
// We need metadata for mp3 audio track, extract info from frame header
let misc = this._parseMP3AudioData(arrayBuffer, dataOffset + 1, dataSize - 1, true);
if (misc == undefined) {
return;
}
meta.audioSampleRate = misc.samplingRate;
meta.channelCount = misc.channelCount;
meta.codec = misc.codec;
meta.originalCodec = misc.originalCodec;
// The decode result of an mp3 sample is 1152 PCM samples
meta.refSampleDuration = 1152 / meta.audioSampleRate * meta.timescale;
Log.v(this.TAG, 'Parsed MPEG Audio Frame Header');
this._audioInitialMetadataDispatched = true;
this._onTrackMetadata('audio', meta);
let mi = this._mediaInfo;
mi.audioCodec = meta.codec;
mi.audioSampleRate = meta.audioSampleRate;
mi.audioChannelCount = meta.channelCount;
mi.audioDataRate = misc.bitRate;
if (mi.hasVideo) {
if (mi.videoCodec != null) {
mi.mimeType = 'video/x-flv; codecs="' + mi.videoCodec + ',' + mi.audioCodec + '"';
}
} else {
mi.mimeType = 'video/x-flv; codecs="' + mi.audioCodec + '"';
}
if (mi.isComplete()) {
this._onMediaInfo(mi);
}
}
// This packet is always a valid audio packet, extract it
let data = this._parseMP3AudioData(arrayBuffer, dataOffset + 1, dataSize - 1, false);
if (data == undefined) {
return;
}
let dts = this._timestampBase + tagTimestamp;
let mp3Sample = {unit: data, length: data.byteLength, dts: dts, pts: dts};
track.samples.push(mp3Sample);
track.length += data.length;
}
}
_parseAACAudioData(arrayBuffer, dataOffset, dataSize) {
if (dataSize <= 1) {
Log.w(this.TAG, 'Flv: Invalid AAC packet, missing AACPacketType or/and Data!');
return;
}
let result = {};
let array = new Uint8Array(arrayBuffer, dataOffset, dataSize);
result.packetType = array[0];
if (array[0] === 0) {
result.data = this._parseAACAudioSpecificConfig(arrayBuffer, dataOffset + 1, dataSize - 1);
} else {
result.data = array.subarray(1);
}
return result;
}
_parseAACAudioSpecificConfig(arrayBuffer, dataOffset, dataSize) {
let array = new Uint8Array(arrayBuffer, dataOffset, dataSize);
let config = null;
/* Audio Object Type:
0: Null
1: AAC Main
2: AAC LC
3: AAC SSR (Scalable Sample Rate)
4: AAC LTP (Long Term Prediction)
5: HE-AAC / SBR (Spectral Band Replication)
6: AAC Scalable
*/
let audioObjectType = 0;
let originalAudioObjectType = 0;
let audioExtensionObjectType = null;
let samplingIndex = 0;
let extensionSamplingIndex = null;
// 5 bits
audioObjectType = originalAudioObjectType = array[0] >>> 3;
// 4 bits
samplingIndex = ((array[0] & 0x07) << 1) | (array[1] >>> 7);
if (samplingIndex < 0 || samplingIndex >= this._mpegSamplingRates.length) {
this._onError(DemuxErrors.FORMAT_ERROR, 'Flv: AAC invalid sampling frequency index!');
return;
}
let samplingFrequence = this._mpegSamplingRates[samplingIndex];
// 4 bits
let channelConfig = (array[1] & 0x78) >>> 3;
if (channelConfig < 0 || channelConfig >= 8) {
this._onError(DemuxErrors.FORMAT_ERROR, 'Flv: AAC invalid channel configuration');
return;
}
if (audioObjectType === 5) { // HE-AAC?
// 4 bits
extensionSamplingIndex = ((array[1] & 0x07) << 1) | (array[2] >>> 7);
// 5 bits
audioExtensionObjectType = (array[2] & 0x7C) >>> 2;
}
// workarounds for various browsers
let userAgent = self.navigator.userAgent.toLowerCase();
if (userAgent.indexOf('firefox') !== -1) {
// firefox: use SBR (HE-AAC) if freq less than 24kHz
if (samplingIndex >= 6) {
audioObjectType = 5;
config = new Array(4);
extensionSamplingIndex = samplingIndex - 3;
} else { // use LC-AAC
audioObjectType = 2;
config = new Array(2);
extensionSamplingIndex = samplingIndex;
}
} else if (userAgent.indexOf('android') !== -1) {
// android: always use LC-AAC
audioObjectType = 2;
config = new Array(2);
extensionSamplingIndex = samplingIndex;
} else {
// for other browsers, e.g. chrome...
// Always use HE-AAC to make it easier to switch aac codec profile
audioObjectType = 5;
extensionSamplingIndex = samplingIndex;
config = new Array(4);
if (samplingIndex >= 6) {
extensionSamplingIndex = samplingIndex - 3;
} else if (channelConfig === 1) { // Mono channel
audioObjectType = 2;
config = new Array(2);
extensionSamplingIndex = samplingIndex;
}
}
config[0] = audioObjectType << 3;
config[0] |= (samplingIndex & 0x0F) >>> 1;
config[1] = (samplingIndex & 0x0F) << 7;
config[1] |= (channelConfig & 0x0F) << 3;
if (audioObjectType === 5) {
config[1] |= ((extensionSamplingIndex & 0x0F) >>> 1);
config[2] = (extensionSamplingIndex & 0x01) << 7;
// extended audio object type: force to 2 (LC-AAC)
config[2] |= (2 << 2);
config[3] = 0;
}
return {
config: config,
samplingRate: samplingFrequence,
channelCount: channelConfig,
codec: 'mp4a.40.' + audioObjectType,
originalCodec: 'mp4a.40.' + originalAudioObjectType
};
}
_parseMP3AudioData(arrayBuffer, dataOffset, dataSize, requestHeader) {
if (dataSize < 4) {
Log.w(this.TAG, 'Flv: Invalid MP3 packet, header missing!');
return;
}
let le = this._littleEndian;
let array = new Uint8Array(arrayBuffer, dataOffset, dataSize);
let result = null;
if (requestHeader) {
if (array[0] !== 0xFF) {
return;
}
let ver = (array[1] >>> 3) & 0x03;
let layer = (array[1] & 0x06) >> 1;
let bitrate_index = (array[2] & 0xF0) >>> 4;
let sampling_freq_index = (array[2] & 0x0C) >>> 2;
let channel_mode = (array[3] >>> 6) & 0x03;
let channel_count = channel_mode !== 3 ? 2 : 1;
let sample_rate = 0;
let bit_rate = 0;
let object_type = 34; // Layer-3, listed in MPEG-4 Audio Object Types
let codec = 'mp3';
switch (ver) {
case 0: // MPEG 2.5
sample_rate = this._mpegAudioV25SampleRateTable[sampling_freq_index];
break;
case 2: // MPEG 2
sample_rate = this._mpegAudioV20SampleRateTable[sampling_freq_index];
break;
case 3: // MPEG 1
sample_rate = this._mpegAudioV10SampleRateTable[sampling_freq_index];
break;
}
switch (layer) {
case 1: // Layer 3
object_type = 34;
if (bitrate_index < this._mpegAudioL3BitRateTable.length) {
bit_rate = this._mpegAudioL3BitRateTable[bitrate_index];
}
break;
case 2: // Layer 2
object_type = 33;
if (bitrate_index < this._mpegAudioL2BitRateTable.length) {
bit_rate = this._mpegAudioL2BitRateTable[bitrate_index];
}
break;
case 3: // Layer 1
object_type = 32;
if (bitrate_index < this._mpegAudioL1BitRateTable.length) {
bit_rate = this._mpegAudioL1BitRateTable[bitrate_index];
}
break;
}
result = {
bitRate: bit_rate,
samplingRate: sample_rate,
channelCount: channel_count,
codec: codec,
originalCodec: codec
};
} else {
result = array;
}
return result;
}
_parseVideoData(arrayBuffer, dataOffset, dataSize, tagTimestamp, tagPosition) {
if (dataSize <= 1) {
Log.w(this.TAG, 'Flv: Invalid video packet, missing VideoData payload!');
return;
}
if (this._hasVideoFlagOverrided === true && this._hasVideo === false) {
// If hasVideo: false indicated explicitly in MediaDataSource,
// Ignore all the video packets
return;
}
let spec = (new Uint8Array(arrayBuffer, dataOffset, dataSize))[0];
let frameType = (spec & 240) >>> 4;
let codecId = spec & 15;
if (codecId !== 7) {
this._onError(DemuxErrors.CODEC_UNSUPPORTED, `Flv: Unsupported codec in video frame: ${codecId}`);
return;
}
this._parseAVCVideoPacket(arrayBuffer, dataOffset + 1, dataSize - 1, tagTimestamp, tagPosition, frameType);
}
_parseAVCVideoPacket(arrayBuffer, dataOffset, dataSize, tagTimestamp, tagPosition, frameType) {
if (dataSize < 4) {
Log.w(this.TAG, 'Flv: Invalid AVC packet, missing AVCPacketType or/and CompositionTime');
return;
}
let le = this._littleEndian;
let v = new DataView(arrayBuffer, dataOffset, dataSize);
let packetType = v.getUint8(0);
let cts_unsigned = v.getUint32(0, !le) & 0x00FFFFFF;
let cts = (cts_unsigned << 8) >> 8; // convert to 24-bit signed int
if (packetType === 0) { // AVCDecoderConfigurationRecord
this._parseAVCDecoderConfigurationRecord(arrayBuffer, dataOffset + 4, dataSize - 4);
} else if (packetType === 1) { // One or more Nalus
this._parseAVCVideoData(arrayBuffer, dataOffset + 4, dataSize - 4, tagTimestamp, tagPosition, frameType, cts);
} else if (packetType === 2) {
// empty, AVC end of sequence
} else {
this._onError(DemuxErrors.FORMAT_ERROR, `Flv: Invalid video packet type ${packetType}`);
return;
}
}
_parseAVCDecoderConfigurationRecord(arrayBuffer, dataOffset, dataSize) {
if (dataSize < 7) {
Log.w(this.TAG, 'Flv: Invalid AVCDecoderConfigurationRecord, lack of data!');
return;
}
let meta = this._videoMetadata;
let track = this._videoTrack;
let le = this._littleEndian;
let v = new DataView(arrayBuffer, dataOffset, dataSize);
if (!meta) {
if (this._hasVideo === false && this._hasVideoFlagOverrided === false) {
this._hasVideo = true;
this._mediaInfo.hasVideo = true;
}
meta = this._videoMetadata = {};
meta.type = 'video';
meta.id = track.id;
meta.timescale = this._timescale;
meta.duration = this._duration;
} else {
if (typeof meta.avcc !== 'undefined') {
Log.w(this.TAG, 'Found another AVCDecoderConfigurationRecord!');
}
}
let version = v.getUint8(0); // configurationVersion
let avcProfile = v.getUint8(1); // avcProfileIndication
let profileCompatibility = v.getUint8(2); // profile_compatibility
let avcLevel = v.getUint8(3); // AVCLevelIndication
if (version !== 1 || avcProfile === 0) {
this._onError(DemuxErrors.FORMAT_ERROR, 'Flv: Invalid AVCDecoderConfigurationRecord');
return;
}
this._naluLengthSize = (v.getUint8(4) & 3) + 1; // lengthSizeMinusOne
if (this._naluLengthSize !== 3 && this._naluLengthSize !== 4) { // holy shit!!!
this._onError(DemuxErrors.FORMAT_ERROR, `Flv: Strange NaluLengthSizeMinusOne: ${this._naluLengthSize - 1}`);
return;
}
let spsCount = v.getUint8(5) & 31; // numOfSequenceParameterSets
if (spsCount === 0) {
this._onError(DemuxErrors.FORMAT_ERROR, 'Flv: Invalid AVCDecoderConfigurationRecord: No SPS');
return;
} else if (spsCount > 1) {
Log.w(this.TAG, `Flv: Strange AVCDecoderConfigurationRecord: SPS Count = ${spsCount}`);
}
let offset = 6;
for (let i = 0; i < spsCount; i++) {
let len = v.getUint16(offset, !le); // sequenceParameterSetLength
offset += 2;
if (len === 0) {
continue;
}
// Notice: Nalu without startcode header (00 00 00 01)
let sps = new Uint8Array(arrayBuffer, dataOffset + offset, len);
offset += len;
let config = SPSParser.parseSPS(sps);
if (i !== 0) {
// ignore other sps's config
continue;
}
meta.codecWidth = config.codec_size.width;
meta.codecHeight = config.codec_size.height;
meta.presentWidth = config.present_size.width;
meta.presentHeight = config.present_size.height;
meta.profile = config.profile_string;
meta.level = config.level_string;
meta.bitDepth = config.bit_depth;
meta.chromaFormat = config.chroma_format;
meta.sarRatio = config.sar_ratio;
meta.frameRate = config.frame_rate;
if (config.frame_rate.fixed === false ||
config.frame_rate.fps_num === 0 ||
config.frame_rate.fps_den === 0) {
meta.frameRate = this._referenceFrameRate;
}
let fps_den = meta.frameRate.fps_den;
let fps_num = meta.frameRate.fps_num;
meta.refSampleDuration = meta.timescale * (fps_den / fps_num);
let codecArray = sps.subarray(1, 4);
let codecString = 'avc1.';
for (let j = 0; j < 3; j++) {
let h = codecArray[j].toString(16);
if (h.length < 2) {
h = '0' + h;
}
codecString += h;
}
meta.codec = codecString;
let mi = this._mediaInfo;
mi.width = meta.codecWidth;
mi.height = meta.codecHeight;
mi.fps = meta.frameRate.fps;
mi.profile = meta.profile;
mi.level = meta.level;
mi.refFrames = config.ref_frames;
mi.chromaFormat = config.chroma_format_string;
mi.sarNum = meta.sarRatio.width;
mi.sarDen = meta.sarRatio.height;
mi.videoCodec = codecString;
if (mi.hasAudio) {
if (mi.audioCodec != null) {
mi.mimeType = 'video/x-flv; codecs="' + mi.videoCodec + ',' + mi.audioCodec + '"';
}
} else {
mi.mimeType = 'video/x-flv; codecs="' + mi.videoCodec + '"';
}
if (mi.isComplete()) {
this._onMediaInfo(mi);
}
}
let ppsCount = v.getUint8(offset); // numOfPictureParameterSets
if (ppsCount === 0) {
this._onError(DemuxErrors.FORMAT_ERROR, 'Flv: Invalid AVCDecoderConfigurationRecord: No PPS');
return;
} else if (ppsCount > 1) {
Log.w(this.TAG, `Flv: Strange AVCDecoderConfigurationRecord: PPS Count = ${ppsCount}`);
}
offset++;
for (let i = 0; i < ppsCount; i++) {
let len = v.getUint16(offset, !le); // pictureParameterSetLength
offset += 2;
if (len === 0) {
continue;
}
// pps is useless for extracting video information
offset += len;
}
meta.avcc = new Uint8Array(dataSize);
meta.avcc.set(new Uint8Array(arrayBuffer, dataOffset, dataSize), 0);
Log.v(this.TAG, 'Parsed AVCDecoderConfigurationRecord');
if (this._isInitialMetadataDispatched()) {
// flush parsed frames
if (this._dispatch && (this._audioTrack.length || this._videoTrack.length)) {
this._onDataAvailable(this._audioTrack, this._videoTrack);
}
} else {
this._videoInitialMetadataDispatched = true;
}
// notify new metadata
this._dispatch = false;
this._onTrackMetadata('video', meta);
}
_parseAVCVideoData(arrayBuffer, dataOffset, dataSize, tagTimestamp, tagPosition, frameType, cts) {
let le = this._littleEndian;
let v = new DataView(arrayBuffer, dataOffset, dataSize);
let units = [], length = 0;
let offset = 0;
const lengthSize = this._naluLengthSize;
let dts = this._timestampBase + tagTimestamp;
let keyframe = (frameType === 1); // from FLV Frame Type constants
while (offset < dataSize) {
if (offset + 4 >= dataSize) {
Log.w(this.TAG, `Malformed Nalu near timestamp ${dts}, offset = ${offset}, dataSize = ${dataSize}`);
break; // data not enough for next Nalu
}
// Nalu with length-header (AVC1)
let naluSize = v.getUint32(offset, !le); // Big-Endian read
if (lengthSize === 3) {
naluSize >>>= 8;
}
if (naluSize > dataSize - lengthSize) {
Log.w(this.TAG, `Malformed Nalus near timestamp ${dts}, NaluSize > DataSize!`);
return;
}
let unitType = v.getUint8(offset + lengthSize) & 0x1F;
if (unitType === 5) { // IDR
keyframe = true;
}
let data = new Uint8Array(arrayBuffer, dataOffset + offset, lengthSize + naluSize);
let unit = {type: unitType, data: data};
units.push(unit);
length += data.byteLength;
offset += lengthSize + naluSize;
}
if (units.length) {
let track = this._videoTrack;
let avcSample = {
units: units,
length: length,
isKeyframe: keyframe,
dts: dts,
cts: cts,
pts: (dts + cts)
};
if (keyframe) {
avcSample.fileposition = tagPosition;
}
track.samples.push(avcSample);
track.length += length;
}
}
}
export default FLVDemuxer;
================================================
FILE: src/demux/sps-parser.js
================================================
/*
* Copyright (C) 2016 Bilibili. All Rights Reserved.
*
* @author zheng qian <xqq@xqq.im>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import ExpGolomb from './exp-golomb.js';
class SPSParser {
static _ebsp2rbsp(uint8array) {
let src = uint8array;
let src_length = src.byteLength;
let dst = new Uint8Array(src_length);
let dst_idx = 0;
for (let i = 0; i < src_length; i++) {
if (i >= 2) {
// Unescape: Skip 0x03 after 00 00
if (src[i] === 0x03 && src[i - 1] === 0x00 && src[i - 2] === 0x00) {
continue;
}
}
dst[dst_idx] = src[i];
dst_idx++;
}
return new Uint8Array(dst.buffer, 0, dst_idx);
}
static parseSPS(uint8array) {
let rbsp = SPSParser._ebsp2rbsp(uint8array);
let gb = new ExpGolomb(rbsp);
gb.readByte();
let profile_idc = gb.readByte(); // profile_idc
gb.readByte(); // constraint_set_flags[5] + reserved_zero[3]
let level_idc = gb.readByte(); // level_idc
gb.readUEG(); // seq_parameter_set_id
let profile_string = SPSParser.getProfileString(profile_idc);
let level_string = SPSParser.getLevelString(level_idc);
let chroma_format_idc = 1;
let chroma_format = 420;
let chroma_format_table = [0, 420, 422, 444];
let bit_depth = 8;
if (profile_idc === 100 || profile_idc === 110 || profile_idc === 122 ||
profile_idc === 244 || profile_idc === 44 || profile_idc === 83 ||
profile_idc === 86 || profile_idc === 118 || profile_idc === 128 ||
profile_idc === 138 || profile_idc === 144) {
chroma_format_idc = gb.readUEG();
if (chroma_format_idc === 3) {
gb.readBits(1); // separate_colour_plane_flag
}
if (chroma_format_idc <= 3) {
chroma_format = chroma_format_table[chroma_format_idc];
}
bit_depth = gb.readUEG() + 8; // bit_depth_luma_minus8
gb.readUEG(); // bit_depth_chroma_minus8
gb.readBits(1); // qpprime_y_zero_transform_bypass_flag
if (gb.readBool()) { // seq_scaling_matrix_present_flag
let scaling_list_count = (chroma_format_idc !== 3) ? 8 : 12;
for (let i = 0; i < scaling_list_count; i++) {
if (gb.readBool()) { // seq_scaling_list_present_flag
if (i < 6) {
SPSParser._skipScalingList(gb, 16);
} else {
SPSParser._skipScalingList(gb, 64);
}
}
}
}
}
gb.readUEG(); // log2_max_frame_num_minus4
let pic_order_cnt_type = gb.readUEG();
if (pic_order_cnt_type === 0) {
gb.readUEG(); // log2_max_pic_order_cnt_lsb_minus_4
} else if (pic_order_cnt_type === 1) {
gb.readBits(1); // delta_pic_order_always_zero_flag
gb.readSEG(); // offset_for_non_ref_pic
gb.readSEG(); // offset_for_top_to_bottom_field
let num_ref_frames_in_pic_order_cnt_cycle = gb.readUEG();
for (let i = 0; i < num_ref_frames_in_pic_order_cnt_cycle; i++) {
gb.readSEG(); // offset_for_ref_frame
}
}
let ref_frames = gb.readUEG(); // max_num_ref_frames
gb.readBits(1); // gaps_in_frame_num_value_allowed_flag
let pic_width_in_mbs_minus1 = gb.readUEG();
let pic_height_in_map_units_minus1 = gb.readUEG();
let frame_mbs_only_flag = gb.readBits(1);
if (frame_mbs_only_flag === 0) {
gb.readBits(1); // mb_adaptive_frame_field_flag
}
gb.readBits(1); // direct_8x8_inference_flag
let frame_crop_left_offset = 0;
let frame_crop_right_offset = 0;
let frame_crop_top_offset = 0;
let frame_crop_bottom_offset = 0;
let frame_cropping_flag = gb.readBool();
if (frame_cropping_flag) {
frame_crop_left_offset = gb.readUEG();
frame_crop_right_offset = gb.readUEG();
frame_crop_top_offset = gb.readUEG();
frame_crop_bottom_offset = gb.readUEG();
}
let sar_width = 1, sar_height = 1;
let fps = 0, fps_fixed = true, fps_num = 0, fps_den = 0;
let vui_parameters_present_flag = gb.readBool();
if (vui_parameters_present_flag) {
if (gb.readBool()) { // aspect_ratio_info_present_flag
let aspect_ratio_idc = gb.readByte();
let sar_w_table = [1, 12, 10, 16, 40, 24, 20, 32, 80, 18, 15, 64, 160, 4, 3, 2];
let sar_h_table = [1, 11, 11, 11, 33, 11, 11, 11, 33, 11, 11, 33, 99, 3, 2, 1];
if (aspect_ratio_idc > 0 && aspect_ratio_idc < 16) {
sar_width = sar_w_table[aspect_ratio_idc - 1];
sar_height = sar_h_table[aspect_ratio_idc - 1];
} else if (aspect_ratio_idc === 255) {
sar_width = gb.readByte() << 8 | gb.readByte();
sar_height = gb.readByte() << 8 | gb.readByte();
}
}
if (gb.readBool()) { // overscan_info_present_flag
gb.readBool(); // overscan_appropriate_flag
}
if (gb.readBool()) { // video_signal_type_present_flag
gb.readBits(4); // video_format & video_full_range_flag
if (gb.readBool()) { // colour_description_present_flag
gb.readBits(24); // colour_primaries & transfer_characteristics & matrix_coefficients
}
}
if (gb.readBool()) { // chroma_loc_info_present_flag
gb.readUEG(); // chroma_sample_loc_type_top_field
gb.readUEG(); // chroma_sample_loc_type_bottom_field
}
if (gb.readBool()) { // timing_info_present_flag
let num_units_in_tick = gb.readBits(32);
let time_scale = gb.readBits(32);
fps_fixed = gb.readBool(); // fixed_frame_rate_flag
fps_num = time_scale;
fps_den = num_units_in_tick * 2;
fps = fps_num / fps_den;
}
}
let sarScale = 1;
if (sar_width !== 1 || sar_height !== 1) {
sarScale = sar_width / sar_height;
}
let crop_unit_x = 0, crop_unit_y = 0;
if (chroma_format_idc === 0) {
crop_unit_x = 1;
crop_unit_y = 2 - frame_mbs_only_flag;
} else {
let sub_wc = (chroma_format_idc === 3) ? 1 : 2;
let sub_hc = (chroma_format_idc === 1) ? 2 : 1;
crop_unit_x = sub_wc;
crop_unit_y = sub_hc * (2 - frame_mbs_only_flag);
}
let codec_width = (pic_width_in_mbs_minus1 + 1) * 16;
let codec_height = (2 - frame_mbs_only_flag) * ((pic_height_in_map_units_minus1 + 1) * 16);
codec_width -= (frame_crop_left_offset + frame_crop_right_offset) * crop_unit_x;
codec_height -= (frame_crop_top_offset + frame_crop_bottom_offset) * crop_unit_y;
let present_width = Math.ceil(codec_width * sarScale);
gb.destroy();
gb = null;
return {
profile_string: profile_string, // baseline, high, high10, ...
level_string: level_string, // 3, 3.1, 4, 4.1, 5, 5.1, ...
bit_depth: bit_depth, // 8bit, 10bit, ...
ref_frames: ref_frames,
chroma_format: chroma_format, // 4:2:0, 4:2:2, ...
chroma_format_string: SPSParser.getChromaFormatString(chroma_format),
frame_rate: {
fixed: fps_fixed,
fps: fps,
fps_den: fps_den,
fps_num: fps_num
},
sar_ratio: {
width: sar_width,
height: sar_height
},
codec_size: {
width: codec_width,
height: codec_height
},
present_size: {
width: present_width,
height: codec_height
}
};
}
static _skipScalingList(gb, count) {
let last_scale = 8, next_scale = 8;
let delta_scale = 0;
for (let i = 0; i < count; i++) {
if (next_scale !== 0) {
delta_scale = gb.readSEG();
next_scale = (last_scale + delta_scale + 256) % 256;
}
last_scale = (next_scale === 0) ? last_scale : next_scale;
}
}
static getProfileString(profile_idc) {
switch (profile_idc) {
case 66:
return 'Baseline';
case 77:
return 'Main';
case 88:
return 'Extended';
case 100:
return 'High';
case 110:
return 'High10';
case 122:
return 'High422';
case 244:
return 'High444';
default:
return 'Unknown';
}
}
static getLevelString(level_idc) {
return (level_idc / 10).toFixed(1);
}
static getChromaFormatString(chroma) {
switch (chroma) {
case 420:
return '4:2:0';
case 422:
return '4:2:2';
case 444:
return '4:4:4';
default:
return 'Unknown';
}
}
}
export default SPSParser;
================================================
FILE: src/flv.js
================================================
/*
* Copyright (C) 2016 Bilibili. All Rights Reserved.
*
* @author zheng qian <xqq@xqq.im>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Polyfill from './utils/polyfill.js';
import Features from './core/features.js';
import {BaseLoader, LoaderStatus, LoaderErrors} from './io/loader.js';
import FlvPlayer from './player/flv-player.js';
import NativePlayer from './player/native-player.js';
import PlayerEvents from './player/player-events.js';
import {ErrorTypes, ErrorDetails} from './player/player-errors.js';
import LoggingControl from './utils/logging-control.js';
import {InvalidArgumentException} from './utils/exception.js';
// here are all the interfaces
// install polyfills
Polyfill.install();
// factory method
function createPlayer(mediaDataSource, optionalConfig) {
let mds = mediaDataSource;
if (mds == null || typeof mds !== 'object') {
throw new InvalidArgumentException('MediaDataSource must be an javascript object!');
}
if (!mds.hasOwnProperty('type')) {
throw new InvalidArgumentException('MediaDataSource must has type field to indicate video file type!');
}
switch (mds.type) {
case 'flv':
return new FlvPlayer(mds, optionalConfig);
default:
return new NativePlayer(mds, optionalConfig);
}
}
// feature detection
function isSupported() {
return Features.supportMSEH264Playback();
}
function getFeatureList() {
return Features.getFeatureList();
}
// interfaces
let flvjs = {};
flvjs.createPlayer = createPlayer;
flvjs.isSupported = isSupported;
flvjs.getFeatureList = getFeatureList;
flvjs.BaseLoader = BaseLoader;
flvjs.LoaderStatus = LoaderStatus;
flvjs.LoaderErrors = LoaderErrors;
flvjs.Events = PlayerEvents;
flvjs.ErrorTypes = ErrorTypes;
flvjs.ErrorDetails = ErrorDetails;
flvjs.FlvPlayer = FlvPlayer;
flvjs.NativePlayer = NativePlayer;
flvjs.LoggingControl = Logging
gitextract_l0qlpdgd/ ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── d.ts/ │ └── flv.d.ts ├── demo/ │ ├── demo.css │ └── index.html ├── docs/ │ ├── api.md │ ├── cors.md │ ├── design.md │ ├── livestream.md │ └── multipart.md ├── package.json ├── src/ │ ├── config.js │ ├── core/ │ │ ├── features.js │ │ ├── media-info.js │ │ ├── media-segment-info.js │ │ ├── mse-controller.js │ │ ├── mse-events.js │ │ ├── transmuxer.js │ │ ├── transmuxing-controller.js │ │ ├── transmuxing-events.js │ │ └── transmuxing-worker.js │ ├── demux/ │ │ ├── amf-parser.js │ │ ├── demux-errors.js │ │ ├── exp-golomb.js │ │ ├── flv-demuxer.js │ │ └── sps-parser.js │ ├── flv.js │ ├── index.js │ ├── io/ │ │ ├── fetch-stream-loader.js │ │ ├── io-controller.js │ │ ├── loader.js │ │ ├── param-seek-handler.js │ │ ├── range-seek-handler.js │ │ ├── speed-sampler.js │ │ ├── websocket-loader.js │ │ ├── xhr-moz-chunked-loader.js │ │ ├── xhr-msstream-loader.js │ │ └── xhr-range-loader.js │ ├── player/ │ │ ├── flv-player.js │ │ ├── native-player.js │ │ ├── player-errors.js │ │ └── player-events.js │ ├── remux/ │ │ ├── aac-silent.js │ │ ├── mp4-generator.js │ │ └── mp4-remuxer.js │ └── utils/ │ ├── browser.js │ ├── exception.js │ ├── logger.js │ ├── logging-control.js │ ├── polyfill.js │ └── utf8-conv.js ├── tsconfig.json ├── tslint.json ├── types/ │ ├── index.d.ts │ ├── test-flv.ts │ └── tsconfig.json └── webpack.config.js
SYMBOL INDEX (513 symbols across 36 files)
FILE: d.ts/flv.d.ts
type MediaSegment (line 22) | interface MediaSegment {
type MediaDataSource (line 28) | interface MediaDataSource {
type Config (line 44) | interface Config {
type CustomSeekHandlerConstructor (line 170) | interface CustomSeekHandlerConstructor {
type SeekHandler (line 174) | interface SeekHandler {
type SeekConfig (line 179) | interface SeekConfig {
type BaseLoaderConstructor (line 184) | interface BaseLoaderConstructor {
type BaseLoader (line 188) | interface BaseLoader {
type CustomLoaderConstructor (line 206) | interface CustomLoaderConstructor {
type Range (line 210) | interface Range {
type LoaderStatus (line 215) | interface LoaderStatus {
type LoaderErrors (line 223) | interface LoaderErrors {
type LoaderErrorMessage (line 232) | interface LoaderErrorMessage {
type FeatureList (line 237) | interface FeatureList {
type PlayerConstructor (line 247) | interface PlayerConstructor {
type Player (line 251) | interface Player {
type NativePlayerStatisticsInfo (line 281) | interface NativePlayerStatisticsInfo {
type FlvPlayerReportStatisticsInfo (line 288) | interface FlvPlayerReportStatisticsInfo {
type FlvPlayerStatisticsInfo (line 298) | interface FlvPlayerStatisticsInfo extends Partial<FlvPlayerReportStatist...
type NativePlayerMediaInfo (line 304) | interface NativePlayerMediaInfo {
type FlvPlayerMediaInfo (line 311) | interface FlvPlayerMediaInfo extends NativePlayerMediaInfo {
type FlvPlayer (line 324) | interface FlvPlayer extends Player {
type NativePlayer (line 329) | interface NativePlayer extends Player {
type LoggingControlConfig (line 334) | interface LoggingControlConfig {
type LoggingControl (line 345) | interface LoggingControl extends LoggingControlConfig {
type Events (line 352) | interface Events {
type ErrorTypes (line 362) | interface ErrorTypes {
type ErrorDetails (line 368) | interface ErrorDetails {
FILE: src/config.js
function createDefaultConfig (line 52) | function createDefaultConfig() {
FILE: src/core/features.js
class Features (line 22) | class Features {
method supportMSEH264Playback (line 24) | static supportMSEH264Playback() {
method supportNetworkStreamIO (line 29) | static supportNetworkStreamIO() {
method getNetworkLoaderTypeName (line 36) | static getNetworkLoaderTypeName() {
method supportNativeMediaPlayback (line 43) | static supportNativeMediaPlayback(mimeType) {
method getFeatureList (line 51) | static getFeatureList() {
FILE: src/core/media-info.js
class MediaInfo (line 19) | class MediaInfo {
method constructor (line 21) | constructor() {
method isComplete (line 52) | isComplete() {
method isSeekable (line 81) | isSeekable() {
method getNearestKeyframe (line 85) | getNearestKeyframe(milliseconds) {
method _search (line 100) | _search(list, value) {
FILE: src/core/media-segment-info.js
class SampleInfo (line 20) | class SampleInfo {
method constructor (line 22) | constructor(dts, pts, duration, originalDts, isSync) {
class MediaSegmentInfo (line 35) | class MediaSegmentInfo {
method constructor (line 37) | constructor() {
method appendSyncPoint (line 49) | appendSyncPoint(sampleInfo) { // also called Random Access Point
class IDRSampleList (line 57) | class IDRSampleList {
method constructor (line 59) | constructor() {
method clear (line 63) | clear() {
method appendArray (line 67) | appendArray(syncPoints) {
method getLastSyncPointBeforeDts (line 81) | getLastSyncPointBeforeDts(dts) {
class MediaSegmentInfoList (line 115) | class MediaSegmentInfoList {
method constructor (line 117) | constructor(type) {
method type (line 123) | get type() {
method length (line 127) | get length() {
method isEmpty (line 131) | isEmpty() {
method clear (line 135) | clear() {
method _searchNearestSegmentBefore (line 140) | _searchNearestSegmentBefore(originalBeginDts) {
method _searchNearestSegmentAfter (line 172) | _searchNearestSegmentAfter(originalBeginDts) {
method append (line 176) | append(mediaSegmentInfo) {
method getLastSegmentBefore (line 198) | getLastSegmentBefore(originalBeginDts) {
method getLastSampleBefore (line 207) | getLastSampleBefore(originalBeginDts) {
method getLastSyncPointBefore (line 216) | getLastSyncPointBefore(originalBeginDts) {
FILE: src/core/mse-controller.js
class MSEController (line 27) | class MSEController {
method constructor (line 29) | constructor(config) {
method destroy (line 82) | destroy() {
method on (line 91) | on(event, listener) {
method off (line 95) | off(event, listener) {
method attachMediaElement (line 99) | attachMediaElement(mediaElement) {
method detachMediaElement (line 113) | detachMediaElement() {
method appendInitSegment (line 168) | appendInitSegment(initSegment, deferred) {
method appendMediaSegment (line 225) | appendMediaSegment(mediaSegment) {
method seek (line 239) | seek(seconds) {
method endOfStream (line 297) | endOfStream() {
method getNearestKeyframe (line 321) | getNearestKeyframe(dts) {
method _needCleanupSourceBuffer (line 325) | _needCleanupSourceBuffer() {
method _doCleanupSourceBuffer (line 347) | _doCleanupSourceBuffer() {
method _updateMediaSourceDuration (line 379) | _updateMediaSourceDuration() {
method _doRemoveRanges (line 400) | _doRemoveRanges() {
method _doAppendSegments (line 414) | _doAppendSegments() {
method _onSourceOpen (line 476) | _onSourceOpen() {
method _onSourceEnded (line 494) | _onSourceEnded() {
method _onSourceClose (line 499) | _onSourceClose() {
method _hasPendingSegments (line 509) | _hasPendingSegments() {
method _hasPendingRemoveRanges (line 514) | _hasPendingRemoveRanges() {
method _onSourceBufferUpdateEnd (line 519) | _onSourceBufferUpdateEnd() {
method _onSourceBufferError (line 532) | _onSourceBufferError(e) {
FILE: src/core/transmuxer.js
class Transmuxer (line 28) | class Transmuxer {
method constructor (line 30) | constructor(mediaDataSource, config) {
method destroy (line 70) | destroy() {
method on (line 86) | on(event, listener) {
method off (line 90) | off(event, listener) {
method hasWorker (line 94) | hasWorker() {
method open (line 98) | open() {
method close (line 106) | close() {
method seek (line 114) | seek(milliseconds) {
method pause (line 122) | pause() {
method resume (line 130) | resume() {
method _onInitSegment (line 138) | _onInitSegment(type, initSegment) {
method _onMediaSegment (line 145) | _onMediaSegment(type, mediaSegment) {
method _onLoadingComplete (line 151) | _onLoadingComplete() {
method _onRecoveredEarlyEof (line 157) | _onRecoveredEarlyEof() {
method _onMediaInfo (line 163) | _onMediaInfo(mediaInfo) {
method _onMetaDataArrived (line 169) | _onMetaDataArrived(metadata) {
method _onScriptDataArrived (line 175) | _onScriptDataArrived(data) {
method _onStatisticsInfo (line 181) | _onStatisticsInfo(statisticsInfo) {
method _onIOError (line 187) | _onIOError(type, info) {
method _onDemuxError (line 193) | _onDemuxError(type, info) {
method _onRecommendSeekpoint (line 199) | _onRecommendSeekpoint(milliseconds) {
method _onLoggingConfigChanged (line 205) | _onLoggingConfigChanged(config) {
method _onWorkerMessage (line 211) | _onWorkerMessage(e) {
FILE: src/core/transmuxing-controller.js
class TransmuxingController (line 31) | class TransmuxingController {
method constructor (line 33) | constructor(mediaDataSource, config) {
method destroy (line 88) | destroy() {
method on (line 112) | on(event, listener) {
method off (line 116) | off(event, listener) {
method start (line 120) | start() {
method _loadSegment (line 125) | _loadSegment(segmentIndex, optionalFrom) {
method stop (line 145) | stop() {
method _internalAbort (line 150) | _internalAbort() {
method pause (line 157) | pause() { // take a rest
method resume (line 164) | resume() {
method seek (line 171) | seek(milliseconds) {
method _searchSegmentIndexContains (line 222) | _searchSegmentIndexContains(milliseconds) {
method _onInitChunkArrival (line 235) | _onInitChunkArrival(data, byteStart) {
method _onMediaInfo (line 293) | _onMediaInfo(mediaInfo) {
method _onMetaDataArrived (line 319) | _onMetaDataArrived(metadata) {
method _onScriptDataArrived (line 323) | _onScriptDataArrived(data) {
method _onIOSeeked (line 327) | _onIOSeeked() {
method _onIOComplete (line 331) | _onIOComplete(extraData) {
method _onIORedirect (line 346) | _onIORedirect(redirectedURL) {
method _onIORecoveredEarlyEof (line 351) | _onIORecoveredEarlyEof() {
method _onIOException (line 355) | _onIOException(type, info) {
method _onDemuxException (line 361) | _onDemuxException(type, info) {
method _onRemuxerInitSegmentArrival (line 366) | _onRemuxerInitSegmentArrival(type, initSegment) {
method _onRemuxerMediaSegmentArrival (line 370) | _onRemuxerMediaSegmentArrival(type, mediaSegment) {
method _enableStatisticsReporter (line 393) | _enableStatisticsReporter() {
method _disableStatisticsReporter (line 401) | _disableStatisticsReporter() {
method _reportSegmentMediaInfo (line 408) | _reportSegmentMediaInfo(segmentIndex) {
method _reportStatisticsInfo (line 420) | _reportStatisticsInfo() {
FILE: src/core/transmuxing-worker.js
function onInitSegment (line 98) | function onInitSegment(type, initSegment) {
function onMediaSegment (line 109) | function onMediaSegment(type, mediaSegment) {
function onLoadingComplete (line 120) | function onLoadingComplete() {
function onRecoveredEarlyEof (line 127) | function onRecoveredEarlyEof() {
function onMediaInfo (line 134) | function onMediaInfo(mediaInfo) {
function onMetaDataArrived (line 142) | function onMetaDataArrived(metadata) {
function onScriptDataArrived (line 150) | function onScriptDataArrived(data) {
function onStatisticsInfo (line 158) | function onStatisticsInfo(statInfo) {
function onIOError (line 166) | function onIOError(type, info) {
function onDemuxError (line 176) | function onDemuxError(type, info) {
function onRecommendSeekpoint (line 186) | function onRecommendSeekpoint(milliseconds) {
function onLogcatCallback (line 193) | function onLogcatCallback(type, str) {
FILE: src/demux/amf-parser.js
class AMF (line 29) | class AMF {
method parseScriptData (line 31) | static parseScriptData(arrayBuffer, dataOffset, dataSize) {
method parseObject (line 46) | static parseObject(arrayBuffer, dataOffset, dataSize) {
method parseVariable (line 64) | static parseVariable(arrayBuffer, dataOffset, dataSize) {
method parseString (line 68) | static parseString(arrayBuffer, dataOffset, dataSize) {
method parseLongString (line 88) | static parseLongString(arrayBuffer, dataOffset, dataSize) {
method parseDate (line 108) | static parseDate(arrayBuffer, dataOffset, dataSize) {
method parseValue (line 123) | static parseValue(arrayBuffer, dataOffset, dataSize) {
FILE: src/demux/exp-golomb.js
class ExpGolomb (line 22) | class ExpGolomb {
method constructor (line 24) | constructor(uint8array) {
method destroy (line 35) | destroy() {
method _fillCurrentWord (line 39) | _fillCurrentWord() {
method readBits (line 53) | readBits(bits) {
method readBool (line 79) | readBool() {
method readByte (line 83) | readByte() {
method _skipLeadingZero (line 87) | _skipLeadingZero() {
method readUEG (line 100) | readUEG() { // unsigned exponential golomb
method readSEG (line 105) | readSEG() { // signed exponential golomb
FILE: src/demux/flv-demuxer.js
function Swap16 (line 26) | function Swap16(src) {
function Swap32 (line 31) | function Swap32(src) {
function ReadBig32 (line 38) | function ReadBig32(array, index) {
class FLVDemuxer (line 46) | class FLVDemuxer {
method constructor (line 48) | constructor(probeData, config) {
method destroy (line 117) | destroy() {
method probe (line 133) | static probe(buffer) {
method bindDataSource (line 159) | bindDataSource(loader) {
method onTrackMetadata (line 165) | get onTrackMetadata() {
method onTrackMetadata (line 169) | set onTrackMetadata(callback) {
method onMediaInfo (line 174) | get onMediaInfo() {
method onMediaInfo (line 178) | set onMediaInfo(callback) {
method onMetaDataArrived (line 182) | get onMetaDataArrived() {
method onMetaDataArrived (line 186) | set onMetaDataArrived(callback) {
method onScriptDataArrived (line 190) | get onScriptDataArrived() {
method onScriptDataArrived (line 194) | set onScriptDataArrived(callback) {
method onError (line 199) | get onError() {
method onError (line 203) | set onError(callback) {
method onDataAvailable (line 208) | get onDataAvailable() {
method onDataAvailable (line 212) | set onDataAvailable(callback) {
method timestampBase (line 217) | get timestampBase() {
method timestampBase (line 221) | set timestampBase(base) {
method overridedDuration (line 225) | get overridedDuration() {
method overridedDuration (line 230) | set overridedDuration(duration) {
method overridedHasAudio (line 237) | set overridedHasAudio(hasAudio) {
method overridedHasVideo (line 244) | set overridedHasVideo(hasVideo) {
method resetMediaInfo (line 250) | resetMediaInfo() {
method _isInitialMetadataDispatched (line 254) | _isInitialMetadataDispatched() {
method parseChunks (line 268) | parseChunks(chunk, byteStart) {
method _parseScriptData (line 368) | _parseScriptData(arrayBuffer, dataOffset, dataSize) {
method _parseKeyframesIndex (line 453) | _parseKeyframesIndex(keyframes) {
method _parseAudioData (line 470) | _parseAudioData(arrayBuffer, dataOffset, dataSize, tagTimestamp) {
method _parseAACAudioData (line 626) | _parseAACAudioData(arrayBuffer, dataOffset, dataSize) {
method _parseAACAudioSpecificConfig (line 646) | _parseAACAudioSpecificConfig(arrayBuffer, dataOffset, dataSize) {
method _parseMP3AudioData (line 747) | _parseMP3AudioData(arrayBuffer, dataOffset, dataSize, requestHeader) {
method _parseVideoData (line 823) | _parseVideoData(arrayBuffer, dataOffset, dataSize, tagTimestamp, tagPo...
method _parseAVCVideoPacket (line 848) | _parseAVCVideoPacket(arrayBuffer, dataOffset, dataSize, tagTimestamp, ...
method _parseAVCDecoderConfigurationRecord (line 873) | _parseAVCDecoderConfigurationRecord(arrayBuffer, dataOffset, dataSize) {
method _parseAVCVideoData (line 1041) | _parseAVCVideoData(arrayBuffer, dataOffset, dataSize, tagTimestamp, ta...
FILE: src/demux/sps-parser.js
class SPSParser (line 21) | class SPSParser {
method _ebsp2rbsp (line 23) | static _ebsp2rbsp(uint8array) {
method parseSPS (line 43) | static parseSPS(uint8array) {
method _skipScalingList (line 229) | static _skipScalingList(gb, count) {
method getProfileString (line 241) | static getProfileString(profile_idc) {
method getLevelString (line 262) | static getLevelString(level_idc) {
method getChromaFormatString (line 266) | static getChromaFormatString(chroma) {
FILE: src/flv.js
function createPlayer (line 36) | function createPlayer(mediaDataSource, optionalConfig) {
function isSupported (line 56) | function isSupported() {
function getFeatureList (line 60) | function getFeatureList() {
FILE: src/io/fetch-stream-loader.js
class FetchStreamLoader (line 30) | class FetchStreamLoader extends BaseLoader {
method isSupported (line 32) | static isSupported() {
method constructor (line 45) | constructor(seekHandler, config) {
method destroy (line 58) | destroy() {
method open (line 65) | open(dataSource, range) {
method abort (line 174) | abort() {
method _pump (line 187) | _pump(reader) { // ReadableStreamReader
FILE: src/io/io-controller.js
class IOController (line 42) | class IOController {
method constructor (line 44) | constructor(dataSource, config, extraData) {
method destroy (line 99) | destroy() {
method isWorking (line 124) | isWorking() {
method isPaused (line 128) | isPaused() {
method status (line 132) | get status() {
method extraData (line 136) | get extraData() {
method extraData (line 140) | set extraData(data) {
method onDataArrival (line 145) | get onDataArrival() {
method onDataArrival (line 149) | set onDataArrival(callback) {
method onSeeked (line 153) | get onSeeked() {
method onSeeked (line 157) | set onSeeked(callback) {
method onError (line 162) | get onError() {
method onError (line 166) | set onError(callback) {
method onComplete (line 170) | get onComplete() {
method onComplete (line 174) | set onComplete(callback) {
method onRedirect (line 178) | get onRedirect() {
method onRedirect (line 182) | set onRedirect(callback) {
method onRecoveredEarlyEof (line 186) | get onRecoveredEarlyEof() {
method onRecoveredEarlyEof (line 190) | set onRecoveredEarlyEof(callback) {
method currentURL (line 194) | get currentURL() {
method hasRedirect (line 198) | get hasRedirect() {
method currentRedirectedURL (line 202) | get currentRedirectedURL() {
method currentSpeed (line 207) | get currentSpeed() {
method loaderType (line 215) | get loaderType() {
method _selectSeekHandler (line 219) | _selectSeekHandler() {
method _selectLoader (line 239) | _selectLoader() {
method _createLoader (line 255) | _createLoader() {
method open (line 267) | open(optionalFrom) {
method abort (line 281) | abort() {
method pause (line 290) | pause() {
method resume (line 306) | resume() {
method seek (line 315) | seek(bytes) {
method _internalSeek (line 328) | _internalSeek(bytes, dropUnconsumed) {
method updateUrl (line 352) | updateUrl(url) {
method _expandBuffer (line 362) | _expandBuffer(expectedBytes) {
method _normalizeSpeed (line 385) | _normalizeSpeed(input) {
method _adjustStashSize (line 409) | _adjustStashSize(normalized) {
method _dispatchChunks (line 436) | _dispatchChunks(chunks, byteStart) {
method _onURLRedirect (line 441) | _onURLRedirect(redirectedURL) {
method _onContentLengthKnown (line 448) | _onContentLengthKnown(contentLength) {
method _onLoaderChunkArrival (line 455) | _onLoaderChunkArrival(chunk, byteStart, receivedLength) {
method _flushStashBuffer (line 565) | _flushStashBuffer(dropUnconsumed) {
method _onLoaderComplete (line 592) | _onLoaderComplete(from, to) {
method _onLoaderError (line 601) | _onLoaderError(type, data) {
FILE: src/io/loader.js
class BaseLoader (line 45) | class BaseLoader {
method constructor (line 47) | constructor(typeName) {
method destroy (line 59) | destroy() {
method isWorking (line 68) | isWorking() {
method type (line 72) | get type() {
method status (line 76) | get status() {
method needStashBuffer (line 80) | get needStashBuffer() {
method onContentLengthKnown (line 84) | get onContentLengthKnown() {
method onContentLengthKnown (line 88) | set onContentLengthKnown(callback) {
method onURLRedirect (line 92) | get onURLRedirect() {
method onURLRedirect (line 96) | set onURLRedirect(callback) {
method onDataArrival (line 100) | get onDataArrival() {
method onDataArrival (line 104) | set onDataArrival(callback) {
method onError (line 108) | get onError() {
method onError (line 112) | set onError(callback) {
method onComplete (line 116) | get onComplete() {
method onComplete (line 120) | set onComplete(callback) {
method open (line 125) | open(dataSource, range) {
method abort (line 129) | abort() {
FILE: src/io/param-seek-handler.js
class ParamSeekHandler (line 19) | class ParamSeekHandler {
method constructor (line 21) | constructor(paramStart, paramEnd) {
method getConfig (line 26) | getConfig(baseUrl, range) {
method removeURLParameters (line 53) | removeURLParameters(seekedURL) {
FILE: src/io/range-seek-handler.js
class RangeSeekHandler (line 19) | class RangeSeekHandler {
method constructor (line 21) | constructor(zeroStart) {
method getConfig (line 25) | getConfig(url, range) {
method removeURLParameters (line 46) | removeURLParameters(seekedURL) {
FILE: src/io/speed-sampler.js
class SpeedSampler (line 20) | class SpeedSampler {
method constructor (line 22) | constructor() {
method reset (line 38) | reset() {
method addBytes (line 44) | addBytes(bytes) {
method currentKBps (line 61) | get currentKBps() {
method lastSecondKBps (line 69) | get lastSecondKBps() {
method averageKBps (line 86) | get averageKBps() {
FILE: src/io/websocket-loader.js
class WebSocketLoader (line 24) | class WebSocketLoader extends BaseLoader {
method isSupported (line 26) | static isSupported() {
method constructor (line 34) | constructor() {
method destroy (line 45) | destroy() {
method open (line 52) | open(dataSource) {
method abort (line 75) | abort() {
method _onWebSocketOpen (line 86) | _onWebSocketOpen(e) {
method _onWebSocketClose (line 90) | _onWebSocketClose(e) {
method _onWebSocketMessage (line 103) | _onWebSocketMessage(e) {
method _dispatchArrayBuffer (line 124) | _dispatchArrayBuffer(arraybuffer) {
method _onWebSocketError (line 134) | _onWebSocketError(e) {
FILE: src/io/xhr-moz-chunked-loader.js
class MozChunkedLoader (line 24) | class MozChunkedLoader extends BaseLoader {
method isSupported (line 26) | static isSupported() {
method constructor (line 39) | constructor(seekHandler, config) {
method destroy (line 53) | destroy() {
method open (line 67) | open(dataSource, range) {
method abort (line 119) | abort() {
method _onReadyStateChange (line 127) | _onReadyStateChange(e) {
method _onProgress (line 151) | _onProgress(e) {
method _onLoadEnd (line 175) | _onLoadEnd(e) {
method _onXhrError (line 189) | _onXhrError(e) {
FILE: src/io/xhr-msstream-loader.js
class MSStreamLoader (line 34) | class MSStreamLoader extends BaseLoader {
method isSupported (line 36) | static isSupported() {
method constructor (line 52) | constructor(seekHandler, config) {
method destroy (line 77) | destroy() {
method open (line 94) | open(dataSource, range) {
method _internalOpen (line 98) | _internalOpen(dataSource, range, isSubrange) {
method abort (line 163) | abort() {
method _internalAbort (line 168) | _internalAbort() {
method _xhrOnReadyStateChange (line 185) | _xhrOnReadyStateChange(e) {
method _xhrOnError (line 230) | _xhrOnError(e) {
method _msrOnProgress (line 242) | _msrOnProgress(e) {
method _doReconnectIfNeeded (line 265) | _doReconnectIfNeeded() {
method _msrOnLoad (line 279) | _msrOnLoad(e) { // actually it is onComplete event
method _msrOnError (line 286) | _msrOnError(e) {
FILE: src/io/xhr-range-loader.js
class RangeLoader (line 25) | class RangeLoader extends BaseLoader {
method isSupported (line 27) | static isSupported() {
method constructor (line 39) | constructor(seekHandler, config) {
method destroy (line 70) | destroy() {
method currentSpeed (line 84) | get currentSpeed() {
method open (line 88) | open(dataSource, range) {
method _openSubRange (line 109) | _openSubRange() {
method _internalOpen (line 125) | _internalOpen(dataSource, range) {
method abort (line 176) | abort() {
method _internalAbort (line 182) | _internalAbort() {
method _onReadyStateChange (line 193) | _onReadyStateChange(e) {
method _onProgress (line 223) | _onProgress(e) {
method _normalizeSpeed (line 265) | _normalizeSpeed(input) {
method _onLoad (line 288) | _onLoad(e) {
method _onXhrError (line 343) | _onXhrError(e) {
FILE: src/player/flv-player.js
class FlvPlayer (line 31) | class FlvPlayer {
method constructor (line 33) | constructor(mediaDataSource, config) {
method destroy (line 92) | destroy() {
method on (line 110) | on(event, listener) {
method off (line 127) | off(event, listener) {
method attachMediaElement (line 131) | attachMediaElement(mediaElement) {
method detachMediaElement (line 171) | detachMediaElement() {
method load (line 187) | load() {
method unload (line 265) | unload() {
method play (line 279) | play() {
method pause (line 283) | pause() {
method type (line 287) | get type() {
method buffered (line 291) | get buffered() {
method duration (line 295) | get duration() {
method volume (line 299) | get volume() {
method volume (line 303) | set volume(value) {
method muted (line 307) | get muted() {
method muted (line 311) | set muted(muted) {
method currentTime (line 315) | get currentTime() {
method currentTime (line 322) | set currentTime(seconds) {
method mediaInfo (line 330) | get mediaInfo() {
method statisticsInfo (line 334) | get statisticsInfo() {
method _fillStatisticsInfo (line 342) | _fillStatisticsInfo(statInfo) {
method _onmseUpdateEnd (line 372) | _onmseUpdateEnd() {
method _onmseBufferFull (line 398) | _onmseBufferFull() {
method _suspendTransmuxer (line 405) | _suspendTransmuxer() {
method _checkProgressAndResume (line 415) | _checkProgressAndResume() {
method _isTimepointBuffered (line 442) | _isTimepointBuffered(seconds) {
method _internalSeek (line 455) | _internalSeek(seconds) {
method _checkAndApplyUnbufferedSeekpoint (line 505) | _checkAndApplyUnbufferedSeekpoint() {
method _checkAndResumeStuckPlayback (line 531) | _checkAndResumeStuckPlayback(stalled) {
method _onvLoadedMetadata (line 547) | _onvLoadedMetadata(e) {
method _onvSeeking (line 554) | _onvSeeking(e) { // handle seeking request from browser's progress bar
method _onvCanPlay (line 595) | _onvCanPlay(e) {
method _onvStalled (line 600) | _onvStalled(e) {
method _onvProgress (line 604) | _onvProgress(e) {
FILE: src/player/native-player.js
class NativePlayer (line 25) | class NativePlayer {
method constructor (line 27) | constructor(mediaDataSource, config) {
method destroy (line 55) | destroy() {
method on (line 66) | on(event, listener) {
method off (line 83) | off(event, listener) {
method attachMediaElement (line 87) | attachMediaElement(mediaElement) {
method detachMediaElement (line 102) | detachMediaElement() {
method load (line 115) | load() {
method unload (line 132) | unload() {
method play (line 143) | play() {
method pause (line 147) | pause() {
method type (line 151) | get type() {
method buffered (line 155) | get buffered() {
method duration (line 159) | get duration() {
method volume (line 163) | get volume() {
method volume (line 167) | set volume(value) {
method muted (line 171) | get muted() {
method muted (line 175) | set muted(muted) {
method currentTime (line 179) | get currentTime() {
method currentTime (line 186) | set currentTime(seconds) {
method mediaInfo (line 194) | get mediaInfo() {
method statisticsInfo (line 209) | get statisticsInfo() {
method _onvLoadedMetadata (line 242) | _onvLoadedMetadata(e) {
method _reportStatisticsInfo (line 250) | _reportStatisticsInfo() {
FILE: src/remux/aac-silent.js
class AAC (line 20) | class AAC {
method getSilentFrame (line 22) | static getSilentFrame(codec, channelCount) {
FILE: src/remux/mp4-generator.js
class MP4 (line 21) | class MP4 {
method init (line 23) | static init() {
method box (line 122) | static box(type) {
method generateInitSegment (line 150) | static generateInitSegment(meta) {
method moov (line 161) | static moov(meta) {
method mvhd (line 169) | static mvhd(timescale, duration) {
method trak (line 206) | static trak(meta) {
method tkhd (line 211) | static tkhd(meta) {
method mdia (line 251) | static mdia(meta) {
method mdhd (line 256) | static mdhd(meta) {
method hdlr (line 277) | static hdlr(meta) {
method minf (line 288) | static minf(meta) {
method dinf (line 299) | static dinf() {
method stbl (line 307) | static stbl(meta) {
method stsd (line 319) | static stsd(meta) {
method mp3 (line 331) | static mp3(meta) {
method mp4a (line 351) | static mp4a(meta) {
method esds (line 371) | static esds(meta) {
method avc1 (line 401) | static avc1(meta) {
method mvex (line 436) | static mvex(meta) {
method trex (line 441) | static trex(meta) {
method moof (line 458) | static moof(track, baseMediaDecodeTime) {
method mfhd (line 462) | static mfhd(sequenceNumber) {
method traf (line 474) | static traf(track, baseMediaDecodeTime) {
method sdtp (line 500) | static sdtp(track) {
method trun (line 516) | static trun(track, offset) {
method mdat (line 561) | static mdat(data) {
FILE: src/remux/mp4-remuxer.js
class MP4Remuxer (line 28) | class MP4Remuxer {
method constructor (line 30) | constructor(config) {
method destroy (line 70) | destroy() {
method bindDataSource (line 83) | bindDataSource(producer) {
method onInitSegment (line 97) | get onInitSegment() {
method onInitSegment (line 101) | set onInitSegment(callback) {
method onMediaSegment (line 113) | get onMediaSegment() {
method onMediaSegment (line 117) | set onMediaSegment(callback) {
method insertDiscontinuity (line 121) | insertDiscontinuity() {
method seek (line 125) | seek(originalDts) {
method remux (line 132) | remux(audioTrack, videoTrack) {
method _onTrackMetadataReceived (line 143) | _onTrackMetadataReceived(type, metadata) {
method _calculateDtsBase (line 180) | _calculateDtsBase(audioTrack, videoTrack) {
method flushStashedSamples (line 196) | flushStashedSamples() {
method _remuxAudio (line 233) | _remuxAudio(audioTrack, force) {
method _remuxVideo (line 562) | _remuxVideo(videoTrack, force) {
method _mergeBoxes (line 758) | _mergeBoxes(moof, mdat) {
FILE: src/utils/browser.js
function detect (line 21) | function detect() {
FILE: src/utils/exception.js
class RuntimeException (line 19) | class RuntimeException {
method constructor (line 21) | constructor(message) {
method name (line 25) | get name() {
method message (line 29) | get message() {
method toString (line 33) | toString() {
class IllegalStateException (line 39) | class IllegalStateException extends RuntimeException {
method constructor (line 41) | constructor(message) {
method name (line 45) | get name() {
class InvalidArgumentException (line 51) | class InvalidArgumentException extends RuntimeException {
method constructor (line 53) | constructor(message) {
method name (line 57) | get name() {
class NotImplementedException (line 63) | class NotImplementedException extends RuntimeException {
method constructor (line 65) | constructor(message) {
method name (line 69) | get name() {
FILE: src/utils/logger.js
class Log (line 21) | class Log {
method e (line 23) | static e(tag, msg) {
method i (line 46) | static i(tag, msg) {
method w (line 67) | static w(tag, msg) {
method d (line 88) | static d(tag, msg) {
method v (line 109) | static v(tag, msg) {
FILE: src/utils/logging-control.js
class LoggingControl (line 22) | class LoggingControl {
method forceGlobalTag (line 24) | static get forceGlobalTag() {
method forceGlobalTag (line 28) | static set forceGlobalTag(enable) {
method globalTag (line 33) | static get globalTag() {
method globalTag (line 37) | static set globalTag(tag) {
method enableAll (line 42) | static get enableAll() {
method enableAll (line 50) | static set enableAll(enable) {
method enableDebug (line 59) | static get enableDebug() {
method enableDebug (line 63) | static set enableDebug(enable) {
method enableVerbose (line 68) | static get enableVerbose() {
method enableVerbose (line 72) | static set enableVerbose(enable) {
method enableInfo (line 77) | static get enableInfo() {
method enableInfo (line 81) | static set enableInfo(enable) {
method enableWarn (line 86) | static get enableWarn() {
method enableWarn (line 90) | static set enableWarn(enable) {
method enableError (line 95) | static get enableError() {
method enableError (line 99) | static set enableError(enable) {
method getConfig (line 104) | static getConfig() {
method applyConfig (line 117) | static applyConfig(config) {
method _notifyChange (line 128) | static _notifyChange() {
method registerListener (line 137) | static registerListener(listener) {
method removeListener (line 141) | static removeListener(listener) {
method addLogListener (line 145) | static addLogListener(listener) {
method removeLogListener (line 153) | static removeLogListener(listener) {
FILE: src/utils/polyfill.js
class Polyfill (line 19) | class Polyfill {
method install (line 21) | static install() {
FILE: src/utils/utf8-conv.js
function checkContinuation (line 20) | function checkContinuation(uint8array, start, checkLength) {
function decodeUTF8 (line 33) | function decodeUTF8(uint8array) {
FILE: types/test-flv.ts
type LoaderStatusAlias (line 3) | type LoaderStatusAlias = flvjs.LoaderStatus;
type LoaderErrorsAlias (line 4) | type LoaderErrorsAlias = flvjs.LoaderErrors;
type MediaDataSourceExt (line 6) | interface MediaDataSourceExt extends flvjs.MediaDataSource {
Condensed preview — 61 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (392K chars).
[
{
"path": ".eslintignore",
"chars": 19,
"preview": "dist/\nnode_modules/"
},
{
"path": ".eslintrc.json",
"chars": 1313,
"preview": "{\n \"extends\": \"eslint:recommended\",\n \"parserOptions\": {\n \"ecmaVersion\": 6,\n \"sourceType\": \"module\",\n"
},
{
"path": ".gitignore",
"chars": 2197,
"preview": "#################\n## Node.js\n#################\n\nnode_modules\nnpm-debug.log\n\n#################\n## Grunt\n#################"
},
{
"path": ".npmignore",
"chars": 2322,
"preview": "## This file is modified from .gitignore from same folder\n## Which allows dist folder and ignores unnecessary folders\n\nd"
},
{
"path": "LICENSE",
"chars": 11357,
"preview": "\n Apache License\n Version 2.0, January 2004\n "
},
{
"path": "README.md",
"chars": 3418,
"preview": "\nflv.js [](https://www.npmjs.com/package/flv.js)\n======\nAn HT"
},
{
"path": "d.ts/flv.d.ts",
"chars": 12852,
"preview": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the A"
},
{
"path": "demo/demo.css",
"chars": 1609,
"preview": ".mainContainer {\n display: block;\n width: 100%;\n margin-left: auto;\n margin-right: auto;\n}\n@media screen and"
},
{
"path": "demo/index.html",
"chars": 8119,
"preview": "<!DOCTYPE html>\n<html>\n\n<head>\n <meta content=\"text/html; charset=utf-8\" http-equiv=\"Content-Type\">\n <title>flv.js"
},
{
"path": "docs/api.md",
"chars": 11822,
"preview": "\nflv.js API\n==========\nThis document use TypeScript-like definitions to describe interfaces.\n\n## Interfaces\n\nflv.js expo"
},
{
"path": "docs/cors.md",
"chars": 2302,
"preview": "\nCORS Configuration\n==================\nAnytime you want to play an FLV stream from another `Origin`, the server must res"
},
{
"path": "docs/design.md",
"chars": 73,
"preview": "\nflv.js design\n======\n\nArchitecture overview:\n\n\n"
},
{
"path": "docs/livestream.md",
"chars": 1652,
"preview": "\nLivestream playback\n===================\nYou need to provide a livestream URL in `MediaDataSource` and indicates `isLive"
},
{
"path": "docs/multipart.md",
"chars": 1840,
"preview": "\nMultipart playback\n==================\nWhen you create FlvPlayer instance, the `MediaDataSource` structure is passing th"
},
{
"path": "package.json",
"chars": 1105,
"preview": "{\n \"name\": \"flv.js\",\n \"version\": \"1.6.2\",\n \"description\": \"HTML5 FLV Player\",\n \"main\": \"./dist/flv.js\",\n \"types\": \""
},
{
"path": "src/config.js",
"chars": 1563,
"preview": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the A"
},
{
"path": "src/core/features.js",
"chars": 2802,
"preview": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the A"
},
{
"path": "src/core/media-info.js",
"chars": 3980,
"preview": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the A"
},
{
"path": "src/core/media-segment-info.js",
"chars": 6552,
"preview": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the A"
},
{
"path": "src/core/mse-controller.js",
"chars": 20565,
"preview": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the A"
},
{
"path": "src/core/mse-events.js",
"chars": 819,
"preview": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the A"
},
{
"path": "src/core/transmuxer.js",
"chars": 8697,
"preview": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the A"
},
{
"path": "src/core/transmuxing-controller.js",
"chars": 15691,
"preview": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the A"
},
{
"path": "src/core/transmuxing-events.js",
"chars": 1145,
"preview": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the A"
},
{
"path": "src/core/transmuxing-worker.js",
"chars": 6178,
"preview": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the A"
},
{
"path": "src/demux/amf-parser.js",
"chars": 8830,
"preview": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the A"
},
{
"path": "src/demux/demux-errors.js",
"chars": 844,
"preview": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the A"
},
{
"path": "src/demux/exp-golomb.js",
"chars": 3750,
"preview": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the A"
},
{
"path": "src/demux/flv-demuxer.js",
"chars": 40041,
"preview": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the A"
},
{
"path": "src/demux/sps-parser.js",
"chars": 10234,
"preview": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the A"
},
{
"path": "src/flv.js",
"chars": 2629,
"preview": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the A"
},
{
"path": "src/index.js",
"chars": 119,
"preview": "// entry/index file\n\n// make it compatible with browserify's umd wrapper\nmodule.exports = require('./flv.js').default;\n"
},
{
"path": "src/io/fetch-stream-loader.js",
"chars": 9825,
"preview": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the A"
},
{
"path": "src/io/io-controller.js",
"chars": 22510,
"preview": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the A"
},
{
"path": "src/io/loader.js",
"chars": 3436,
"preview": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the A"
},
{
"path": "src/io/param-seek-handler.js",
"chars": 2354,
"preview": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the A"
},
{
"path": "src/io/range-seek-handler.js",
"chars": 1425,
"preview": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the A"
},
{
"path": "src/io/speed-sampler.js",
"chars": 2956,
"preview": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the A"
},
{
"path": "src/io/websocket-loader.js",
"chars": 4230,
"preview": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the A"
},
{
"path": "src/io/xhr-moz-chunked-loader.js",
"chars": 6732,
"preview": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the A"
},
{
"path": "src/io/xhr-msstream-loader.js",
"chars": 10259,
"preview": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the A"
},
{
"path": "src/io/xhr-range-loader.js",
"chars": 11739,
"preview": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the A"
},
{
"path": "src/player/flv-player.js",
"chars": 21639,
"preview": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the A"
},
{
"path": "src/player/native-player.js",
"chars": 7834,
"preview": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the A"
},
{
"path": "src/player/player-errors.js",
"chars": 1375,
"preview": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the A"
},
{
"path": "src/player/player-events.js",
"chars": 979,
"preview": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the A"
},
{
"path": "src/remux/aac-silent.js",
"chars": 3892,
"preview": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * This file is modified from dailymotion's hls.js library (h"
},
{
"path": "src/remux/mp4-generator.js",
"chars": 20909,
"preview": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * This file is derived from dailymotion's hls.js library (hl"
},
{
"path": "src/remux/mp4-remuxer.js",
"chars": 28095,
"preview": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the A"
},
{
"path": "src/utils/browser.js",
"chars": 3740,
"preview": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the A"
},
{
"path": "src/utils/exception.js",
"chars": 1521,
"preview": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the A"
},
{
"path": "src/utils/logger.js",
"chars": 3086,
"preview": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the A"
},
{
"path": "src/utils/logging-control.js",
"chars": 4403,
"preview": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the A"
},
{
"path": "src/utils/polyfill.js",
"chars": 1827,
"preview": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the A"
},
{
"path": "src/utils/utf8-conv.js",
"chars": 2894,
"preview": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * This file is derived from C++ project libWinTF8 (https://g"
},
{
"path": "tsconfig.json",
"chars": 386,
"preview": "{\n \"compilerOptions\": {\n \"outDir\": \"./dist/\",\n \"allowJs\": true,\n \"sourceMap\": true,\n \"mod"
},
{
"path": "tslint.json",
"chars": 33,
"preview": "{ \"extends\": \"dtslint/dt.json\" }\n"
},
{
"path": "types/index.d.ts",
"chars": 55,
"preview": "// TypeScript Version: 2.3\n\nimport '../d.ts/flv.d.ts';\n"
},
{
"path": "types/test-flv.ts",
"chars": 201,
"preview": "import flvjs from '../';\n\ntype LoaderStatusAlias = flvjs.LoaderStatus;\ntype LoaderErrorsAlias = flvjs.LoaderErrors;\n\nint"
},
{
"path": "types/tsconfig.json",
"chars": 505,
"preview": "{\n \"compilerOptions\": {\n \"module\": \"commonjs\",\n \"lib\": [\n \"es6\",\n \"dom\"\n ]"
},
{
"path": "webpack.config.js",
"chars": 1523,
"preview": "const webpack = require('webpack');\nconst pkg = require('./package.json');\nconst path = require('path');\nconst TerserPlu"
}
]
About this extraction
This page contains the full source code of the bilibili/flv.js GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 61 files (368.0 KB), approximately 91.8k tokens, and a symbol index with 513 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.