init first version

This commit is contained in:
yinguohui 2018-06-24 11:02:55 +08:00
parent 1a13d8abc4
commit e77ee95e93
375 changed files with 78867 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

9
.babelrc Normal file
View File

@ -0,0 +1,9 @@
{
"presets": [
["env", {
"targets": {
"node": true
}
}]
]
}

1
.czrc Normal file
View File

@ -0,0 +1 @@
{ "path": "cz-conventional-changelog" }

3
.eslintignore Normal file
View File

@ -0,0 +1,3 @@
node_modules/*
browser/*
dist/*

3
.eslintrc.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = {
"extends": "standard"
};

7
.gitignore vendored
View File

@ -33,7 +33,11 @@ bower_components
build/Release
# Dependency directories
<<<<<<< HEAD
node_modules/
=======
node_modules
>>>>>>> d8ce877... init first version
jspm_packages/
# TypeScript v1 declaration files
@ -59,3 +63,6 @@ typings/
# next.js build output
.next
#lerna-changelog
.changelog

3
CHANGELOG.md Normal file
View File

@ -0,0 +1,3 @@

96
README.md Normal file
View File

@ -0,0 +1,96 @@
<div align="center">
<img src="./xgplayer.png" width="384" height="96">
</div>
<div align="center">
<a href="https://www.npmjs.com/package/xgplayer" target="_blank">
<img src="https://img.shields.io/npm/v/xgplayer.svg" alt="npm">
</a>
<a href="https://www.npmjs.com/package/xgplayer">
<img src="https://img.shields.io/npm/dm/xgplaer.svg" alg="download">
</a>
<a href="https://www.npmjs.com/package/xgplayer" target="_blank">
<img src="https://img.shields.io/npm/l/xgplayer.svg" alt="license">
</a>
<a href="http://commitizen.github.io/cz-cli/">
<img src="https://img.shields.io/badge/commitizen-friendly-brightgreen.svg" alt="commitizen">
</a>
</div>
### Introduction
xgplayer is a web video player library. it has designed a separate, detachable UI component based on the principle that everything is componentized. More importantly, it is not only flexible in the UI layer, but also bold in its functionality: it gets rid of video loading, buffering, and format support for video dependence. Especially on mp4
it can be staged loading for that does not support streaming mp4. This means seamless switching with clarity, load control, and video savings. It also integrates on-demand and live support for FLV, HLS, and dash. [Document](http://h5player.bytedance.com/)
### Start
1. Install
```
$ npm install xgplayer
```
2. Usage
Step 1:
```html
<div id="vs"></div>
```
Step 2:
```js
import Player from 'xgplayer'
let player=new Player({
id:'vs',
url:'http://s2.pstatp.com/cdn/expire-1-M/byted-player-videos/1.0.0/xgplayer-demo.mp4'
})
```
This is the easiest way to configure the player,then it runs with video. For more advanced content, see the plug-in section or documentation. [more config](http://h5player.bytedance.com/config)
### Plugins
xgplayer provides more plugins, plugins are divided into two categories: one is self-starting, and another inherits the player's core class named xgplayer. In principle, the officially provided plug-ins are self-starting and the packaged third-party libraries are inherited. Some feature plug-ins themselves can provide a downgrade scenario that suggests a self-start approach, or an inheritance approach if not. The player supports custom plugins for more content viewing [plugins](http://h5player.bytedance.com/plugins/)
The following is how to use a self-starting plug-in
```js
import Player from 'xgplayer'
import 'xgplyaer-mp4'
let player=new Player({
id:'video',
url:'//abc.com/test.mp4'
})
```
<code>xgplayer-mp4</code>plugin is self-starting, It loads mp4 video itself, parses mp4 format, implements custom loading, buffering, seamless switching, and so on. it will automatically downgrade devices that do not support [MSE](https://www.w3.org/TR/media-source/). [details](http://h5player.bytedance.com/plugins/#xgplayer-mp4)
### Mobile Support
xgplayer supports mobile terminal, but android device brand and system are numerous, there are much compatibility problems, the player provides whitelist mechanism to ensure the perfect operation in mobile terminal. [whitelist](http://h5player.bytedance.com/config/#%E7%99%BD%E5%90%8D%E5%8D%95)
### Dev
```
$ git clone git@github.com:bytedance/xgplayer.git
$ cd xgplayer
$ npm install
$ npm run dev
```
please visit [http://localhost:9090/examples/index.html](http://localhost:9090/examples/index.html)
### License
[MIT](http://opensource.org/licenses/MIT)

95
README.zh-CN.md Normal file
View File

@ -0,0 +1,95 @@
<div align="center">
<img src="./xgplayer.png" width="384" height="96">
</div>
<div align="center">
<a href="https://www.npmjs.com/package/xgplayer" target="_blank">
<img src="https://img.shields.io/npm/v/xgplayer.svg" alt="npm">
</a>
<a href="https://www.npmjs.com/package/xgplayer">
<img src="https://img.shields.io/npm/dm/xgplaer.svg" alg="download">
</a>
<a href="https://www.npmjs.com/package/xgplayer" target="_blank">
<img src="https://img.shields.io/npm/l/xgplayer.svg" alt="license">
</a>
<a href="http://commitizen.github.io/cz-cli/">
<img src="https://img.shields.io/badge/commitizen-friendly-brightgreen.svg" alt="commitizen">
</a>
</div>
### 概述
西瓜播放器是一个Web视频播放器类库它本着一切都是组件化的原则设计了独立可拆卸的 UI 组件。更重要的是它不只是在 UI 层有灵活的表现,在功能上也做了大胆的尝试:摆脱视频加载、缓冲、格式支持对 video 的依赖。尤其是在 mp4 点播上做了较大的努力,让本不支持流式播放的 mp4 能做到分段加载,这就意味着可以做到清晰度无缝切换、加载控制、节省视频流量。同时,它也集成了对 flv、hls、dash 的点播和直播支持。[文档](http://h5player.bytedance.com/)
### 起步
1. 安装
```
$ npm install xgplayer
```
2. 使用
- Step 1:
```html
<div id="vs"></div>
```
- Step 2:
```js
import Player from 'xgplayer'
let player=new Player({
id:'vs',
url:'http://s2.pstatp.com/cdn/expire-1-M/byted-player-videos/1.0.0/xgplayer-demo.mp4'
})
```
这是最简单的播放器配置方法,基本功能可以跑起来,如果想使用高级功能参考插件一节或者文档。[更多配置](http://h5player.bytedance.com/config/)
### 插件
西瓜播放器提供了较多的插件,插件分两类:一部分是自启动的,一部分是继承播放器核心类 xgplayer 的。原则上官方提供插件都是自启动的,封装的第三方类库都是继承方式。有些功能插件本身能提供降级方案建议使用自启动方式,否则建议使用继承方式。播放器支持自定义插件,更多内容查看 [插件](http://h5player.bytedance.com/plugins/)
对于自启动的插件使用方法如下:
```js
import Player from 'xgplayer'
import 'xgplyaer-mp4'
let player=new Player({
id:'video',
url:'//abc.com/test.mp4'
})
```
<code>xgplayer-mp4</code>插件就是自启动的,它会自己加载 mp4 视频、解析 mp4 格式,实现自定义加载、缓冲、无缝切换等[详情]((http://h5player.bytedance.com/plugins/#xgplayer-mp4))。对于不支持 [MSE](https://www.w3.org/TR/media-source/) 的设备自动降级。
### Mobile Support
西瓜播放器支持移动端,不过安卓设备品牌和系统众多,兼容性问题很多,播放器提供白名单机制保证在移动端完美的运行。[白名单机制](http://h5player/bytedance.com/config/#白名单)
### Dev
```
$ git clone git@github.com:bytedance/xgplayer.git
$ cd xgplayer
$ npm install
$ npm run dev
```
访问 [http://localhost:9090/examples/index.html](http://localhost:9090/examples/index.html)
### License
[MIT](http://opensource.org/licenses/MIT)

13
app.js Normal file
View File

@ -0,0 +1,13 @@
import Koa from 'koa'
import Serve from 'koa-static'
import Cors from '@koa/cors'
import Range from 'koa-range'
const app = new Koa()
app.use(Range)
app.use(Cors())
app.use(Serve('.'))
app.listen(9090)
console.log(`server is ready,please visit http://localhost:9090/examples/index.html`)

14
lerna.json Normal file
View File

@ -0,0 +1,14 @@
{
"packages": [
"packages/*"
],
"changelog": {
"repo": "cucygh/xgplayer",
"labels": {
"mp4": "xgplayer-mp4",
"hls": "xgplayer-hls"
},
"cacheDir": ".changelog"
},
"version": "1.0.2"
}

10660
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

65
package.json Normal file
View File

@ -0,0 +1,65 @@
{
"devDependencies": {
"@koa/cors": "^2.2.1",
"babel-core": "^6.26.3",
"babel-loader": "^7.1.4",
"babel-plugin-add-module-exports": "^0.2.1",
"babel-preset-env": "^1.7.0",
"babel-preset-es2015": "^6.24.1",
"css-loader": "^0.28.11",
"cz-conventional-changelog": "^2.1.0",
"eslint": "^4.19.1",
"eslint-config-standard": "^11.0.0",
"eslint-plugin-import": "^2.12.0",
"eslint-plugin-node": "^6.0.1",
"eslint-plugin-promise": "^3.8.0",
"eslint-plugin-standard": "^3.1.0",
"koa": "^2.5.1",
"koa-range": "^0.3.0",
"koa-static": "^4.0.3",
"lerna": "^3.0.0-alpha.1",
"lerna-changelog": "^0.8.0",
"lint-staged": "^7.2.0",
"style-loader": "^0.21.0",
"webpack": "^4.12.0",
"webpack-cli": "^3.0.8",
"webworkify-webpack-dropin": "^1.1.9"
},
"name": "xgplayer",
"version": "0.0.1",
"description": "xgplayer for video player",
"main": "index.js",
"scripts": {
"test": "node_modules/.bin/lerna-changelog",
"dev": "babel-node app.js",
"publish": "lerna publish",
"cz": "git cz",
"precz": "lint-staged",
"remove": "lerna run clean",
"clean": "lerna clean"
},
"repository": {
"type": "git",
"url": "git+https://github.com/bytedance/xgplayer.git"
},
"keywords": [
"xgplayer",
"mp4",
"hls",
"flv",
"dash"
],
"lint-staged": {
"src/**/*.js": [
"eslint -c .eslintrc --fix",
"git add"
]
},
"author": "yinguohui@bytedance.com",
"license": "MIT",
"bugs": {
"url": "https://github.com/bytedance/xgplayer/issues"
},
"homepage": "https://github.com/bytedance/xgplayer#readme",
"dependencies": {}
}

BIN
packages/xgplayer-flv.js/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,9 @@
{
"presets": [
"es2015"
],
"plugins": [
"add-module-exports",
"babel-plugin-bulk-import"
]
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4332
packages/xgplayer-flv.js/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,44 @@
{
"name": "xgplayer-flv.js",
"version": "1.0.1",
"description": "web video player",
"main": "./dist/index.js",
"scripts": {
"init": "npm install",
"clean": "rm -rf dist browser",
"prepare": "npm run build",
"build": "webpack --progress --display-chunks -p",
"watch": "webpack --progress --display-chunks -p --watch"
},
"babel": {
"presets": [
"es2015"
],
"plugins": [
"add-module-exports",
"babel-plugin-bulk-import"
]
},
"repository": {
"type": "git",
"url": "git+https://github.com/bytedance/xgplayer.git"
},
"author": "cuc_ygh@163.com",
"license": "ISC",
"devDependencies": {
"babel-core": "^6.26.3",
"babel-loader": "^7.1.4",
"babel-plugin-add-module-exports": "^0.2.1",
"babel-plugin-bulk-import": "^1.0.2",
"babel-preset-es2015": "^6.24.1",
"webpack": "^4.8.3"
},
"peerDependency": {
"xgplayer": "^0.1.0"
},
"dependencies": {
"es6-promise": "^4.2.4",
"glob": "^7.1.2",
"webworkify": "^1.5.0"
}
}

View File

@ -0,0 +1,7 @@
module.exports = {
plugins: {
'postcss-cssnext': {
browserslist: ['cover 99.5%'],
},
},
};

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,51 @@
/*
* 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
};
export function createDefaultConfig() {
return Object.assign({}, defaultConfig);
}

View File

@ -0,0 +1,75 @@
/*
* 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;

View File

@ -0,0 +1,130 @@
/*
* 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;

View File

@ -0,0 +1,230 @@
/*
* 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;
}
}
}

View File

@ -0,0 +1,556 @@
/*
* 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.definitionChange = false;
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.removeSourceBuffer(sb);
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;
}
}
newSourceInitSegment (initSegment) {
let is = initSegment;
let mimeType = `${is.container}`;
if (is.codec && is.codec.length > 0) {
mimeType += `;codecs=${is.codec}`;
}
let ps = this._pendingSegments[is.type];
ps.splice(0, ps.length);
this._pendingSegments[is.type] = [];
this._pendingRemoveRanges[is.type] = [];
this._lastInitSegments[is.type] = [];
let ms = this._mediaSource;
this.definitionChange = true;
if (this._sourceBuffers[is.type]) {
ms.removeSourceBuffer(this._sourceBuffers[is.type]);
let sb = this._sourceBuffers[is.type] = this._mediaSource.addSourceBuffer(mimeType);
sb.addEventListener('error', this.e.onSourceBufferError);
sb.addEventListener('updateend', this.e.onSourceBufferUpdateEnd);
}
// this.definitionChange = false;
}
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.definitionChange === false) {
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;

View File

@ -0,0 +1,26 @@
/*
* 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;

View File

@ -0,0 +1,241 @@
/*
* 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 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();
this.isDefinitionChanging = false;
if (config.enableWorker && typeof (Worker) !== 'undefined') {
try {
let work = require('webworkify');
this._worker = work(TransmuxingWorker);
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.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 && this._emitter.addListener(event, listener);
}
off(event, listener) {
this._emitter && 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 && this._emitter.emit(TransmuxingEvents.INIT_SEGMENT, type, initSegment);
});
}
_onMediaSegment(type, mediaSegment) {
Promise.resolve().then(() => {
this._emitter && this._emitter.emit(TransmuxingEvents.MEDIA_SEGMENT, type, mediaSegment);
});
}
_onLoadingComplete() {
Promise.resolve().then(() => {
this._emitter && this._emitter.emit(TransmuxingEvents.LOADING_COMPLETE);
});
}
_onRecoveredEarlyEof() {
Promise.resolve().then(() => {
this._emitter && this._emitter.emit(TransmuxingEvents.RECOVERED_EARLY_EOF);
});
}
_onMediaInfo(mediaInfo) {
Promise.resolve().then(() => {
this._emitter && this._emitter.emit(TransmuxingEvents.MEDIA_INFO, mediaInfo);
});
}
_onStatisticsInfo(statisticsInfo) {
Promise.resolve().then(() => {
this._emitter && this._emitter.emit(TransmuxingEvents.STATISTICS_INFO, statisticsInfo);
});
}
_onIOError(type, info) {
Promise.resolve().then(() => {
this._emitter && this._emitter.emit(TransmuxingEvents.IO_ERROR, type, info);
});
}
_onDemuxError(type, info) {
Promise.resolve().then(() => {
this._emitter && this._emitter.emit(TransmuxingEvents.DEMUX_ERROR, type, info);
});
}
_onRecommendSeekpoint(milliseconds) {
Promise.resolve().then(() => {
this._emitter && 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.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;

View File

@ -0,0 +1,428 @@
/*
* 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._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);
});
}
}
_onIOSeeked() {
this._remuxer.insertDiscontinuity();
}
_onIOComplete(extraData) {
let segmentIndex = extraData;
let nextSegmentIndex = segmentIndex + 1;
if (nextSegmentIndex < this._mediaDataSource.segments.length) {
this._internalAbort();
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;

View File

@ -0,0 +1,31 @@
/*
* 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',
STATISTICS_INFO: 'statistics_info',
RECOMMEND_SEEKPOINT: 'recommend_seekpoint'
};
export default TransmuxingEvents;

View File

@ -0,0 +1,187 @@
/*
* 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.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 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;

View File

@ -0,0 +1,243 @@
/*
* 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;

View File

@ -0,0 +1,26 @@
/*
* 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;

View File

@ -0,0 +1,116 @@
/*
* 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 diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,281 @@
/*
* 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;

View File

@ -0,0 +1,87 @@
/*
* 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 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.Events = PlayerEvents;
flvjs.ErrorTypes = ErrorTypes;
flvjs.ErrorDetails = ErrorDetails;
flvjs.FlvPlayer = FlvPlayer;
flvjs.NativePlayer = NativePlayer;
flvjs.LoggingControl = LoggingControl;
Object.defineProperty(flvjs, 'version', {
enumerable: true,
get: function () {
// replaced by browserify-versionify transform
return '__VERSION__';
}
});
export default flvjs;

View File

@ -0,0 +1,4 @@
// entry/index file
// make it compatible with browserify's umd wrapper
module.exports = require('./flv.js').default;

View File

@ -0,0 +1,219 @@
/*
* 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 Browser from '../utils/browser.js';
import {BaseLoader, LoaderStatus, LoaderErrors} from './loader.js';
import {RuntimeException} from '../utils/exception.js';
/* fetch + stream IO loader. Currently working on chrome 43+.
* fetch provides a better alternative http API to XMLHttpRequest
*
* fetch spec https://fetch.spec.whatwg.org/
* stream spec https://streams.spec.whatwg.org/
*/
class FetchStreamLoader extends BaseLoader {
static isSupported() {
try {
// fetch + stream is broken on Microsoft Edge. Disable before build 15048.
// see https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/8196907/
// Fixed in Jan 10, 2017. Build 15048+ removed from blacklist.
let isWorkWellEdge = Browser.msedge && Browser.version.minor >= 15048;
let browserNotBlacklisted = Browser.msedge ? isWorkWellEdge : true;
return (self.fetch && self.ReadableStream && browserNotBlacklisted);
} catch (e) {
return false;
}
}
constructor(seekHandler, config) {
super('fetch-stream-loader');
this.TAG = 'FetchStreamLoader';
this._seekHandler = seekHandler;
this._config = config;
this._needStash = true;
this._requestAbort = false;
this._contentLength = null;
this._receivedLength = 0;
}
destroy() {
if (this.isWorking()) {
this.abort();
}
super.destroy();
}
open(dataSource, range) {
this._dataSource = dataSource;
this._range = range;
let sourceURL = dataSource.url;
if (this._config.reuseRedirectedURL && dataSource.redirectedURL != undefined) {
sourceURL = dataSource.redirectedURL;
}
let seekConfig = this._seekHandler.getConfig(sourceURL, range);
let headers = new self.Headers();
if (typeof seekConfig.headers === 'object') {
let configHeaders = seekConfig.headers;
for (let key in configHeaders) {
if (configHeaders.hasOwnProperty(key)) {
headers.append(key, configHeaders[key]);
}
}
}
let params = {
method: 'GET',
headers: headers,
mode: 'cors',
cache: 'default',
// The default policy of Fetch API in the whatwg standard
// Safari incorrectly indicates 'no-referrer' as default policy, fuck it
referrerPolicy: 'no-referrer-when-downgrade'
};
// cors is enabled by default
if (dataSource.cors === false) {
// no-cors means 'disregard cors policy', which can only be used in ServiceWorker
params.mode = 'same-origin';
}
// withCredentials is disabled by default
if (dataSource.withCredentials) {
params.credentials = 'include';
}
// referrerPolicy from config
if (dataSource.referrerPolicy) {
params.referrerPolicy = dataSource.referrerPolicy;
}
this._status = LoaderStatus.kConnecting;
self.fetch(seekConfig.url, params).then((res) => {
if (this._requestAbort) {
this._requestAbort = false;
this._status = LoaderStatus.kIdle;
return;
}
if (res.ok && (res.status >= 200 && res.status <= 299)) {
if (res.url !== seekConfig.url) {
if (this._onURLRedirect) {
let redirectedURL = this._seekHandler.removeURLParameters(res.url);
this._onURLRedirect(redirectedURL);
}
}
let lengthHeader = res.headers.get('Content-Length');
if (lengthHeader != null) {
this._contentLength = parseInt(lengthHeader);
if (this._contentLength !== 0) {
if (this._onContentLengthKnown) {
this._onContentLengthKnown(this._contentLength);
}
}
}
return this._pump.call(this, res.body.getReader());
} else {
this._status = LoaderStatus.kError;
if (this._onError) {
this._onError(LoaderErrors.HTTP_STATUS_CODE_INVALID, {code: res.status, msg: res.statusText});
} else {
throw new RuntimeException('FetchStreamLoader: Http code invalid, ' + res.status + ' ' + res.statusText);
}
}
}).catch((e) => {
this._status = LoaderStatus.kError;
if (this._onError) {
this._onError(LoaderErrors.EXCEPTION, {code: -1, msg: e.message});
} else {
throw e;
}
});
}
abort() {
this._requestAbort = true;
}
_pump(reader) { // ReadableStreamReader
return reader.read().then((result) => {
if (result.done) {
this._status = LoaderStatus.kComplete;
if (this._onComplete) {
this._onComplete(this._range.from, this._range.from + this._receivedLength - 1);
}
} else {
if (this._requestAbort === true) {
this._requestAbort = false;
this._status = LoaderStatus.kComplete;
return reader.cancel();
}
this._status = LoaderStatus.kBuffering;
let chunk = result.value.buffer;
let byteStart = this._range.from + this._receivedLength;
this._receivedLength += chunk.byteLength;
if (this._onDataArrival) {
this._onDataArrival(chunk, byteStart, this._receivedLength);
}
this._pump(reader);
}
}).catch((e) => {
if (e.code === 11 && Browser.msedge) { // InvalidStateError on Microsoft Edge
// Workaround: Edge may throw InvalidStateError after ReadableStreamReader.cancel() call
// Ignore the unknown exception.
// Related issue: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/11265202/
return;
}
this._status = LoaderStatus.kError;
let type = 0;
let info = null;
if ((e.code === 19 || e.message === 'network error') && // NETWORK_ERR
(this._contentLength === null ||
(this._contentLength !== null && this._receivedLength < this._contentLength))) {
type = LoaderErrors.EARLY_EOF;
info = {code: e.code, msg: 'Fetch stream meet Early-EOF'};
} else {
type = LoaderErrors.EXCEPTION;
info = {code: e.code, msg: e.message};
}
if (this._onError) {
this._onError(type, info);
} else {
throw new RuntimeException(info.msg);
}
});
}
}
export default FetchStreamLoader;

View File

@ -0,0 +1,645 @@
/*
* 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 SpeedSampler from './speed-sampler.js';
import {LoaderStatus, LoaderErrors} from './loader.js';
import FetchStreamLoader from './fetch-stream-loader.js';
import MozChunkedLoader from './xhr-moz-chunked-loader.js';
import MSStreamLoader from './xhr-msstream-loader.js';
import RangeLoader from './xhr-range-loader.js';
import WebSocketLoader from './websocket-loader.js';
import RangeSeekHandler from './range-seek-handler.js';
import ParamSeekHandler from './param-seek-handler.js';
import {RuntimeException, IllegalStateException, InvalidArgumentException} from '../utils/exception.js';
/**
* DataSource: {
* url: string,
* filesize: number,
* cors: boolean,
* withCredentials: boolean
* }
*
*/
// Manage IO Loaders
class IOController {
constructor(dataSource, config, extraData) {
this.TAG = 'IOController';
this._config = config;
this._extraData = extraData;
this._stashInitialSize = 1024 * 384; // default initial size: 384KB
if (config.stashInitialSize != undefined && config.stashInitialSize > 0) {
// apply from config
this._stashInitialSize = config.stashInitialSize;
}
this._stashUsed = 0;
this._stashSize = this._stashInitialSize;
this._bufferSize = 1024 * 1024 * 3; // initial size: 3MB
this._stashBuffer = new ArrayBuffer(this._bufferSize);
this._stashByteStart = 0;
this._enableStash = true;
if (config.enableStashBuffer === false) {
this._enableStash = false;
}
this._loader = null;
this._loaderClass = null;
this._seekHandler = null;
this._dataSource = dataSource;
this._isWebSocketURL = /wss?:\/\/(.+?)/.test(dataSource.url);
this._refTotalLength = dataSource.filesize ? dataSource.filesize : null;
this._totalLength = this._refTotalLength;
this._fullRequestFlag = false;
this._currentRange = null;
this._redirectedURL = null;
this._speedNormalized = 0;
this._speedSampler = new SpeedSampler();
this._speedNormalizeList = [64, 128, 256, 384, 512, 768, 1024, 1536, 2048, 3072, 4096];
this._isEarlyEofReconnecting = false;
this._paused = false;
this._resumeFrom = 0;
this._onDataArrival = null;
this._onSeeked = null;
this._onError = null;
this._onComplete = null;
this._onRedirect = null;
this._onRecoveredEarlyEof = null;
this._selectSeekHandler();
this._selectLoader();
this._createLoader();
}
destroy() {
if (this._loader.isWorking()) {
this._loader.abort();
}
this._loader.destroy();
this._loader = null;
this._loaderClass = null;
this._dataSource = null;
this._stashBuffer = null;
this._stashUsed = this._stashSize = this._bufferSize = this._stashByteStart = 0;
this._currentRange = null;
this._speedSampler = null;
this._isEarlyEofReconnecting = false;
this._onDataArrival = null;
this._onSeeked = null;
this._onError = null;
this._onComplete = null;
this._onRedirect = null;
this._onRecoveredEarlyEof = null;
this._extraData = null;
}
isWorking() {
return this._loader && this._loader.isWorking() && !this._paused;
}
isPaused() {
return this._paused;
}
get status() {
return this._loader.status;
}
get extraData() {
return this._extraData;
}
set extraData(data) {
this._extraData = data;
}
// prototype: function onDataArrival(chunks: ArrayBuffer, byteStart: number): number
get onDataArrival() {
return this._onDataArrival;
}
set onDataArrival(callback) {
this._onDataArrival = callback;
}
get onSeeked() {
return this._onSeeked;
}
set onSeeked(callback) {
this._onSeeked = callback;
}
// prototype: function onError(type: number, info: {code: number, msg: string}): void
get onError() {
return this._onError;
}
set onError(callback) {
this._onError = callback;
}
get onComplete() {
return this._onComplete;
}
set onComplete(callback) {
this._onComplete = callback;
}
get onRedirect() {
return this._onRedirect;
}
set onRedirect(callback) {
this._onRedirect = callback;
}
get onRecoveredEarlyEof() {
return this._onRecoveredEarlyEof;
}
set onRecoveredEarlyEof(callback) {
this._onRecoveredEarlyEof = callback;
}
get currentURL() {
return this._dataSource.url;
}
get hasRedirect() {
return (this._redirectedURL != null || this._dataSource.redirectedURL != undefined);
}
get currentRedirectedURL() {
return this._redirectedURL || this._dataSource.redirectedURL;
}
// in KB/s
get currentSpeed() {
if (this._loaderClass === RangeLoader) {
// SpeedSampler is inaccuracy if loader is RangeLoader
return this._loader.currentSpeed;
}
return this._speedSampler.lastSecondKBps;
}
get loaderType() {
return this._loader.type;
}
_selectSeekHandler() {
let config = this._config;
if (config.seekType === 'range') {
this._seekHandler = new RangeSeekHandler(this._config.rangeLoadZeroStart);
} else if (config.seekType === 'param') {
let paramStart = config.seekParamStart || 'bstart';
let paramEnd = config.seekParamEnd || 'bend';
this._seekHandler = new ParamSeekHandler(paramStart, paramEnd);
} else if (config.seekType === 'custom') {
if (typeof config.customSeekHandler !== 'function') {
throw new InvalidArgumentException('Custom seekType specified in config but invalid customSeekHandler!');
}
this._seekHandler = new config.customSeekHandler();
} else {
throw new InvalidArgumentException(`Invalid seekType in config: ${config.seekType}`);
}
}
_selectLoader() {
if (this._isWebSocketURL) {
this._loaderClass = WebSocketLoader;
} else if (FetchStreamLoader.isSupported()) {
this._loaderClass = FetchStreamLoader;
} else if (MozChunkedLoader.isSupported()) {
this._loaderClass = MozChunkedLoader;
} else if (RangeLoader.isSupported()) {
this._loaderClass = RangeLoader;
} else {
throw new RuntimeException('Your browser doesn\'t support xhr with arraybuffer responseType!');
}
}
_createLoader() {
this._loader = new this._loaderClass(this._seekHandler, this._config);
if (this._loader.needStashBuffer === false) {
this._enableStash = false;
}
this._loader.onContentLengthKnown = this._onContentLengthKnown.bind(this);
this._loader.onURLRedirect = this._onURLRedirect.bind(this);
this._loader.onDataArrival = this._onLoaderChunkArrival.bind(this);
this._loader.onComplete = this._onLoaderComplete.bind(this);
this._loader.onError = this._onLoaderError.bind(this);
}
open(optionalFrom) {
this._currentRange = {from: 0, to: -1};
if (optionalFrom) {
this._currentRange.from = optionalFrom;
}
this._speedSampler.reset();
if (!optionalFrom) {
this._fullRequestFlag = true;
}
this._loader.open(this._dataSource, Object.assign({}, this._currentRange));
}
abort() {
this._loader.abort();
if (this._paused) {
this._paused = false;
this._resumeFrom = 0;
}
}
pause() {
if (this.isWorking()) {
this._loader.abort();
if (this._stashUsed !== 0) {
this._resumeFrom = this._stashByteStart;
this._currentRange.to = this._stashByteStart - 1;
} else {
this._resumeFrom = this._currentRange.to + 1;
}
this._stashUsed = 0;
this._stashByteStart = 0;
this._paused = true;
}
}
resume() {
if (this._paused) {
this._paused = false;
let bytes = this._resumeFrom;
this._resumeFrom = 0;
this._internalSeek(bytes, true);
}
}
seek(bytes) {
this._paused = false;
this._stashUsed = 0;
this._stashByteStart = 0;
this._internalSeek(bytes, true);
}
/**
* When seeking request is from media seeking, unconsumed stash data should be dropped
* However, stash data shouldn't be dropped if seeking requested from http reconnection
*
* @dropUnconsumed: Ignore and discard all unconsumed data in stash buffer
*/
_internalSeek(bytes, dropUnconsumed) {
if (this._loader.isWorking()) {
this._loader.abort();
}
// dispatch & flush stash buffer before seek
this._flushStashBuffer(dropUnconsumed);
this._loader.destroy();
this._loader = null;
let requestRange = {from: bytes, to: -1};
this._currentRange = {from: requestRange.from, to: -1};
this._speedSampler.reset();
this._stashSize = this._stashInitialSize;
this._createLoader();
this._loader.open(this._dataSource, requestRange);
if (this._onSeeked) {
this._onSeeked();
}
}
updateUrl(url) {
if (!url || typeof url !== 'string' || url.length === 0) {
throw new InvalidArgumentException('Url must be a non-empty string!');
}
this._dataSource.url = url;
// TODO: replace with new url
}
_expandBuffer(expectedBytes) {
let bufferNewSize = this._stashSize;
while (bufferNewSize + 1024 * 1024 * 1 < expectedBytes) {
bufferNewSize *= 2;
}
bufferNewSize += 1024 * 1024 * 1; // bufferSize = stashSize + 1MB
if (bufferNewSize === this._bufferSize) {
return;
}
let newBuffer = new ArrayBuffer(bufferNewSize);
if (this._stashUsed > 0) { // copy existing data into new buffer
let stashOldArray = new Uint8Array(this._stashBuffer, 0, this._stashUsed);
let stashNewArray = new Uint8Array(newBuffer, 0, bufferNewSize);
stashNewArray.set(stashOldArray, 0);
}
this._stashBuffer = newBuffer;
this._bufferSize = bufferNewSize;
}
_normalizeSpeed(input) {
let list = this._speedNormalizeList;
let last = list.length - 1;
let mid = 0;
let lbound = 0;
let ubound = last;
if (input < list[0]) {
return list[0];
}
// binary search
while (lbound <= ubound) {
mid = lbound + Math.floor((ubound - lbound) / 2);
if (mid === last || (input >= list[mid] && input < list[mid + 1])) {
return list[mid];
} else if (list[mid] < input) {
lbound = mid + 1;
} else {
ubound = mid - 1;
}
}
}
_adjustStashSize(normalized) {
let stashSizeKB = 0;
if (this._config.isLive) {
// live stream: always use single normalized speed for size of stashSizeKB
stashSizeKB = normalized;
} else {
if (normalized < 512) {
stashSizeKB = normalized;
} else if (normalized >= 512 && normalized <= 1024) {
stashSizeKB = Math.floor(normalized * 1.5);
} else {
stashSizeKB = normalized * 2;
}
}
if (stashSizeKB > 8192) {
stashSizeKB = 8192;
}
let bufferSize = stashSizeKB * 1024 + 1024 * 1024 * 1; // stashSize + 1MB
if (this._bufferSize < bufferSize) {
this._expandBuffer(bufferSize);
}
this._stashSize = stashSizeKB * 1024;
}
_dispatchChunks(chunks, byteStart) {
this._currentRange.to = byteStart + chunks.byteLength - 1;
return this._onDataArrival(chunks, byteStart);
}
_onURLRedirect(redirectedURL) {
this._redirectedURL = redirectedURL;
if (this._onRedirect) {
this._onRedirect(redirectedURL);
}
}
_onContentLengthKnown(contentLength) {
if (contentLength && this._fullRequestFlag) {
this._totalLength = contentLength;
this._fullRequestFlag = false;
}
}
_onLoaderChunkArrival(chunk, byteStart, receivedLength) {
if (!this._onDataArrival) {
throw new IllegalStateException('IOController: No existing consumer (onDataArrival) callback!');
}
if (this._paused) {
return;
}
if (this._isEarlyEofReconnecting) {
// Auto-reconnect for EarlyEof succeed, notify to upper-layer by callback
this._isEarlyEofReconnecting = false;
if (this._onRecoveredEarlyEof) {
this._onRecoveredEarlyEof();
}
}
this._speedSampler.addBytes(chunk.byteLength);
// adjust stash buffer size according to network speed dynamically
let KBps = this._speedSampler.lastSecondKBps;
if (KBps !== 0) {
let normalized = this._normalizeSpeed(KBps);
if (this._speedNormalized !== normalized) {
this._speedNormalized = normalized;
this._adjustStashSize(normalized);
}
}
if (!this._enableStash) { // disable stash
if (this._stashUsed === 0) {
// dispatch chunk directly to consumer;
// check ret value (consumed bytes) and stash unconsumed to stashBuffer
let consumed = this._dispatchChunks(chunk, byteStart);
if (consumed < chunk.byteLength) { // unconsumed data remain.
let remain = chunk.byteLength - consumed;
if (remain > this._bufferSize) {
this._expandBuffer(remain);
}
let stashArray = new Uint8Array(this._stashBuffer, 0, this._bufferSize);
stashArray.set(new Uint8Array(chunk, consumed), 0);
this._stashUsed += remain;
this._stashByteStart = byteStart + consumed;
}
} else {
// else: Merge chunk into stashBuffer, and dispatch stashBuffer to consumer.
if (this._stashUsed + chunk.byteLength > this._bufferSize) {
this._expandBuffer(this._stashUsed + chunk.byteLength);
}
let stashArray = new Uint8Array(this._stashBuffer, 0, this._bufferSize);
stashArray.set(new Uint8Array(chunk), this._stashUsed);
this._stashUsed += chunk.byteLength;
let consumed = this._dispatchChunks(this._stashBuffer.slice(0, this._stashUsed), this._stashByteStart);
if (consumed < this._stashUsed && consumed > 0) { // unconsumed data remain
let remainArray = new Uint8Array(this._stashBuffer, consumed);
stashArray.set(remainArray, 0);
}
this._stashUsed -= consumed;
this._stashByteStart += consumed;
}
} else { // enable stash
if (this._stashUsed === 0 && this._stashByteStart === 0) { // seeked? or init chunk?
// This is the first chunk after seek action
this._stashByteStart = byteStart;
}
if (this._stashUsed + chunk.byteLength <= this._stashSize) {
// just stash
let stashArray = new Uint8Array(this._stashBuffer, 0, this._stashSize);
stashArray.set(new Uint8Array(chunk), this._stashUsed);
this._stashUsed += chunk.byteLength;
} else { // stashUsed + chunkSize > stashSize, size limit exceeded
let stashArray = new Uint8Array(this._stashBuffer, 0, this._bufferSize);
if (this._stashUsed > 0) { // There're stash datas in buffer
// dispatch the whole stashBuffer, and stash remain data
// then append chunk to stashBuffer (stash)
let buffer = this._stashBuffer.slice(0, this._stashUsed);
let consumed = this._dispatchChunks(buffer, this._stashByteStart);
if (consumed < buffer.byteLength) {
if (consumed > 0) {
let remainArray = new Uint8Array(buffer, consumed);
stashArray.set(remainArray, 0);
this._stashUsed = remainArray.byteLength;
this._stashByteStart += consumed;
}
} else {
this._stashUsed = 0;
this._stashByteStart += consumed;
}
if (this._stashUsed + chunk.byteLength > this._bufferSize) {
this._expandBuffer(this._stashUsed + chunk.byteLength);
stashArray = new Uint8Array(this._stashBuffer, 0, this._bufferSize);
}
stashArray.set(new Uint8Array(chunk), this._stashUsed);
this._stashUsed += chunk.byteLength;
} else { // stash buffer empty, but chunkSize > stashSize (oh, holy shit)
// dispatch chunk directly and stash remain data
let consumed = this._dispatchChunks(chunk, byteStart);
if (consumed < chunk.byteLength) {
let remain = chunk.byteLength - consumed;
if (remain > this._bufferSize) {
this._expandBuffer(remain);
stashArray = new Uint8Array(this._stashBuffer, 0, this._bufferSize);
}
stashArray.set(new Uint8Array(chunk, consumed), 0);
this._stashUsed += remain;
this._stashByteStart = byteStart + consumed;
}
}
}
}
}
_flushStashBuffer(dropUnconsumed) {
if (this._stashUsed > 0) {
let buffer = this._stashBuffer.slice(0, this._stashUsed);
let consumed = this._dispatchChunks(buffer, this._stashByteStart);
let remain = buffer.byteLength - consumed;
if (consumed < buffer.byteLength) {
if (dropUnconsumed) {
Log.w(this.TAG, `${remain} bytes unconsumed data remain when flush buffer, dropped`);
} else {
if (consumed > 0) {
let stashArray = new Uint8Array(this._stashBuffer, 0, this._bufferSize);
let remainArray = new Uint8Array(buffer, consumed);
stashArray.set(remainArray, 0);
this._stashUsed = remainArray.byteLength;
this._stashByteStart += consumed;
}
return 0;
}
}
this._stashUsed = 0;
this._stashByteStart = 0;
return remain;
}
return 0;
}
_onLoaderComplete(from, to) {
// Force-flush stash buffer, and drop unconsumed data
this._flushStashBuffer(true);
if (this._onComplete) {
this._onComplete(this._extraData);
}
}
_onLoaderError(type, data) {
Log.e(this.TAG, `Loader error, code = ${data.code}, msg = ${data.msg}`);
this._flushStashBuffer(false);
if (this._isEarlyEofReconnecting) {
// Auto-reconnect for EarlyEof failed, throw UnrecoverableEarlyEof error to upper-layer
this._isEarlyEofReconnecting = false;
type = LoaderErrors.UNRECOVERABLE_EARLY_EOF;
}
switch (type) {
case LoaderErrors.EARLY_EOF: {
if (!this._config.isLive) {
// Do internal http reconnect if not live stream
if (this._totalLength) {
let nextFrom = this._currentRange.to + 1;
if (nextFrom < this._totalLength) {
Log.w(this.TAG, 'Connection lost, trying reconnect...');
this._isEarlyEofReconnecting = true;
this._internalSeek(nextFrom, false);
}
return;
}
// else: We don't know totalLength, throw UnrecoverableEarlyEof
}
// live stream: throw UnrecoverableEarlyEof error to upper-layer
type = LoaderErrors.UNRECOVERABLE_EARLY_EOF;
break;
}
case LoaderErrors.UNRECOVERABLE_EARLY_EOF:
case LoaderErrors.CONNECTING_TIMEOUT:
case LoaderErrors.HTTP_STATUS_CODE_INVALID:
case LoaderErrors.EXCEPTION:
break;
}
if (this._onError) {
this._onError(type, data);
} else {
throw new RuntimeException('IOException: ' + data.msg);
}
}
}
export default IOController;

View File

@ -0,0 +1,134 @@
/*
* 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 {NotImplementedException} from '../utils/exception.js';
export const LoaderStatus = {
kIdle: 0,
kConnecting: 1,
kBuffering: 2,
kError: 3,
kComplete: 4
};
export const LoaderErrors = {
OK: 'OK',
EXCEPTION: 'Exception',
HTTP_STATUS_CODE_INVALID: 'HttpStatusCodeInvalid',
CONNECTING_TIMEOUT: 'ConnectingTimeout',
EARLY_EOF: 'EarlyEof',
UNRECOVERABLE_EARLY_EOF: 'UnrecoverableEarlyEof'
};
/* Loader has callbacks which have following prototypes:
* function onContentLengthKnown(contentLength: number): void
* function onURLRedirect(url: string): void
* function onDataArrival(chunk: ArrayBuffer, byteStart: number, receivedLength: number): void
* function onError(errorType: number, errorInfo: {code: number, msg: string}): void
* function onComplete(rangeFrom: number, rangeTo: number): void
*/
export class BaseLoader {
constructor(typeName) {
this._type = typeName || 'undefined';
this._status = LoaderStatus.kIdle;
this._needStash = false;
// callbacks
this._onContentLengthKnown = null;
this._onURLRedirect = null;
this._onDataArrival = null;
this._onError = null;
this._onComplete = null;
}
destroy() {
this._status = LoaderStatus.kIdle;
this._onContentLengthKnown = null;
this._onURLRedirect = null;
this._onDataArrival = null;
this._onError = null;
this._onComplete = null;
}
isWorking() {
return this._status === LoaderStatus.kConnecting || this._status === LoaderStatus.kBuffering;
}
get type() {
return this._type;
}
get status() {
return this._status;
}
get needStashBuffer() {
return this._needStash;
}
get onContentLengthKnown() {
return this._onContentLengthKnown;
}
set onContentLengthKnown(callback) {
this._onContentLengthKnown = callback;
}
get onURLRedirect() {
return this._onURLRedirect;
}
set onURLRedirect(callback) {
this._onURLRedirect = callback;
}
get onDataArrival() {
return this._onDataArrival;
}
set onDataArrival(callback) {
this._onDataArrival = callback;
}
get onError() {
return this._onError;
}
set onError(callback) {
this._onError = callback;
}
get onComplete() {
return this._onComplete;
}
set onComplete(callback) {
this._onComplete = callback;
}
// pure virtual
open(dataSource, range) {
throw new NotImplementedException('Unimplemented abstract function!');
}
abort() {
throw new NotImplementedException('Unimplemented abstract function!');
}
}

View File

@ -0,0 +1,85 @@
/*
* 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 ParamSeekHandler {
constructor(paramStart, paramEnd) {
this._startName = paramStart;
this._endName = paramEnd;
}
getConfig(baseUrl, range) {
let url = baseUrl;
if (range.from !== 0 || range.to !== -1) {
let needAnd = true;
if (url.indexOf('?') === -1) {
url += '?';
needAnd = false;
}
if (needAnd) {
url += '&';
}
url += `${this._startName}=${range.from.toString()}`;
if (range.to !== -1) {
url += `&${this._endName}=${range.to.toString()}`;
}
}
return {
url: url,
headers: {}
};
}
removeURLParameters(seekedURL) {
let baseURL = seekedURL.split('?')[0];
let params = undefined;
let queryIndex = seekedURL.indexOf('?');
if (queryIndex !== -1) {
params = seekedURL.substring(queryIndex + 1);
}
let resultParams = '';
if (params != undefined && params.length > 0) {
let pairs = params.split('&');
for (let i = 0; i < pairs.length; i++) {
let pair = pairs[i].split('=');
let requireAnd = (i > 0);
if (pair[0] !== this._startName && pair[0] !== this._endName) {
if (requireAnd) {
resultParams += '&';
}
resultParams += pairs[i];
}
}
}
return (resultParams.length === 0) ? baseURL : baseURL + '?' + resultParams;
}
}
export default ParamSeekHandler;

View File

@ -0,0 +1,52 @@
/*
* 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 RangeSeekHandler {
constructor(zeroStart) {
this._zeroStart = zeroStart || false;
}
getConfig(url, range) {
let headers = {};
if (range.from !== 0 || range.to !== -1) {
let param;
if (range.to !== -1) {
param = `bytes=${range.from.toString()}-${range.to.toString()}`;
} else {
param = `bytes=${range.from.toString()}-`;
}
headers['Range'] = param;
} else if (this._zeroStart) {
headers['Range'] = 'bytes=0-';
}
return {
url: url,
headers: headers
};
}
removeURLParameters(seekedURL) {
return seekedURL;
}
}
export default RangeSeekHandler;

View File

@ -0,0 +1,93 @@
/*
* 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.
*/
// Utility class to calculate realtime network I/O speed
class SpeedSampler {
constructor() {
// milliseconds
this._firstCheckpoint = 0;
this._lastCheckpoint = 0;
this._intervalBytes = 0;
this._totalBytes = 0;
this._lastSecondBytes = 0;
// compatibility detection
if (self.performance && self.performance.now) {
this._now = self.performance.now.bind(self.performance);
} else {
this._now = Date.now;
}
}
reset() {
this._firstCheckpoint = this._lastCheckpoint = 0;
this._totalBytes = this._intervalBytes = 0;
this._lastSecondBytes = 0;
}
addBytes(bytes) {
if (this._firstCheckpoint === 0) {
this._firstCheckpoint = this._now();
this._lastCheckpoint = this._firstCheckpoint;
this._intervalBytes += bytes;
this._totalBytes += bytes;
} else if (this._now() - this._lastCheckpoint < 1000) {
this._intervalBytes += bytes;
this._totalBytes += bytes;
} else { // duration >= 1000
this._lastSecondBytes = this._intervalBytes;
this._intervalBytes = bytes;
this._totalBytes += bytes;
this._lastCheckpoint = this._now();
}
}
get currentKBps() {
this.addBytes(0);
let durationSeconds = (this._now() - this._lastCheckpoint) / 1000;
if (durationSeconds == 0) durationSeconds = 1;
return (this._intervalBytes / durationSeconds) / 1024;
}
get lastSecondKBps() {
this.addBytes(0);
if (this._lastSecondBytes !== 0) {
return this._lastSecondBytes / 1024;
} else { // lastSecondBytes === 0
if (this._now() - this._lastCheckpoint >= 500) {
// if time interval since last checkpoint has exceeded 500ms
// the speed is nearly accurate
return this.currentKBps;
} else {
// We don't know
return 0;
}
}
}
get averageKBps() {
let durationSeconds = (this._now() - this._firstCheckpoint) / 1000;
return (this._totalBytes / durationSeconds) / 1024;
}
}
export default SpeedSampler;

View File

@ -0,0 +1,151 @@
/*
* 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 {BaseLoader, LoaderStatus, LoaderErrors} from './loader.js';
import {RuntimeException} from '../utils/exception.js';
// For FLV over WebSocket live stream
class WebSocketLoader extends BaseLoader {
static isSupported() {
try {
return (typeof self.WebSocket !== 'undefined');
} catch (e) {
return false;
}
}
constructor() {
super('websocket-loader');
this.TAG = 'WebSocketLoader';
this._needStash = true;
this._ws = null;
this._requestAbort = false;
this._receivedLength = 0;
}
destroy() {
if (this._ws) {
this.abort();
}
super.destroy();
}
open(dataSource) {
try {
let ws = this._ws = new self.WebSocket(dataSource.url);
ws.binaryType = 'arraybuffer';
ws.onopen = this._onWebSocketOpen.bind(this);
ws.onclose = this._onWebSocketClose.bind(this);
ws.onmessage = this._onWebSocketMessage.bind(this);
ws.onerror = this._onWebSocketError.bind(this);
this._status = LoaderStatus.kConnecting;
} catch (e) {
this._status = LoaderStatus.kError;
let info = {code: e.code, msg: e.message};
if (this._onError) {
this._onError(LoaderErrors.EXCEPTION, info);
} else {
throw new RuntimeException(info.msg);
}
}
}
abort() {
let ws = this._ws;
if (ws && (ws.readyState === 0 || ws.readyState === 1)) { // CONNECTING || OPEN
this._requestAbort = true;
ws.close();
}
this._ws = null;
this._status = LoaderStatus.kComplete;
}
_onWebSocketOpen(e) {
this._status = LoaderStatus.kBuffering;
}
_onWebSocketClose(e) {
if (this._requestAbort === true) {
this._requestAbort = false;
return;
}
this._status = LoaderStatus.kComplete;
if (this._onComplete) {
this._onComplete(0, this._receivedLength - 1);
}
}
_onWebSocketMessage(e) {
if (e.data instanceof ArrayBuffer) {
this._dispatchArrayBuffer(e.data);
} else if (e.data instanceof Blob) {
let reader = new FileReader();
reader.onload = () => {
this._dispatchArrayBuffer(reader.result);
};
reader.readAsArrayBuffer(e.data);
} else {
this._status = LoaderStatus.kError;
let info = {code: -1, msg: 'Unsupported WebSocket message type: ' + e.data.constructor.name};
if (this._onError) {
this._onError(LoaderErrors.EXCEPTION, info);
} else {
throw new RuntimeException(info.msg);
}
}
}
_dispatchArrayBuffer(arraybuffer) {
let chunk = arraybuffer;
let byteStart = this._receivedLength;
this._receivedLength += chunk.byteLength;
if (this._onDataArrival) {
this._onDataArrival(chunk, byteStart, this._receivedLength);
}
}
_onWebSocketError(e) {
this._status = LoaderStatus.kError;
let info = {
code: e.code,
msg: e.message
};
if (this._onError) {
this._onError(LoaderErrors.EXCEPTION, info);
} else {
throw new RuntimeException(info.msg);
}
}
}
export default WebSocketLoader;

View File

@ -0,0 +1,200 @@
/*
* 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 {BaseLoader, LoaderStatus, LoaderErrors} from './loader.js';
import {RuntimeException} from '../utils/exception.js';
// For FireFox browser which supports `xhr.responseType = 'moz-chunked-arraybuffer'`
class MozChunkedLoader extends BaseLoader {
static isSupported() {
try {
let xhr = new XMLHttpRequest();
// Firefox 37- requires .open() to be called before setting responseType
xhr.open('GET', 'https://example.com', true);
xhr.responseType = 'moz-chunked-arraybuffer';
return (xhr.responseType === 'moz-chunked-arraybuffer');
} catch (e) {
Log.w('MozChunkedLoader', e.message);
return false;
}
}
constructor(seekHandler, config) {
super('xhr-moz-chunked-loader');
this.TAG = 'MozChunkedLoader';
this._seekHandler = seekHandler;
this._config = config;
this._needStash = true;
this._xhr = null;
this._requestAbort = false;
this._contentLength = null;
this._receivedLength = 0;
}
destroy() {
if (this.isWorking()) {
this.abort();
}
if (this._xhr) {
this._xhr.onreadystatechange = null;
this._xhr.onprogress = null;
this._xhr.onloadend = null;
this._xhr.onerror = null;
this._xhr = null;
}
super.destroy();
}
open(dataSource, range) {
this._dataSource = dataSource;
this._range = range;
let sourceURL = dataSource.url;
if (this._config.reuseRedirectedURL && dataSource.redirectedURL != undefined) {
sourceURL = dataSource.redirectedURL;
}
let seekConfig = this._seekHandler.getConfig(sourceURL, range);
this._requestURL = seekConfig.url;
let xhr = this._xhr = new XMLHttpRequest();
xhr.open('GET', seekConfig.url, true);
xhr.responseType = 'moz-chunked-arraybuffer';
xhr.onreadystatechange = this._onReadyStateChange.bind(this);
xhr.onprogress = this._onProgress.bind(this);
xhr.onloadend = this._onLoadEnd.bind(this);
xhr.onerror = this._onXhrError.bind(this);
// cors is auto detected and enabled by xhr
// withCredentials is disabled by default
if (dataSource.withCredentials) {
xhr.withCredentials = true;
}
if (typeof seekConfig.headers === 'object') {
let headers = seekConfig.headers;
for (let key in headers) {
if (headers.hasOwnProperty(key)) {
xhr.setRequestHeader(key, headers[key]);
}
}
}
this._status = LoaderStatus.kConnecting;
xhr.send();
}
abort() {
this._requestAbort = true;
if (this._xhr) {
this._xhr.abort();
}
this._status = LoaderStatus.kComplete;
}
_onReadyStateChange(e) {
let xhr = e.target;
if (xhr.readyState === 2) { // HEADERS_RECEIVED
if (xhr.responseURL != undefined && xhr.responseURL !== this._requestURL) {
if (this._onURLRedirect) {
let redirectedURL = this._seekHandler.removeURLParameters(xhr.responseURL);
this._onURLRedirect(redirectedURL);
}
}
if (xhr.status !== 0 && (xhr.status < 200 || xhr.status > 299)) {
this._status = LoaderStatus.kError;
if (this._onError) {
this._onError(LoaderErrors.HTTP_STATUS_CODE_INVALID, {code: xhr.status, msg: xhr.statusText});
} else {
throw new RuntimeException('MozChunkedLoader: Http code invalid, ' + xhr.status + ' ' + xhr.statusText);
}
} else {
this._status = LoaderStatus.kBuffering;
}
}
}
_onProgress(e) {
if (this._status === LoaderStatus.kError) {
// Ignore error response
return;
}
if (this._contentLength === null) {
if (e.total !== null && e.total !== 0) {
this._contentLength = e.total;
if (this._onContentLengthKnown) {
this._onContentLengthKnown(this._contentLength);
}
}
}
let chunk = e.target.response;
let byteStart = this._range.from + this._receivedLength;
this._receivedLength += chunk.byteLength;
if (this._onDataArrival) {
this._onDataArrival(chunk, byteStart, this._receivedLength);
}
}
_onLoadEnd(e) {
if (this._requestAbort === true) {
this._requestAbort = false;
return;
} else if (this._status === LoaderStatus.kError) {
return;
}
this._status = LoaderStatus.kComplete;
if (this._onComplete) {
this._onComplete(this._range.from, this._range.from + this._receivedLength - 1);
}
}
_onXhrError(e) {
this._status = LoaderStatus.kError;
let type = 0;
let info = null;
if (this._contentLength && e.loaded < this._contentLength) {
type = LoaderErrors.EARLY_EOF;
info = {code: -1, msg: 'Moz-Chunked stream meet Early-Eof'};
} else {
type = LoaderErrors.EXCEPTION;
info = {code: -1, msg: e.constructor.name + ' ' + e.type};
}
if (this._onError) {
this._onError(type, info);
} else {
throw new RuntimeException(info.msg);
}
}
}
export default MozChunkedLoader;

View File

@ -0,0 +1,296 @@
/*
* 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 {BaseLoader, LoaderStatus, LoaderErrors} from './loader.js';
import {RuntimeException} from '../utils/exception.js';
/* Notice: ms-stream may cause IE/Edge browser crash if seek too frequently!!!
* The browser may crash in wininet.dll. Disable for now.
*
* For IE11/Edge browser by microsoft which supports `xhr.responseType = 'ms-stream'`
* Notice that ms-stream API sucks. The buffer is always expanding along with downloading.
*
* We need to abort the xhr if buffer size exceeded limit size (e.g. 16 MiB), then do reconnect.
* in order to release previous ArrayBuffer to avoid memory leak
*
* Otherwise, the ArrayBuffer will increase to a terrible size that equals final file size.
*/
class MSStreamLoader extends BaseLoader {
static isSupported() {
try {
if (typeof self.MSStream === 'undefined' || typeof self.MSStreamReader === 'undefined') {
return false;
}
let xhr = new XMLHttpRequest();
xhr.open('GET', 'https://example.com', true);
xhr.responseType = 'ms-stream';
return (xhr.responseType === 'ms-stream');
} catch (e) {
Log.w('MSStreamLoader', e.message);
return false;
}
}
constructor(seekHandler, config) {
super('xhr-msstream-loader');
this.TAG = 'MSStreamLoader';
this._seekHandler = seekHandler;
this._config = config;
this._needStash = true;
this._xhr = null;
this._reader = null; // MSStreamReader
this._totalRange = null;
this._currentRange = null;
this._currentRequestURL = null;
this._currentRedirectedURL = null;
this._contentLength = null;
this._receivedLength = 0;
this._bufferLimit = 16 * 1024 * 1024; // 16MB
this._lastTimeBufferSize = 0;
this._isReconnecting = false;
}
destroy() {
if (this.isWorking()) {
this.abort();
}
if (this._reader) {
this._reader.onprogress = null;
this._reader.onload = null;
this._reader.onerror = null;
this._reader = null;
}
if (this._xhr) {
this._xhr.onreadystatechange = null;
this._xhr = null;
}
super.destroy();
}
open(dataSource, range) {
this._internalOpen(dataSource, range, false);
}
_internalOpen(dataSource, range, isSubrange) {
this._dataSource = dataSource;
if (!isSubrange) {
this._totalRange = range;
} else {
this._currentRange = range;
}
let sourceURL = dataSource.url;
if (this._config.reuseRedirectedURL) {
if (this._currentRedirectedURL != undefined) {
sourceURL = this._currentRedirectedURL;
} else if (dataSource.redirectedURL != undefined) {
sourceURL = dataSource.redirectedURL;
}
}
let seekConfig = this._seekHandler.getConfig(sourceURL, range);
this._currentRequestURL = seekConfig.url;
let reader = this._reader = new self.MSStreamReader();
reader.onprogress = this._msrOnProgress.bind(this);
reader.onload = this._msrOnLoad.bind(this);
reader.onerror = this._msrOnError.bind(this);
let xhr = this._xhr = new XMLHttpRequest();
xhr.open('GET', seekConfig.url, true);
xhr.responseType = 'ms-stream';
xhr.onreadystatechange = this._xhrOnReadyStateChange.bind(this);
xhr.onerror = this._xhrOnError.bind(this);
if (dataSource.withCredentials) {
xhr.withCredentials = true;
}
if (typeof seekConfig.headers === 'object') {
let headers = seekConfig.headers;
for (let key in headers) {
if (headers.hasOwnProperty(key)) {
xhr.setRequestHeader(key, headers[key]);
}
}
}
if (this._isReconnecting) {
this._isReconnecting = false;
} else {
this._status = LoaderStatus.kConnecting;
}
xhr.send();
}
abort() {
this._internalAbort();
this._status = LoaderStatus.kComplete;
}
_internalAbort() {
if (this._reader) {
if (this._reader.readyState === 1) { // LOADING
this._reader.abort();
}
this._reader.onprogress = null;
this._reader.onload = null;
this._reader.onerror = null;
this._reader = null;
}
if (this._xhr) {
this._xhr.abort();
this._xhr.onreadystatechange = null;
this._xhr = null;
}
}
_xhrOnReadyStateChange(e) {
let xhr = e.target;
if (xhr.readyState === 2) { // HEADERS_RECEIVED
if (xhr.status >= 200 && xhr.status <= 299) {
this._status = LoaderStatus.kBuffering;
if (xhr.responseURL != undefined) {
let redirectedURL = this._seekHandler.removeURLParameters(xhr.responseURL);
if (xhr.responseURL !== this._currentRequestURL && redirectedURL !== this._currentRedirectedURL) {
this._currentRedirectedURL = redirectedURL;
if (this._onURLRedirect) {
this._onURLRedirect(redirectedURL);
}
}
}
let lengthHeader = xhr.getResponseHeader('Content-Length');
if (lengthHeader != null && this._contentLength == null) {
let length = parseInt(lengthHeader);
if (length > 0) {
this._contentLength = length;
if (this._onContentLengthKnown) {
this._onContentLengthKnown(this._contentLength);
}
}
}
} else {
this._status = LoaderStatus.kError;
if (this._onError) {
this._onError(LoaderErrors.HTTP_STATUS_CODE_INVALID, {code: xhr.status, msg: xhr.statusText});
} else {
throw new RuntimeException('MSStreamLoader: Http code invalid, ' + xhr.status + ' ' + xhr.statusText);
}
}
} else if (xhr.readyState === 3) { // LOADING
if (xhr.status >= 200 && xhr.status <= 299) {
this._status = LoaderStatus.kBuffering;
let msstream = xhr.response;
this._reader.readAsArrayBuffer(msstream);
}
}
}
_xhrOnError(e) {
this._status = LoaderStatus.kError;
let type = LoaderErrors.EXCEPTION;
let info = {code: -1, msg: e.constructor.name + ' ' + e.type};
if (this._onError) {
this._onError(type, info);
} else {
throw new RuntimeException(info.msg);
}
}
_msrOnProgress(e) {
let reader = e.target;
let bigbuffer = reader.result;
if (bigbuffer == null) { // result may be null, workaround for buggy M$
this._doReconnectIfNeeded();
return;
}
let slice = bigbuffer.slice(this._lastTimeBufferSize);
this._lastTimeBufferSize = bigbuffer.byteLength;
let byteStart = this._totalRange.from + this._receivedLength;
this._receivedLength += slice.byteLength;
if (this._onDataArrival) {
this._onDataArrival(slice, byteStart, this._receivedLength);
}
if (bigbuffer.byteLength >= this._bufferLimit) {
Log.v(this.TAG, `MSStream buffer exceeded max size near ${byteStart + slice.byteLength}, reconnecting...`);
this._doReconnectIfNeeded();
}
}
_doReconnectIfNeeded() {
if (this._contentLength == null || this._receivedLength < this._contentLength) {
this._isReconnecting = true;
this._lastTimeBufferSize = 0;
this._internalAbort();
let range = {
from: this._totalRange.from + this._receivedLength,
to: -1
};
this._internalOpen(this._dataSource, range, true);
}
}
_msrOnLoad(e) { // actually it is onComplete event
this._status = LoaderStatus.kComplete;
if (this._onComplete) {
this._onComplete(this._totalRange.from, this._totalRange.from + this._receivedLength - 1);
}
}
_msrOnError(e) {
this._status = LoaderStatus.kError;
let type = 0;
let info = null;
if (this._contentLength && this._receivedLength < this._contentLength) {
type = LoaderErrors.EARLY_EOF;
info = {code: -1, msg: 'MSStream meet Early-Eof'};
} else {
type = LoaderErrors.EARLY_EOF;
info = {code: -1, msg: e.constructor.name + ' ' + e.type};
}
if (this._onError) {
this._onError(type, info);
} else {
throw new RuntimeException(info.msg);
}
}
}
export default MSStreamLoader;

View File

@ -0,0 +1,355 @@
/*
* 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 SpeedSampler from './speed-sampler.js';
import {BaseLoader, LoaderStatus, LoaderErrors} from './loader.js';
import {RuntimeException} from '../utils/exception.js';
// Universal IO Loader, implemented by adding Range header in xhr's request header
class RangeLoader extends BaseLoader {
static isSupported() {
try {
let xhr = new XMLHttpRequest();
xhr.open('GET', 'https://example.com', true);
xhr.responseType = 'arraybuffer';
return (xhr.responseType === 'arraybuffer');
} catch (e) {
Log.w('RangeLoader', e.message);
return false;
}
}
constructor(seekHandler, config) {
super('xhr-range-loader');
this.TAG = 'RangeLoader';
this._seekHandler = seekHandler;
this._config = config;
this._needStash = false;
this._chunkSizeKBList = [
128, 256, 384, 512, 768, 1024, 1536, 2048, 3072, 4096, 5120, 6144, 7168, 8192
];
this._currentChunkSizeKB = 384;
this._currentSpeedNormalized = 0;
this._zeroSpeedChunkCount = 0;
this._xhr = null;
this._speedSampler = new SpeedSampler();
this._requestAbort = false;
this._waitForTotalLength = false;
this._totalLengthReceived = false;
this._currentRequestURL = null;
this._currentRedirectedURL = null;
this._currentRequestRange = null;
this._totalLength = null; // size of the entire file
this._contentLength = null; // Content-Length of entire request range
this._receivedLength = 0; // total received bytes
this._lastTimeLoaded = 0; // received bytes of current request sub-range
}
destroy() {
if (this.isWorking()) {
this.abort();
}
if (this._xhr) {
this._xhr.onreadystatechange = null;
this._xhr.onprogress = null;
this._xhr.onload = null;
this._xhr.onerror = null;
this._xhr = null;
}
super.destroy();
}
get currentSpeed() {
return this._speedSampler.lastSecondKBps;
}
open(dataSource, range) {
this._dataSource = dataSource;
this._range = range;
this._status = LoaderStatus.kConnecting;
let useRefTotalLength = false;
if (this._dataSource.filesize != undefined && this._dataSource.filesize !== 0) {
useRefTotalLength = true;
this._totalLength = this._dataSource.filesize;
}
if (!this._totalLengthReceived && !useRefTotalLength) {
// We need total filesize
this._waitForTotalLength = true;
this._internalOpen(this._dataSource, {from: 0, to: -1});
} else {
// We have filesize, start loading
this._openSubRange();
}
}
_openSubRange() {
let chunkSize = this._currentChunkSizeKB * 1024;
let from = this._range.from + this._receivedLength;
let to = from + chunkSize;
if (this._contentLength != null) {
if (to - this._range.from >= this._contentLength) {
to = this._range.from + this._contentLength - 1;
}
}
this._currentRequestRange = {from, to};
this._internalOpen(this._dataSource, this._currentRequestRange);
}
_internalOpen(dataSource, range) {
this._lastTimeLoaded = 0;
let sourceURL = dataSource.url;
if (this._config.reuseRedirectedURL) {
if (this._currentRedirectedURL != undefined) {
sourceURL = this._currentRedirectedURL;
} else if (dataSource.redirectedURL != undefined) {
sourceURL = dataSource.redirectedURL;
}
}
let seekConfig = this._seekHandler.getConfig(sourceURL, range);
this._currentRequestURL = seekConfig.url;
let xhr = this._xhr = new XMLHttpRequest();
xhr.open('GET', seekConfig.url, true);
xhr.responseType = 'arraybuffer';
xhr.onreadystatechange = this._onReadyStateChange.bind(this);
xhr.onprogress = this._onProgress.bind(this);
xhr.onload = this._onLoad.bind(this);
xhr.onerror = this._onXhrError.bind(this);
if (dataSource.withCredentials) {
xhr.withCredentials = true;
}
if (typeof seekConfig.headers === 'object') {
let headers = seekConfig.headers;
for (let key in headers) {
if (headers.hasOwnProperty(key)) {
xhr.setRequestHeader(key, headers[key]);
}
}
}
xhr.send();
}
abort() {
this._requestAbort = true;
this._internalAbort();
this._status = LoaderStatus.kComplete;
}
_internalAbort() {
if (this._xhr) {
this._xhr.onreadystatechange = null;
this._xhr.onprogress = null;
this._xhr.onload = null;
this._xhr.onerror = null;
this._xhr.abort();
this._xhr = null;
}
}
_onReadyStateChange(e) {
let xhr = e.target;
if (xhr.readyState === 2) { // HEADERS_RECEIVED
if (xhr.responseURL != undefined) { // if the browser support this property
let redirectedURL = this._seekHandler.removeURLParameters(xhr.responseURL);
if (xhr.responseURL !== this._currentRequestURL && redirectedURL !== this._currentRedirectedURL) {
this._currentRedirectedURL = redirectedURL;
if (this._onURLRedirect) {
this._onURLRedirect(redirectedURL);
}
}
}
if ((xhr.status >= 200 && xhr.status <= 299)) {
if (this._waitForTotalLength) {
return;
}
this._status = LoaderStatus.kBuffering;
} else {
this._status = LoaderStatus.kError;
if (this._onError) {
this._onError(LoaderErrors.HTTP_STATUS_CODE_INVALID, {code: xhr.status, msg: xhr.statusText});
} else {
throw new RuntimeException('RangeLoader: Http code invalid, ' + xhr.status + ' ' + xhr.statusText);
}
}
}
}
_onProgress(e) {
if (this._status === LoaderStatus.kError) {
// Ignore error response
return;
}
if (this._contentLength === null) {
let openNextRange = false;
if (this._waitForTotalLength) {
this._waitForTotalLength = false;
this._totalLengthReceived = true;
openNextRange = true;
let total = e.total;
this._internalAbort();
if (total != null & total !== 0) {
this._totalLength = total;
}
}
// calculate currrent request range's contentLength
if (this._range.to === -1) {
this._contentLength = this._totalLength - this._range.from;
} else { // to !== -1
this._contentLength = this._range.to - this._range.from + 1;
}
if (openNextRange) {
this._openSubRange();
return;
}
if (this._onContentLengthKnown) {
this._onContentLengthKnown(this._contentLength);
}
}
let delta = e.loaded - this._lastTimeLoaded;
this._lastTimeLoaded = e.loaded;
this._speedSampler.addBytes(delta);
}
_normalizeSpeed(input) {
let list = this._chunkSizeKBList;
let last = list.length - 1;
let mid = 0;
let lbound = 0;
let ubound = last;
if (input < list[0]) {
return list[0];
}
while (lbound <= ubound) {
mid = lbound + Math.floor((ubound - lbound) / 2);
if (mid === last || (input >= list[mid] && input < list[mid + 1])) {
return list[mid];
} else if (list[mid] < input) {
lbound = mid + 1;
} else {
ubound = mid - 1;
}
}
}
_onLoad(e) {
if (this._status === LoaderStatus.kError) {
// Ignore error response
return;
}
if (this._waitForTotalLength) {
this._waitForTotalLength = false;
return;
}
this._lastTimeLoaded = 0;
let KBps = this._speedSampler.lastSecondKBps;
if (KBps === 0) {
this._zeroSpeedChunkCount++;
if (this._zeroSpeedChunkCount >= 3) {
// Try get currentKBps after 3 chunks
KBps = this._speedSampler.currentKBps;
}
}
if (KBps !== 0) {
let normalized = this._normalizeSpeed(KBps);
if (this._currentSpeedNormalized !== normalized) {
this._currentSpeedNormalized = normalized;
this._currentChunkSizeKB = normalized;
}
}
let chunk = e.target.response;
let byteStart = this._range.from + this._receivedLength;
this._receivedLength += chunk.byteLength;
let reportComplete = false;
if (this._contentLength != null && this._receivedLength < this._contentLength) {
// continue load next chunk
this._openSubRange();
} else {
reportComplete = true;
}
// dispatch received chunk
if (this._onDataArrival) {
this._onDataArrival(chunk, byteStart, this._receivedLength);
}
if (reportComplete) {
this._status = LoaderStatus.kComplete;
if (this._onComplete) {
this._onComplete(this._range.from, this._range.from + this._receivedLength - 1);
}
}
}
_onXhrError(e) {
this._status = LoaderStatus.kError;
let type = 0;
let info = null;
if (this._contentLength && this._receivedLength > 0
&& this._receivedLength < this._contentLength) {
type = LoaderErrors.EARLY_EOF;
info = {code: -1, msg: 'RangeLoader meet Early-Eof'};
} else {
type = LoaderErrors.EXCEPTION;
info = {code: -1, msg: e.constructor.name + ' ' + e.type};
}
if (this._onError) {
this._onError(type, info);
} else {
throw new RuntimeException(info.msg);
}
}
}
export default RangeLoader;

View File

@ -0,0 +1,717 @@
/*
* 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 PlayerEvents from './player-events.js';
import Transmuxer from '../core/transmuxer.js';
import TransmuxingEvents from '../core/transmuxing-events.js';
import MSEController from '../core/mse-controller.js';
import MSEEvents from '../core/mse-events.js';
import {ErrorTypes, ErrorDetails} from './player-errors.js';
import {createDefaultConfig} from '../config.js';
import {InvalidArgumentException, IllegalStateException} from '../utils/exception.js';
class FlvPlayer {
constructor(mediaDataSource, config) {
this.TAG = 'FlvPlayer';
this._type = 'FlvPlayer';
this._emitter = new EventEmitter();
this._config = createDefaultConfig();
if (typeof config === 'object') {
Object.assign(this._config, config);
}
if (mediaDataSource.type.toLowerCase() !== 'flv') {
throw new InvalidArgumentException('FlvPlayer requires an flv MediaDataSource input!');
}
if (mediaDataSource.isLive === true) {
this._config.isLive = true;
}
this.e = {
onvLoadedMetadata: this._onvLoadedMetadata.bind(this),
onvSeeking: this._onvSeeking.bind(this),
onvCanPlay: this._onvCanPlay.bind(this),
onvStalled: this._onvStalled.bind(this),
onvProgress: this._onvProgress.bind(this)
};
if (self.performance && self.performance.now) {
this._now = self.performance.now.bind(self.performance);
} else {
this._now = Date.now;
}
this._pendingSeekTime = null; // in seconds
this._requestSetTime = false;
this._seekpointRecord = null;
this._progressChecker = null;
this._mediaDataSource = mediaDataSource;
this._mediaElement = null;
this._msectl = null;
this._transmuxer = null;
this._mseSourceOpened = false;
this._hasPendingLoad = false;
this._receivedCanPlay = false;
this._mediaInfo = null;
this._statisticsInfo = null;
let chromeNeedIDRFix = (Browser.chrome &&
(Browser.version.major < 50 ||
(Browser.version.major === 50 && Browser.version.build < 2661)));
this._alwaysSeekKeyframe = (chromeNeedIDRFix || Browser.msedge || Browser.msie) ? true : false;
if (this._alwaysSeekKeyframe) {
this._config.accurateSeek = false;
}
this._tempPendingSegments = {
audio: [],
video: [],
};
this._definitionRetryTimes = 0;
}
destroy() {
if (this._progressChecker != null) {
window.clearInterval(this._progressChecker);
this._progressChecker = null;
}
if (this._transmuxer) {
this.unload();
}
if (this._mediaElement) {
this.detachMediaElement();
}
this.e = null;
this._mediaDataSource = null;
this._emitter.removeAllListeners();
this._emitter = null;
}
on(event, listener) {
if (event === PlayerEvents.MEDIA_INFO) {
if (this._mediaInfo != null) {
Promise.resolve().then(() => {
this._emitter.emit(PlayerEvents.MEDIA_INFO, this.mediaInfo);
});
}
} else if (event === PlayerEvents.STATISTICS_INFO) {
if (this._statisticsInfo != null) {
Promise.resolve().then(() => {
this._emitter.emit(PlayerEvents.STATISTICS_INFO, this.statisticsInfo);
});
}
}
this._emitter.addListener(event, listener);
}
off(event, listener) {
this._emitter.removeListener(event, listener);
}
attachMediaElement(mediaElement) {
this._mediaElement = mediaElement;
mediaElement.addEventListener('loadedmetadata', this.e.onvLoadedMetadata);
mediaElement.addEventListener('seeking', this.e.onvSeeking);
mediaElement.addEventListener('canplay', this.e.onvCanPlay);
mediaElement.addEventListener('stalled', this.e.onvStalled);
mediaElement.addEventListener('progress', this.e.onvProgress);
this._msectl = new MSEController(this._config);
this._msectl.on(MSEEvents.UPDATE_END, this._onmseUpdateEnd.bind(this));
this._msectl.on(MSEEvents.BUFFER_FULL, this._onmseBufferFull.bind(this));
this._msectl.on(MSEEvents.SOURCE_OPEN, () => {
this._mseSourceOpened = true;
if (this._hasPendingLoad) {
this._hasPendingLoad = false;
this.load();
}
});
this._msectl.on(MSEEvents.ERROR, (info) => {
this._emitter.emit(PlayerEvents.ERROR,
ErrorTypes.MEDIA_ERROR,
ErrorDetails.MEDIA_MSE_ERROR,
info
);
});
this._msectl.attachMediaElement(mediaElement);
if (this._pendingSeekTime != null) {
try {
mediaElement.currentTime = this._pendingSeekTime;
this._pendingSeekTime = null;
} catch (e) {
// IE11 may throw InvalidStateError if readyState === 0
// We can defer set currentTime operation after loadedmetadata
}
}
}
detachMediaElement() {
if (this._mediaElement) {
this._msectl.detachMediaElement();
this._mediaElement.removeEventListener('loadedmetadata', this.e.onvLoadedMetadata);
this._mediaElement.removeEventListener('seeking', this.e.onvSeeking);
this._mediaElement.removeEventListener('canplay', this.e.onvCanPlay);
this._mediaElement.removeEventListener('stalled', this.e.onvStalled);
this._mediaElement.removeEventListener('progress', this.e.onvProgress);
this._mediaElement = null;
}
if (this._msectl) {
this._msectl.destroy();
this._msectl = null;
}
}
onDefinitionChange(url, expectTime = 3) {
setTimeout(() => {
if (!this.isDefinitionDataReady && this._definitionRetryTimes < 3) {
this._definitionRetryTimes += 1;
this.onDefinitionChange(url, expectTime);
} else if (this.isDefinitionDataReady) {
this._transmuxer.destroy();
Object.keys(this._tempPendingSegments).forEach(key => {
this._msectl._pendingSegments[key] = this._tempPendingSegments[key];
});
this._tempPendingSegments = {
audio: [],
video: [],
};
this._transmuxer = this._tempTransmuxer;
this._definitionRetryTimes = 0;
} else if (this._definitionRetryTimes >= 3) {
this._definitionRetryTimes = 0;
if (this._tempTransmuxer) {
this._tempTransmuxer.destroy();
this._tempTransmuxer = null;
this._emitter.emit(PlayerEvents.ERROR, ErrorTypes.NETWORK_ERROR, '', '清晰度切换失败!');
}
this._definitionRetryTimes = 0;
}
this._tempPendingSegments = {
audio: [],
video: [],
};
}, expectTime * 1000);
this._mediaDataSource.segments[0].url = url;
this._tempMds = Object.assign({}, this._mediaDataSource, { url });
this._tempTransmuxer = new Transmuxer(this._tempMds, this._config);
this._tempTransmuxer.on(TransmuxingEvents.INIT_SEGMENT, (type, is) => {
if (!this._config.isLive) {
this._tempPendingSegments[type] = [is];
} else {
this._msectl.doClearSourceBuffer();
this._msectl.appendInitSegment(is);
if (this._transmuxer !== this._tempTransmuxer) {
this._transmuxer.destroy();
}
this._transmuxer = this._tempTransmuxer;
}
});
this._tempTransmuxer.on(TransmuxingEvents.MEDIA_SEGMENT, (type, ms) => {
if (!this._config.isLive) {
if (this._transmuxer === this._tempTransmuxer) {
this._msectl.appendMediaSegment(ms);
} else {
this._tempPendingSegments[type] && this._tempPendingSegments[type].push(ms);
}
} else {
this._msectl.appendMediaSegment(ms);
}
// lazyLoad check
if (this._config.lazyLoad && !this._config.isLive) {
let currentTime = this._mediaElement.currentTime;
if (ms.info.endDts >= (currentTime + this._config.lazyLoadMaxDuration) * 1000) {
if (this._progressChecker == null) {
Log.v(this.TAG, 'Maximum buffering duration exceeded, suspend transmuxing task');
this._suspendTransmuxer();
}
}
}
});
this._tempTransmuxer.on(TransmuxingEvents.LOADING_COMPLETE, () => {
this._msectl.endOfStream();
this._emitter.emit(PlayerEvents.LOADING_COMPLETE);
});
this._tempTransmuxer.on(TransmuxingEvents.RECOVERED_EARLY_EOF, () => {
this._emitter.emit(PlayerEvents.RECOVERED_EARLY_EOF);
});
this._tempTransmuxer.on(TransmuxingEvents.IO_ERROR, (detail, info) => {
this._emitter.emit(PlayerEvents.ERROR, ErrorTypes.NETWORK_ERROR, detail, info);
});
this._tempTransmuxer.on(TransmuxingEvents.DEMUX_ERROR, (detail, info) => {
this._emitter.emit(PlayerEvents.ERROR, ErrorTypes.MEDIA_ERROR, detail, {code: -1, msg: info});
});
this._tempTransmuxer.on(TransmuxingEvents.MEDIA_INFO, (mediaInfo) => {
this._mediaInfo = mediaInfo;
this._tempTransmuxer.seek((this._mediaElement.currentTime + expectTime) * 1000);
this._emitter.emit(PlayerEvents.MEDIA_INFO, Object.assign({}, mediaInfo));
});
this._tempTransmuxer.on(TransmuxingEvents.STATISTICS_INFO, (statInfo) => {
this._statisticsInfo = this._fillStatisticsInfo(statInfo);
this._emitter.emit(PlayerEvents.STATISTICS_INFO, Object.assign({}, this._statisticsInfo));
});
this._tempTransmuxer.on(TransmuxingEvents.RECOMMEND_SEEKPOINT, (milliseconds) => {
if (this._transmuxer === this._tempTransmuxer && this._mediaElement && !this._config.accurateSeek) {
this._requestSetTime = true;
this._mediaElement.currentTime = milliseconds / 1000;
}
});
this._tempTransmuxer.open();
}
load() {
if (!this._mediaElement) {
throw new IllegalStateException('HTMLMediaElement must be attached before load()!');
}
if (this._transmuxer) {
throw new IllegalStateException('FlvPlayer.load() has been called, please call unload() first!');
}
if (this._hasPendingLoad) {
return;
}
if (this._config.deferLoadAfterSourceOpen && this._mseSourceOpened === false) {
this._hasPendingLoad = true;
return;
}
if (this._mediaElement.readyState > 0) {
this._requestSetTime = true;
// IE11 may throw InvalidStateError if readyState === 0
this._mediaElement.currentTime = 0;
}
this._transmuxer = new Transmuxer(this._mediaDataSource, this._config);
this._transmuxer.on(TransmuxingEvents.INIT_SEGMENT, (type, is) => {
this._msectl.appendInitSegment(is);
});
this._transmuxer.on(TransmuxingEvents.MEDIA_SEGMENT, (type, ms) => {
this._msectl.appendMediaSegment(ms);
// lazyLoad check
if (this._config.lazyLoad && !this._config.isLive) {
let currentTime = this._mediaElement.currentTime;
if (ms.info.endDts >= (currentTime + this._config.lazyLoadMaxDuration) * 1000) {
if (this._progressChecker == null) {
Log.v(this.TAG, 'Maximum buffering duration exceeded, suspend transmuxing task');
this._suspendTransmuxer();
}
}
}
});
this._transmuxer.on(TransmuxingEvents.LOADING_COMPLETE, () => {
this._msectl.endOfStream();
this._emitter.emit(PlayerEvents.LOADING_COMPLETE);
});
this._transmuxer.on(TransmuxingEvents.RECOVERED_EARLY_EOF, () => {
this._emitter.emit(PlayerEvents.RECOVERED_EARLY_EOF);
});
this._transmuxer.on(TransmuxingEvents.IO_ERROR, (detail, info) => {
this._emitter.emit(PlayerEvents.ERROR, ErrorTypes.NETWORK_ERROR, detail, info);
});
this._transmuxer.on(TransmuxingEvents.DEMUX_ERROR, (detail, info) => {
this._emitter.emit(PlayerEvents.ERROR, ErrorTypes.MEDIA_ERROR, detail, {code: -1, msg: info});
});
this._transmuxer.on(TransmuxingEvents.MEDIA_INFO, (mediaInfo) => {
this._mediaInfo = mediaInfo;
this._emitter.emit(PlayerEvents.MEDIA_INFO, Object.assign({}, mediaInfo));
});
this._transmuxer.on(TransmuxingEvents.STATISTICS_INFO, (statInfo) => {
this._statisticsInfo = this._fillStatisticsInfo(statInfo);
this._emitter.emit(PlayerEvents.STATISTICS_INFO, Object.assign({}, this._statisticsInfo));
});
this._transmuxer.on(TransmuxingEvents.RECOMMEND_SEEKPOINT, (milliseconds) => {
if (this._mediaElement && !this._config.accurateSeek) {
this._requestSetTime = true;
this._mediaElement.currentTime = milliseconds / 1000;
}
});
this._transmuxer.open();
}
unload() {
if (this._mediaElement) {
this._mediaElement.pause();
}
if (this._msectl) {
this._msectl.seek(0);
}
if (this._transmuxer) {
this._transmuxer.close();
this._transmuxer.destroy();
this._transmuxer = null;
}
}
play() {
return this._mediaElement.play();
}
pause() {
this._mediaElement.pause();
}
get type() {
return this._type;
}
get buffered() {
return this._mediaElement.buffered;
}
get duration() {
return this._mediaElement.duration;
}
get volume() {
return this._mediaElement.volume;
}
set volume(value) {
this._mediaElement.volume = value;
}
get muted() {
return this._mediaElement.muted;
}
set muted(muted) {
this._mediaElement.muted = muted;
}
get currentTime() {
if (this._mediaElement) {
return this._mediaElement.currentTime;
}
return 0;
}
set currentTime(seconds) {
if (this._mediaElement) {
this._internalSeek(seconds);
} else {
this._pendingSeekTime = seconds;
}
}
get mediaInfo() {
return Object.assign({}, this._mediaInfo);
}
get statisticsInfo() {
if (this._statisticsInfo == null) {
this._statisticsInfo = {};
}
this._statisticsInfo = this._fillStatisticsInfo(this._statisticsInfo);
return Object.assign({}, this._statisticsInfo);
}
_fillStatisticsInfo(statInfo) {
statInfo.playerType = this._type;
if (!(this._mediaElement instanceof HTMLVideoElement)) {
return statInfo;
}
let hasQualityInfo = true;
let decoded = 0;
let dropped = 0;
if (this._mediaElement.getVideoPlaybackQuality) {
let quality = this._mediaElement.getVideoPlaybackQuality();
decoded = quality.totalVideoFrames;
dropped = quality.droppedVideoFrames;
} else if (this._mediaElement.webkitDecodedFrameCount != undefined) {
decoded = this._mediaElement.webkitDecodedFrameCount;
dropped = this._mediaElement.webkitDroppedFrameCount;
} else {
hasQualityInfo = false;
}
if (hasQualityInfo) {
statInfo.decodedFrames = decoded;
statInfo.droppedFrames = dropped;
}
return statInfo;
}
_onmseUpdateEnd() {
if (!this._config.lazyLoad || this._config.isLive) {
return;
}
let buffered = this._mediaElement.buffered;
let currentTime = this._mediaElement.currentTime;
let currentRangeStart = 0;
let currentRangeEnd = 0;
for (let i = 0; i < buffered.length; i++) {
let start = buffered.start(i);
let end = buffered.end(i);
if (start <= currentTime && currentTime < end) {
currentRangeStart = start;
currentRangeEnd = end;
break;
}
}
if (currentRangeEnd >= currentTime + this._config.lazyLoadMaxDuration && this._progressChecker == null) {
Log.v(this.TAG, 'Maximum buffering duration exceeded, suspend transmuxing task');
this._suspendTransmuxer();
}
}
_onmseBufferFull() {
Log.v(this.TAG, 'MSE SourceBuffer is full, suspend transmuxing task');
if (this._progressChecker == null) {
this._suspendTransmuxer();
}
}
_suspendTransmuxer() {
if (this._transmuxer) {
this._transmuxer.pause();
if (this._progressChecker == null) {
this._progressChecker = window.setInterval(this._checkProgressAndResume.bind(this), 1000);
}
}
}
_checkProgressAndResume() {
let currentTime = this._mediaElement.currentTime;
let buffered = this._mediaElement.buffered;
let needResume = false;
for (let i = 0; i < buffered.length; i++) {
let from = buffered.start(i);
let to = buffered.end(i);
if (currentTime >= from && currentTime < to) {
if (currentTime >= to - this._config.lazyLoadRecoverDuration) {
needResume = true;
}
break;
}
}
if (needResume) {
window.clearInterval(this._progressChecker);
this._progressChecker = null;
if (needResume) {
Log.v(this.TAG, 'Continue loading from paused position');
this._transmuxer.resume();
}
}
}
_isTimepointBuffered(seconds) {
let buffered = this._mediaElement.buffered;
for (let i = 0; i < buffered.length; i++) {
let from = buffered.start(i);
let to = buffered.end(i);
if (seconds >= from && seconds < to) {
return true;
}
}
return false;
}
_internalSeek(seconds) {
let directSeek = this._isTimepointBuffered(seconds);
let directSeekBegin = false;
let directSeekBeginTime = 0;
if (seconds < 1.0 && this._mediaElement.buffered.length > 0) {
let videoBeginTime = this._mediaElement.buffered.start(0);
if ((videoBeginTime < 1.0 && seconds < videoBeginTime) || Browser.safari) {
directSeekBegin = true;
// also workaround for Safari: Seek to 0 may cause video stuck, use 0.1 to avoid
directSeekBeginTime = Browser.safari ? 0.1 : videoBeginTime;
}
}
if (directSeekBegin) { // seek to video begin, set currentTime directly if beginPTS buffered
this._requestSetTime = true;
this._mediaElement.currentTime = directSeekBeginTime;
} else if (directSeek) { // buffered position
if (!this._alwaysSeekKeyframe) {
this._requestSetTime = true;
this._mediaElement.currentTime = seconds;
} else {
let idr = this._msectl.getNearestKeyframe(Math.floor(seconds * 1000));
this._requestSetTime = true;
if (idr != null) {
this._mediaElement.currentTime = idr.dts / 1000;
} else {
this._mediaElement.currentTime = seconds;
}
}
if (this._progressChecker != null) {
this._checkProgressAndResume();
}
} else {
if (this._progressChecker != null) {
window.clearInterval(this._progressChecker);
this._progressChecker = null;
}
this._msectl.seek(seconds);
this._transmuxer.seek(Math.floor(seconds * 1000)); // in milliseconds
// no need to set mediaElement.currentTime if non-accurateSeek,
// just wait for the recommend_seekpoint callback
if (this._config.accurateSeek) {
this._requestSetTime = true;
this._mediaElement.currentTime = seconds;
}
}
}
_checkAndApplyUnbufferedSeekpoint() {
if (this._seekpointRecord) {
if (this._seekpointRecord.recordTime <= this._now() - 100) {
let target = this._mediaElement.currentTime;
this._seekpointRecord = null;
if (!this._isTimepointBuffered(target)) {
if (this._progressChecker != null) {
window.clearTimeout(this._progressChecker);
this._progressChecker = null;
}
// .currentTime is consists with .buffered timestamp
// Chrome/Edge use DTS, while FireFox/Safari use PTS
this._msectl.seek(target);
this._transmuxer.seek(Math.floor(target * 1000));
// set currentTime if accurateSeek, or wait for recommend_seekpoint callback
if (this._config.accurateSeek) {
this._requestSetTime = true;
this._mediaElement.currentTime = target;
}
}
} else {
window.setTimeout(this._checkAndApplyUnbufferedSeekpoint.bind(this), 50);
}
}
}
_checkAndResumeStuckPlayback(stalled) {
let media = this._mediaElement;
if (stalled || !this._receivedCanPlay || media.readyState < 2) { // HAVE_CURRENT_DATA
let buffered = media.buffered;
if (buffered.length > 0 && media.currentTime < buffered.start(0)) {
Log.w(this.TAG, `Playback seems stuck at ${media.currentTime}, seek to ${buffered.start(0)}`);
this._requestSetTime = true;
this._mediaElement.currentTime = buffered.start(0);
this._mediaElement.removeEventListener('progress', this.e.onvProgress);
}
} else {
// Playback didn't stuck, remove progress event listener
this._mediaElement.removeEventListener('progress', this.e.onvProgress);
}
}
_onvLoadedMetadata(e) {
if (this._pendingSeekTime != null) {
this._mediaElement.currentTime = this._pendingSeekTime;
this._pendingSeekTime = null;
}
}
_onvSeeking(e) { // handle seeking request from browser's progress bar
let target = this._mediaElement.currentTime;
let buffered = this._mediaElement.buffered;
if (this._requestSetTime) {
this._requestSetTime = false;
return;
}
if (target < 1.0 && buffered.length > 0) {
// seek to video begin, set currentTime directly if beginPTS buffered
let videoBeginTime = buffered.start(0);
if ((videoBeginTime < 1.0 && target < videoBeginTime) || Browser.safari) {
this._requestSetTime = true;
// also workaround for Safari: Seek to 0 may cause video stuck, use 0.1 to avoid
this._mediaElement.currentTime = Browser.safari ? 0.1 : videoBeginTime;
return;
}
}
if (this._isTimepointBuffered(target)) {
if (this._alwaysSeekKeyframe) {
let idr = this._msectl.getNearestKeyframe(Math.floor(target * 1000));
if (idr != null) {
this._requestSetTime = true;
this._mediaElement.currentTime = idr.dts / 1000;
}
}
if (this._progressChecker != null) {
this._checkProgressAndResume();
}
return;
}
this._seekpointRecord = {
seekPoint: target,
recordTime: this._now()
};
window.setTimeout(this._checkAndApplyUnbufferedSeekpoint.bind(this), 50);
}
_onvCanPlay(e) {
this._receivedCanPlay = true;
this._mediaElement.removeEventListener('canplay', this.e.onvCanPlay);
}
_onvStalled(e) {
this._checkAndResumeStuckPlayback(true);
}
_onvProgress(e) {
this._checkAndResumeStuckPlayback();
}
get isDefinitionDataReady() {
const minSegmentLen = this._config.isLive ? 1 : 2;
return Object.keys(this._tempPendingSegments).every((key) => this._tempPendingSegments[key].length >= minSegmentLen);
}
}
export default FlvPlayer;

View File

@ -0,0 +1,256 @@
/*
* 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 PlayerEvents from './player-events.js';
import {createDefaultConfig} from '../config.js';
import {InvalidArgumentException, IllegalStateException} from '../utils/exception.js';
// Player wrapper for browser's native player (HTMLVideoElement) without MediaSource src.
class NativePlayer {
constructor(mediaDataSource, config) {
this.TAG = 'NativePlayer';
this._type = 'NativePlayer';
this._emitter = new EventEmitter();
this._config = createDefaultConfig();
if (typeof config === 'object') {
Object.assign(this._config, config);
}
if (mediaDataSource.type.toLowerCase() === 'flv') {
throw new InvalidArgumentException('NativePlayer does\'t support flv MediaDataSource input!');
}
if (mediaDataSource.hasOwnProperty('segments')) {
throw new InvalidArgumentException(`NativePlayer(${mediaDataSource.type}) doesn't support multipart playback!`);
}
this.e = {
onvLoadedMetadata: this._onvLoadedMetadata.bind(this)
};
this._pendingSeekTime = null;
this._statisticsReporter = null;
this._mediaDataSource = mediaDataSource;
this._mediaElement = null;
}
destroy() {
if (this._mediaElement) {
this.unload();
this.detachMediaElement();
}
this.e = null;
this._mediaDataSource = null;
this._emitter.removeAllListeners();
this._emitter = null;
}
on(event, listener) {
if (event === PlayerEvents.MEDIA_INFO) {
if (this._mediaElement != null && this._mediaElement.readyState !== 0) { // HAVE_NOTHING
Promise.resolve().then(() => {
this._emitter.emit(PlayerEvents.MEDIA_INFO, this.mediaInfo);
});
}
} else if (event === PlayerEvents.STATISTICS_INFO) {
if (this._mediaElement != null && this._mediaElement.readyState !== 0) {
Promise.resolve().then(() => {
this._emitter.emit(PlayerEvents.STATISTICS_INFO, this.statisticsInfo);
});
}
}
this._emitter.addListener(event, listener);
}
off(event, listener) {
this._emitter.removeListener(event, listener);
}
attachMediaElement(mediaElement) {
this._mediaElement = mediaElement;
mediaElement.addEventListener('loadedmetadata', this.e.onvLoadedMetadata);
if (this._pendingSeekTime != null) {
try {
mediaElement.currentTime = this._pendingSeekTime;
this._pendingSeekTime = null;
} catch (e) {
// IE11 may throw InvalidStateError if readyState === 0
// Defer set currentTime operation after loadedmetadata
}
}
}
detachMediaElement() {
if (this._mediaElement) {
this._mediaElement.src = '';
this._mediaElement.removeAttribute('src');
this._mediaElement.removeEventListener('loadedmetadata', this.e.onvLoadedMetadata);
this._mediaElement = null;
}
if (this._statisticsReporter != null) {
window.clearInterval(this._statisticsReporter);
this._statisticsReporter = null;
}
}
load() {
if (!this._mediaElement) {
throw new IllegalStateException('HTMLMediaElement must be attached before load()!');
}
this._mediaElement.src = this._mediaDataSource.url;
if (this._mediaElement.readyState > 0) {
this._mediaElement.currentTime = 0;
}
this._mediaElement.preload = 'auto';
this._mediaElement.load();
this._statisticsReporter = window.setInterval(
this._reportStatisticsInfo.bind(this),
this._config.statisticsInfoReportInterval);
}
unload() {
if (this._mediaElement) {
this._mediaElement.src = '';
this._mediaElement.removeAttribute('src');
}
if (this._statisticsReporter != null) {
window.clearInterval(this._statisticsReporter);
this._statisticsReporter = null;
}
}
play() {
return this._mediaElement.play();
}
pause() {
this._mediaElement.pause();
}
get type() {
return this._type;
}
get buffered() {
return this._mediaElement.buffered;
}
get duration() {
return this._mediaElement.duration;
}
get volume() {
return this._mediaElement.volume;
}
set volume(value) {
this._mediaElement.volume = value;
}
get muted() {
return this._mediaElement.muted;
}
set muted(muted) {
this._mediaElement.muted = muted;
}
get currentTime() {
if (this._mediaElement) {
return this._mediaElement.currentTime;
}
return 0;
}
set currentTime(seconds) {
if (this._mediaElement) {
this._mediaElement.currentTime = seconds;
} else {
this._pendingSeekTime = seconds;
}
}
get mediaInfo() {
let mediaPrefix = (this._mediaElement instanceof HTMLAudioElement) ? 'audio/' : 'video/';
let info = {
mimeType: mediaPrefix + this._mediaDataSource.type
};
if (this._mediaElement) {
info.duration = Math.floor(this._mediaElement.duration * 1000);
if (this._mediaElement instanceof HTMLVideoElement) {
info.width = this._mediaElement.videoWidth;
info.height = this._mediaElement.videoHeight;
}
}
return info;
}
get statisticsInfo() {
let info = {
playerType: this._type,
url: this._mediaDataSource.url
};
if (!(this._mediaElement instanceof HTMLVideoElement)) {
return info;
}
let hasQualityInfo = true;
let decoded = 0;
let dropped = 0;
if (this._mediaElement.getVideoPlaybackQuality) {
let quality = this._mediaElement.getVideoPlaybackQuality();
decoded = quality.totalVideoFrames;
dropped = quality.droppedVideoFrames;
} else if (this._mediaElement.webkitDecodedFrameCount != undefined) {
decoded = this._mediaElement.webkitDecodedFrameCount;
dropped = this._mediaElement.webkitDroppedFrameCount;
} else {
hasQualityInfo = false;
}
if (hasQualityInfo) {
info.decodedFrames = decoded;
info.droppedFrames = dropped;
}
return info;
}
_onvLoadedMetadata(e) {
if (this._pendingSeekTime != null) {
this._mediaElement.currentTime = this._pendingSeekTime;
this._pendingSeekTime = null;
}
this._emitter.emit(PlayerEvents.MEDIA_INFO, this.mediaInfo);
}
_reportStatisticsInfo() {
this._emitter.emit(PlayerEvents.STATISTICS_INFO, this.statisticsInfo);
}
}
export default NativePlayer;

View File

@ -0,0 +1,39 @@
/*
* 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 {LoaderErrors} from '../io/loader.js';
import DemuxErrors from '../demux/demux-errors.js';
export const ErrorTypes = {
NETWORK_ERROR: 'NetworkError',
MEDIA_ERROR: 'MediaError',
OTHER_ERROR: 'OtherError'
};
export const ErrorDetails = {
NETWORK_EXCEPTION: LoaderErrors.EXCEPTION,
NETWORK_STATUS_CODE_INVALID: LoaderErrors.HTTP_STATUS_CODE_INVALID,
NETWORK_TIMEOUT: LoaderErrors.CONNECTING_TIMEOUT,
NETWORK_UNRECOVERABLE_EARLY_EOF: LoaderErrors.UNRECOVERABLE_EARLY_EOF,
MEDIA_MSE_ERROR: 'MediaMSEError',
MEDIA_FORMAT_ERROR: DemuxErrors.FORMAT_ERROR,
MEDIA_FORMAT_UNSUPPORTED: DemuxErrors.FORMAT_UNSUPPORTED,
MEDIA_CODEC_UNSUPPORTED: DemuxErrors.CODEC_UNSUPPORTED
};

View File

@ -0,0 +1,27 @@
/*
* 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 PlayerEvents = {
ERROR: 'error',
LOADING_COMPLETE: 'loading_complete',
RECOVERED_EARLY_EOF: 'recovered_early_eof',
MEDIA_INFO: 'media_info',
STATISTICS_INFO: 'statistics_info'
};
export default PlayerEvents;

View File

@ -0,0 +1,56 @@
/*
* Copyright (C) 2016 Bilibili. All Rights Reserved.
*
* This file is modified from dailymotion's hls.js library (hls.js/src/helper/aac.js)
* @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 AAC {
static getSilentFrame(codec, channelCount) {
if (codec === 'mp4a.40.2') {
// handle LC-AAC
if (channelCount === 1) {
return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x23, 0x80]);
} else if (channelCount === 2) {
return new Uint8Array([0x21, 0x00, 0x49, 0x90, 0x02, 0x19, 0x00, 0x23, 0x80]);
} else if (channelCount === 3) {
return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x20, 0x84, 0x01, 0x26, 0x40, 0x08, 0x64, 0x00, 0x8e]);
} else if (channelCount === 4) {
return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x20, 0x84, 0x01, 0x26, 0x40, 0x08, 0x64, 0x00, 0x80, 0x2c, 0x80, 0x08, 0x02, 0x38]);
} else if (channelCount === 5) {
return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x20, 0x84, 0x01, 0x26, 0x40, 0x08, 0x64, 0x00, 0x82, 0x30, 0x04, 0x99, 0x00, 0x21, 0x90, 0x02, 0x38]);
} else if (channelCount === 6) {
return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x20, 0x84, 0x01, 0x26, 0x40, 0x08, 0x64, 0x00, 0x82, 0x30, 0x04, 0x99, 0x00, 0x21, 0x90, 0x02, 0x00, 0xb2, 0x00, 0x20, 0x08, 0xe0]);
}
} else {
// handle HE-AAC (mp4a.40.5 / mp4a.40.29)
if (channelCount === 1) {
// ffmpeg -y -f lavfi -i "aevalsrc=0:d=0.05" -c:a libfdk_aac -profile:a aac_he -b:a 4k output.aac && hexdump -v -e '16/1 "0x%x," "\n"' -v output.aac
return new Uint8Array([0x1, 0x40, 0x22, 0x80, 0xa3, 0x4e, 0xe6, 0x80, 0xba, 0x8, 0x0, 0x0, 0x0, 0x1c, 0x6, 0xf1, 0xc1, 0xa, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5e]);
} else if (channelCount === 2) {
// ffmpeg -y -f lavfi -i "aevalsrc=0|0:d=0.05" -c:a libfdk_aac -profile:a aac_he_v2 -b:a 4k output.aac && hexdump -v -e '16/1 "0x%x," "\n"' -v output.aac
return new Uint8Array([0x1, 0x40, 0x22, 0x80, 0xa3, 0x5e, 0xe6, 0x80, 0xba, 0x8, 0x0, 0x0, 0x0, 0x0, 0x95, 0x0, 0x6, 0xf1, 0xa1, 0xa, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5e]);
} else if (channelCount === 3) {
// ffmpeg -y -f lavfi -i "aevalsrc=0|0|0:d=0.05" -c:a libfdk_aac -profile:a aac_he_v2 -b:a 4k output.aac && hexdump -v -e '16/1 "0x%x," "\n"' -v output.aac
return new Uint8Array([0x1, 0x40, 0x22, 0x80, 0xa3, 0x5e, 0xe6, 0x80, 0xba, 0x8, 0x0, 0x0, 0x0, 0x0, 0x95, 0x0, 0x6, 0xf1, 0xa1, 0xa, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5e]);
}
}
return null;
}
}
export default AAC;

View File

@ -0,0 +1,569 @@
/*
* Copyright (C) 2016 Bilibili. All Rights Reserved.
*
* This file is derived from dailymotion's hls.js library (hls.js/src/remux/mp4-generator.js)
* @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.
*/
// MP4 boxes generator for ISO BMFF (ISO Base Media File Format, defined in ISO/IEC 14496-12)
class MP4 {
static init() {
MP4.types = {
avc1: [], avcC: [], btrt: [], dinf: [],
dref: [], esds: [], ftyp: [], hdlr: [],
mdat: [], mdhd: [], mdia: [], mfhd: [],
minf: [], moof: [], moov: [], mp4a: [],
mvex: [], mvhd: [], sdtp: [], stbl: [],
stco: [], stsc: [], stsd: [], stsz: [],
stts: [], tfdt: [], tfhd: [], traf: [],
trak: [], trun: [], trex: [], tkhd: [],
vmhd: [], smhd: [], '.mp3': []
};
for (let name in MP4.types) {
if (MP4.types.hasOwnProperty(name)) {
MP4.types[name] = [
name.charCodeAt(0),
name.charCodeAt(1),
name.charCodeAt(2),
name.charCodeAt(3)
];
}
}
let constants = MP4.constants = {};
constants.FTYP = new Uint8Array([
0x69, 0x73, 0x6F, 0x6D, // major_brand: isom
0x0, 0x0, 0x0, 0x1, // minor_version: 0x01
0x69, 0x73, 0x6F, 0x6D, // isom
0x61, 0x76, 0x63, 0x31 // avc1
]);
constants.STSD_PREFIX = new Uint8Array([
0x00, 0x00, 0x00, 0x00, // version(0) + flags
0x00, 0x00, 0x00, 0x01 // entry_count
]);
constants.STTS = new Uint8Array([
0x00, 0x00, 0x00, 0x00, // version(0) + flags
0x00, 0x00, 0x00, 0x00 // entry_count
]);
constants.STSC = constants.STCO = constants.STTS;
constants.STSZ = new Uint8Array([
0x00, 0x00, 0x00, 0x00, // version(0) + flags
0x00, 0x00, 0x00, 0x00, // sample_size
0x00, 0x00, 0x00, 0x00 // sample_count
]);
constants.HDLR_VIDEO = new Uint8Array([
0x00, 0x00, 0x00, 0x00, // version(0) + flags
0x00, 0x00, 0x00, 0x00, // pre_defined
0x76, 0x69, 0x64, 0x65, // handler_type: 'vide'
0x00, 0x00, 0x00, 0x00, // reserved: 3 * 4 bytes
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x56, 0x69, 0x64, 0x65,
0x6F, 0x48, 0x61, 0x6E,
0x64, 0x6C, 0x65, 0x72, 0x00 // name: VideoHandler
]);
constants.HDLR_AUDIO = new Uint8Array([
0x00, 0x00, 0x00, 0x00, // version(0) + flags
0x00, 0x00, 0x00, 0x00, // pre_defined
0x73, 0x6F, 0x75, 0x6E, // handler_type: 'soun'
0x00, 0x00, 0x00, 0x00, // reserved: 3 * 4 bytes
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x53, 0x6F, 0x75, 0x6E,
0x64, 0x48, 0x61, 0x6E,
0x64, 0x6C, 0x65, 0x72, 0x00 // name: SoundHandler
]);
constants.DREF = new Uint8Array([
0x00, 0x00, 0x00, 0x00, // version(0) + flags
0x00, 0x00, 0x00, 0x01, // entry_count
0x00, 0x00, 0x00, 0x0C, // entry_size
0x75, 0x72, 0x6C, 0x20, // type 'url '
0x00, 0x00, 0x00, 0x01 // version(0) + flags
]);
// Sound media header
constants.SMHD = new Uint8Array([
0x00, 0x00, 0x00, 0x00, // version(0) + flags
0x00, 0x00, 0x00, 0x00 // balance(2) + reserved(2)
]);
// video media header
constants.VMHD = new Uint8Array([
0x00, 0x00, 0x00, 0x01, // version(0) + flags
0x00, 0x00, // graphicsmode: 2 bytes
0x00, 0x00, 0x00, 0x00, // opcolor: 3 * 2 bytes
0x00, 0x00
]);
}
// Generate a box
static box(type) {
let size = 8;
let result = null;
let datas = Array.prototype.slice.call(arguments, 1);
let arrayCount = datas.length;
for (let i = 0; i < arrayCount; i++) {
size += datas[i].byteLength;
}
result = new Uint8Array(size);
result[0] = (size >>> 24) & 0xFF; // size
result[1] = (size >>> 16) & 0xFF;
result[2] = (size >>> 8) & 0xFF;
result[3] = (size) & 0xFF;
result.set(type, 4); // type
let offset = 8;
for (let i = 0; i < arrayCount; i++) { // data body
result.set(datas[i], offset);
offset += datas[i].byteLength;
}
return result;
}
// emit ftyp & moov
static generateInitSegment(meta) {
let ftyp = MP4.box(MP4.types.ftyp, MP4.constants.FTYP);
let moov = MP4.moov(meta);
let result = new Uint8Array(ftyp.byteLength + moov.byteLength);
result.set(ftyp, 0);
result.set(moov, ftyp.byteLength);
return result;
}
// Movie metadata box
static moov(meta) {
let mvhd = MP4.mvhd(meta.timescale, meta.duration);
let trak = MP4.trak(meta);
let mvex = MP4.mvex(meta);
return MP4.box(MP4.types.moov, mvhd, trak, mvex);
}
// Movie header box
static mvhd(timescale, duration) {
return MP4.box(MP4.types.mvhd, new Uint8Array([
0x00, 0x00, 0x00, 0x00, // version(0) + flags
0x00, 0x00, 0x00, 0x00, // creation_time
0x00, 0x00, 0x00, 0x00, // modification_time
(timescale >>> 24) & 0xFF, // timescale: 4 bytes
(timescale >>> 16) & 0xFF,
(timescale >>> 8) & 0xFF,
(timescale) & 0xFF,
(duration >>> 24) & 0xFF, // duration: 4 bytes
(duration >>> 16) & 0xFF,
(duration >>> 8) & 0xFF,
(duration) & 0xFF,
0x00, 0x01, 0x00, 0x00, // Preferred rate: 1.0
0x01, 0x00, 0x00, 0x00, // PreferredVolume(1.0, 2bytes) + reserved(2bytes)
0x00, 0x00, 0x00, 0x00, // reserved: 4 + 4 bytes
0x00, 0x00, 0x00, 0x00,
0x00, 0x01, 0x00, 0x00, // ----begin composition matrix----
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x01, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x40, 0x00, 0x00, 0x00, // ----end composition matrix----
0x00, 0x00, 0x00, 0x00, // ----begin pre_defined 6 * 4 bytes----
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, // ----end pre_defined 6 * 4 bytes----
0xFF, 0xFF, 0xFF, 0xFF // next_track_ID
]));
}
// Track box
static trak(meta) {
return MP4.box(MP4.types.trak, MP4.tkhd(meta), MP4.mdia(meta));
}
// Track header box
static tkhd(meta) {
let trackId = meta.id, duration = meta.duration;
let width = meta.presentWidth, height = meta.presentHeight;
return MP4.box(MP4.types.tkhd, new Uint8Array([
0x00, 0x00, 0x00, 0x07, // version(0) + flags
0x00, 0x00, 0x00, 0x00, // creation_time
0x00, 0x00, 0x00, 0x00, // modification_time
(trackId >>> 24) & 0xFF, // track_ID: 4 bytes
(trackId >>> 16) & 0xFF,
(trackId >>> 8) & 0xFF,
(trackId) & 0xFF,
0x00, 0x00, 0x00, 0x00, // reserved: 4 bytes
(duration >>> 24) & 0xFF, // duration: 4 bytes
(duration >>> 16) & 0xFF,
(duration >>> 8) & 0xFF,
(duration) & 0xFF,
0x00, 0x00, 0x00, 0x00, // reserved: 2 * 4 bytes
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, // layer(2bytes) + alternate_group(2bytes)
0x00, 0x00, 0x00, 0x00, // volume(2bytes) + reserved(2bytes)
0x00, 0x01, 0x00, 0x00, // ----begin composition matrix----
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x01, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x40, 0x00, 0x00, 0x00, // ----end composition matrix----
(width >>> 8) & 0xFF, // width and height
(width) & 0xFF,
0x00, 0x00,
(height >>> 8) & 0xFF,
(height) & 0xFF,
0x00, 0x00
]));
}
// Media Box
static mdia(meta) {
return MP4.box(MP4.types.mdia, MP4.mdhd(meta), MP4.hdlr(meta), MP4.minf(meta));
}
// Media header box
static mdhd(meta) {
let timescale = meta.timescale;
let duration = meta.duration;
return MP4.box(MP4.types.mdhd, new Uint8Array([
0x00, 0x00, 0x00, 0x00, // version(0) + flags
0x00, 0x00, 0x00, 0x00, // creation_time
0x00, 0x00, 0x00, 0x00, // modification_time
(timescale >>> 24) & 0xFF, // timescale: 4 bytes
(timescale >>> 16) & 0xFF,
(timescale >>> 8) & 0xFF,
(timescale) & 0xFF,
(duration >>> 24) & 0xFF, // duration: 4 bytes
(duration >>> 16) & 0xFF,
(duration >>> 8) & 0xFF,
(duration) & 0xFF,
0x55, 0xC4, // language: und (undetermined)
0x00, 0x00 // pre_defined = 0
]));
}
// Media handler reference box
static hdlr(meta) {
let data = null;
if (meta.type === 'audio') {
data = MP4.constants.HDLR_AUDIO;
} else {
data = MP4.constants.HDLR_VIDEO;
}
return MP4.box(MP4.types.hdlr, data);
}
// Media infomation box
static minf(meta) {
let xmhd = null;
if (meta.type === 'audio') {
xmhd = MP4.box(MP4.types.smhd, MP4.constants.SMHD);
} else {
xmhd = MP4.box(MP4.types.vmhd, MP4.constants.VMHD);
}
return MP4.box(MP4.types.minf, xmhd, MP4.dinf(), MP4.stbl(meta));
}
// Data infomation box
static dinf() {
let result = MP4.box(MP4.types.dinf,
MP4.box(MP4.types.dref, MP4.constants.DREF)
);
return result;
}
// Sample table box
static stbl(meta) {
let result = MP4.box(MP4.types.stbl, // type: stbl
MP4.stsd(meta), // Sample Description Table
MP4.box(MP4.types.stts, MP4.constants.STTS), // Time-To-Sample
MP4.box(MP4.types.stsc, MP4.constants.STSC), // Sample-To-Chunk
MP4.box(MP4.types.stsz, MP4.constants.STSZ), // Sample size
MP4.box(MP4.types.stco, MP4.constants.STCO) // Chunk offset
);
return result;
}
// Sample description box
static stsd(meta) {
if (meta.type === 'audio') {
if (meta.codec === 'mp3') {
return MP4.box(MP4.types.stsd, MP4.constants.STSD_PREFIX, MP4.mp3(meta));
}
// else: aac -> mp4a
return MP4.box(MP4.types.stsd, MP4.constants.STSD_PREFIX, MP4.mp4a(meta));
} else {
return MP4.box(MP4.types.stsd, MP4.constants.STSD_PREFIX, MP4.avc1(meta));
}
}
static mp3(meta) {
let channelCount = meta.channelCount;
let sampleRate = meta.audioSampleRate;
let data = new Uint8Array([
0x00, 0x00, 0x00, 0x00, // reserved(4)
0x00, 0x00, 0x00, 0x01, // reserved(2) + data_reference_index(2)
0x00, 0x00, 0x00, 0x00, // reserved: 2 * 4 bytes
0x00, 0x00, 0x00, 0x00,
0x00, channelCount, // channelCount(2)
0x00, 0x10, // sampleSize(2)
0x00, 0x00, 0x00, 0x00, // reserved(4)
(sampleRate >>> 8) & 0xFF, // Audio sample rate
(sampleRate) & 0xFF,
0x00, 0x00
]);
return MP4.box(MP4.types['.mp3'], data);
}
static mp4a(meta) {
let channelCount = meta.channelCount;
let sampleRate = meta.audioSampleRate;
let data = new Uint8Array([
0x00, 0x00, 0x00, 0x00, // reserved(4)
0x00, 0x00, 0x00, 0x01, // reserved(2) + data_reference_index(2)
0x00, 0x00, 0x00, 0x00, // reserved: 2 * 4 bytes
0x00, 0x00, 0x00, 0x00,
0x00, channelCount, // channelCount(2)
0x00, 0x10, // sampleSize(2)
0x00, 0x00, 0x00, 0x00, // reserved(4)
(sampleRate >>> 8) & 0xFF, // Audio sample rate
(sampleRate) & 0xFF,
0x00, 0x00
]);
return MP4.box(MP4.types.mp4a, data, MP4.esds(meta));
}
static esds(meta) {
let config = meta.config || [];
let configSize = config.length;
let data = new Uint8Array([
0x00, 0x00, 0x00, 0x00, // version 0 + flags
0x03, // descriptor_type
0x17 + configSize, // length3
0x00, 0x01, // es_id
0x00, // stream_priority
0x04, // descriptor_type
0x0F + configSize, // length
0x40, // codec: mpeg4_audio
0x15, // stream_type: Audio
0x00, 0x00, 0x00, // buffer_size
0x00, 0x00, 0x00, 0x00, // maxBitrate
0x00, 0x00, 0x00, 0x00, // avgBitrate
0x05 // descriptor_type
].concat([
configSize
]).concat(
config
).concat([
0x06, 0x01, 0x02 // GASpecificConfig
]));
return MP4.box(MP4.types.esds, data);
}
static avc1(meta) {
let avcc = meta.avcc;
let width = meta.codecWidth, height = meta.codecHeight;
let data = new Uint8Array([
0x00, 0x00, 0x00, 0x00, // reserved(4)
0x00, 0x00, 0x00, 0x01, // reserved(2) + data_reference_index(2)
0x00, 0x00, 0x00, 0x00, // pre_defined(2) + reserved(2)
0x00, 0x00, 0x00, 0x00, // pre_defined: 3 * 4 bytes
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
(width >>> 8) & 0xFF, // width: 2 bytes
(width) & 0xFF,
(height >>> 8) & 0xFF, // height: 2 bytes
(height) & 0xFF,
0x00, 0x48, 0x00, 0x00, // horizresolution: 4 bytes
0x00, 0x48, 0x00, 0x00, // vertresolution: 4 bytes
0x00, 0x00, 0x00, 0x00, // reserved: 4 bytes
0x00, 0x01, // frame_count
0x0A, // strlen
0x78, 0x71, 0x71, 0x2F, // compressorname: 32 bytes
0x66, 0x6C, 0x76, 0x2E,
0x6A, 0x73, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00,
0x00, 0x18, // depth
0xFF, 0xFF // pre_defined = -1
]);
return MP4.box(MP4.types.avc1, data, MP4.box(MP4.types.avcC, avcc));
}
// Movie Extends box
static mvex(meta) {
return MP4.box(MP4.types.mvex, MP4.trex(meta));
}
// Track Extends box
static trex(meta) {
let trackId = meta.id;
let data = new Uint8Array([
0x00, 0x00, 0x00, 0x00, // version(0) + flags
(trackId >>> 24) & 0xFF, // track_ID
(trackId >>> 16) & 0xFF,
(trackId >>> 8) & 0xFF,
(trackId) & 0xFF,
0x00, 0x00, 0x00, 0x01, // default_sample_description_index
0x00, 0x00, 0x00, 0x00, // default_sample_duration
0x00, 0x00, 0x00, 0x00, // default_sample_size
0x00, 0x01, 0x00, 0x01 // default_sample_flags
]);
return MP4.box(MP4.types.trex, data);
}
// Movie fragment box
static moof(track, baseMediaDecodeTime) {
return MP4.box(MP4.types.moof, MP4.mfhd(track.sequenceNumber), MP4.traf(track, baseMediaDecodeTime));
}
static mfhd(sequenceNumber) {
let data = new Uint8Array([
0x00, 0x00, 0x00, 0x00,
(sequenceNumber >>> 24) & 0xFF, // sequence_number: int32
(sequenceNumber >>> 16) & 0xFF,
(sequenceNumber >>> 8) & 0xFF,
(sequenceNumber) & 0xFF
]);
return MP4.box(MP4.types.mfhd, data);
}
// Track fragment box
static traf(track, baseMediaDecodeTime) {
let trackId = track.id;
// Track fragment header box
let tfhd = MP4.box(MP4.types.tfhd, new Uint8Array([
0x00, 0x00, 0x00, 0x00, // version(0) & flags
(trackId >>> 24) & 0xFF, // track_ID
(trackId >>> 16) & 0xFF,
(trackId >>> 8) & 0xFF,
(trackId) & 0xFF
]));
// Track Fragment Decode Time
let tfdt = MP4.box(MP4.types.tfdt, new Uint8Array([
0x00, 0x00, 0x00, 0x00, // version(0) & flags
(baseMediaDecodeTime >>> 24) & 0xFF, // baseMediaDecodeTime: int32
(baseMediaDecodeTime >>> 16) & 0xFF,
(baseMediaDecodeTime >>> 8) & 0xFF,
(baseMediaDecodeTime) & 0xFF
]));
let sdtp = MP4.sdtp(track);
let trun = MP4.trun(track, sdtp.byteLength + 16 + 16 + 8 + 16 + 8 + 8);
return MP4.box(MP4.types.traf, tfhd, tfdt, trun, sdtp);
}
// Sample Dependency Type box
static sdtp(track) {
let samples = track.samples || [];
let sampleCount = samples.length;
let data = new Uint8Array(4 + sampleCount);
// 0~4 bytes: version(0) & flags
for (let i = 0; i < sampleCount; i++) {
let flags = samples[i].flags;
data[i + 4] = (flags.isLeading << 6) // is_leading: 2 (bit)
| (flags.dependsOn << 4) // sample_depends_on
| (flags.isDependedOn << 2) // sample_is_depended_on
| (flags.hasRedundancy); // sample_has_redundancy
}
return MP4.box(MP4.types.sdtp, data);
}
// Track fragment run box
static trun(track, offset) {
let samples = track.samples || [];
let sampleCount = samples.length;
let dataSize = 12 + 16 * sampleCount;
let data = new Uint8Array(dataSize);
offset += 8 + dataSize;
data.set([
0x00, 0x00, 0x0F, 0x01, // version(0) & flags
(sampleCount >>> 24) & 0xFF, // sample_count
(sampleCount >>> 16) & 0xFF,
(sampleCount >>> 8) & 0xFF,
(sampleCount) & 0xFF,
(offset >>> 24) & 0xFF, // data_offset
(offset >>> 16) & 0xFF,
(offset >>> 8) & 0xFF,
(offset) & 0xFF
], 0);
for (let i = 0; i < sampleCount; i++) {
let duration = samples[i].duration;
let size = samples[i].size;
let flags = samples[i].flags;
let cts = samples[i].cts;
data.set([
(duration >>> 24) & 0xFF, // sample_duration
(duration >>> 16) & 0xFF,
(duration >>> 8) & 0xFF,
(duration) & 0xFF,
(size >>> 24) & 0xFF, // sample_size
(size >>> 16) & 0xFF,
(size >>> 8) & 0xFF,
(size) & 0xFF,
(flags.isLeading << 2) | flags.dependsOn, // sample_flags
(flags.isDependedOn << 6) | (flags.hasRedundancy << 4) | flags.isNonSync,
0x00, 0x00, // sample_degradation_priority
(cts >>> 24) & 0xFF, // sample_composition_time_offset
(cts >>> 16) & 0xFF,
(cts >>> 8) & 0xFF,
(cts) & 0xFF
], 12 + 16 * i);
}
return MP4.box(MP4.types.trun, data);
}
static mdat(data) {
return MP4.box(MP4.types.mdat, data);
}
}
MP4.init();
export default MP4;

View File

@ -0,0 +1,743 @@
/*
* 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 MP4 from './mp4-generator.js';
import AAC from './aac-silent.js';
import Browser from '../utils/browser.js';
import {SampleInfo, MediaSegmentInfo, MediaSegmentInfoList} from '../core/media-segment-info.js';
import {IllegalStateException} from '../utils/exception.js';
// Fragmented mp4 remuxer
class MP4Remuxer {
constructor(config) {
this.TAG = 'MP4Remuxer';
this._config = config;
this._isLive = (config.isLive === true) ? true : false;
this._dtsBase = -1;
this._dtsBaseInited = false;
this._audioDtsBase = Infinity;
this._videoDtsBase = Infinity;
this._audioNextDts = undefined;
this._videoNextDts = undefined;
this._audioStashedLastSample = null;
this._videoStashedLastSample = null;
this._audioMeta = null;
this._videoMeta = null;
this._audioSegmentInfoList = new MediaSegmentInfoList('audio');
this._videoSegmentInfoList = new MediaSegmentInfoList('video');
this._onInitSegment = null;
this._onMediaSegment = null;
// Workaround for chrome < 50: Always force first sample as a Random Access Point in media segment
// see https://bugs.chromium.org/p/chromium/issues/detail?id=229412
this._forceFirstIDR = (Browser.chrome &&
(Browser.version.major < 50 ||
(Browser.version.major === 50 && Browser.version.build < 2661))) ? true : false;
// Workaround for IE11/Edge: Fill silent aac frame after keyframe-seeking
// Make audio beginDts equals with video beginDts, in order to fix seek freeze
this._fillSilentAfterSeek = (Browser.msedge || Browser.msie);
// While only FireFox supports 'audio/mp4, codecs="mp3"', use 'audio/mpeg' for chrome, safari, ...
this._mp3UseMpegAudio = !Browser.firefox;
this._fillAudioTimestampGap = this._config.fixAudioTimestampGap;
}
destroy() {
this._dtsBase = -1;
this._dtsBaseInited = false;
this._audioMeta = null;
this._videoMeta = null;
this._audioSegmentInfoList.clear();
this._audioSegmentInfoList = null;
this._videoSegmentInfoList.clear();
this._videoSegmentInfoList = null;
this._onInitSegment = null;
this._onMediaSegment = null;
}
bindDataSource(producer) {
producer.onDataAvailable = this.remux.bind(this);
producer.onTrackMetadata = this._onTrackMetadataReceived.bind(this);
return this;
}
/* prototype: function onInitSegment(type: string, initSegment: ArrayBuffer): void
InitSegment: {
type: string,
data: ArrayBuffer,
codec: string,
container: string
}
*/
get onInitSegment() {
return this._onInitSegment;
}
set onInitSegment(callback) {
this._onInitSegment = callback;
}
/* prototype: function onMediaSegment(type: string, mediaSegment: MediaSegment): void
MediaSegment: {
type: string,
data: ArrayBuffer,
sampleCount: int32
info: MediaSegmentInfo
}
*/
get onMediaSegment() {
return this._onMediaSegment;
}
set onMediaSegment(callback) {
this._onMediaSegment = callback;
}
insertDiscontinuity() {
this._audioNextDts = this._videoNextDts = undefined;
}
seek(originalDts) {
this._audioStashedLastSample = null;
this._videoStashedLastSample = null;
this._videoSegmentInfoList.clear();
this._audioSegmentInfoList.clear();
}
remux(audioTrack, videoTrack) {
if (!this._onMediaSegment) {
throw new IllegalStateException('MP4Remuxer: onMediaSegment callback must be specificed!');
}
if (!this._dtsBaseInited) {
this._calculateDtsBase(audioTrack, videoTrack);
}
this._remuxVideo(videoTrack);
this._remuxAudio(audioTrack);
}
_onTrackMetadataReceived(type, metadata) {
let metabox = null;
let container = 'mp4';
let codec = metadata.codec;
if (type === 'audio') {
this._audioMeta = metadata;
if (metadata.codec === 'mp3' && this._mp3UseMpegAudio) {
// 'audio/mpeg' for MP3 audio track
container = 'mpeg';
codec = '';
metabox = new Uint8Array();
} else {
// 'audio/mp4, codecs="codec"'
metabox = MP4.generateInitSegment(metadata);
}
} else if (type === 'video') {
this._videoMeta = metadata;
metabox = MP4.generateInitSegment(metadata);
} else {
return;
}
// dispatch metabox (Initialization Segment)
if (!this._onInitSegment) {
throw new IllegalStateException('MP4Remuxer: onInitSegment callback must be specified!');
}
this._onInitSegment(type, {
type: type,
data: metabox.buffer,
codec: codec,
container: `${type}/${container}`,
mediaDuration: metadata.duration // in timescale 1000 (milliseconds)
});
}
_calculateDtsBase(audioTrack, videoTrack) {
if (this._dtsBaseInited) {
return;
}
if (audioTrack.samples && audioTrack.samples.length) {
this._audioDtsBase = audioTrack.samples[0].dts;
}
if (videoTrack.samples && videoTrack.samples.length) {
this._videoDtsBase = videoTrack.samples[0].dts;
}
this._dtsBase = Math.min(this._audioDtsBase, this._videoDtsBase);
this._dtsBaseInited = true;
}
flushStashedSamples() {
let videoSample = this._videoStashedLastSample;
let audioSample = this._audioStashedLastSample;
let videoTrack = {
type: 'video',
id: 1,
sequenceNumber: 0,
samples: [],
length: 0
};
if (videoSample != null) {
videoTrack.samples.push(videoSample);
videoTrack.length = videoSample.length;
}
let audioTrack = {
type: 'audio',
id: 2,
sequenceNumber: 0,
samples: [],
length: 0
};
if (audioSample != null) {
audioTrack.samples.push(audioSample);
audioTrack.length = audioSample.length;
}
this._videoStashedLastSample = null;
this._audioStashedLastSample = null;
this._remuxVideo(videoTrack, true);
this._remuxAudio(audioTrack, true);
}
_remuxAudio(audioTrack, force) {
if (this._audioMeta == null) {
return;
}
let track = audioTrack;
let samples = track.samples;
let dtsCorrection = undefined;
let firstDts = -1, lastDts = -1, lastPts = -1;
let refSampleDuration = this._audioMeta.refSampleDuration;
let mpegRawTrack = this._audioMeta.codec === 'mp3' && this._mp3UseMpegAudio;
let firstSegmentAfterSeek = this._dtsBaseInited && this._audioNextDts === undefined;
let insertPrefixSilentFrame = false;
if (!samples || samples.length === 0) {
return;
}
if (samples.length === 1 && !force) {
// If [sample count in current batch] === 1 && (force != true)
// Ignore and keep in demuxer's queue
return;
} // else if (force === true) do remux
let offset = 0;
let mdatbox = null;
let mdatBytes = 0;
// calculate initial mdat size
if (mpegRawTrack) {
// for raw mpeg buffer
offset = 0;
mdatBytes = track.length;
} else {
// for fmp4 mdat box
offset = 8; // size + type
mdatBytes = 8 + track.length;
}
let lastSample = null;
// Pop the lastSample and waiting for stash
if (samples.length > 1) {
lastSample = samples.pop();
mdatBytes -= lastSample.length;
}
// Insert [stashed lastSample in the previous batch] to the front
if (this._audioStashedLastSample != null) {
let sample = this._audioStashedLastSample;
this._audioStashedLastSample = null;
samples.unshift(sample);
mdatBytes += sample.length;
}
// Stash the lastSample of current batch, waiting for next batch
if (lastSample != null) {
this._audioStashedLastSample = lastSample;
}
let firstSampleOriginalDts = samples[0].dts - this._dtsBase;
// calculate dtsCorrection
if (this._audioNextDts) {
dtsCorrection = firstSampleOriginalDts - this._audioNextDts;
} else { // this._audioNextDts == undefined
if (this._audioSegmentInfoList.isEmpty()) {
dtsCorrection = 0;
if (this._fillSilentAfterSeek && !this._videoSegmentInfoList.isEmpty()) {
if (this._audioMeta.originalCodec !== 'mp3') {
insertPrefixSilentFrame = true;
}
}
} else {
let lastSample = this._audioSegmentInfoList.getLastSampleBefore(firstSampleOriginalDts);
if (lastSample != null) {
let distance = (firstSampleOriginalDts - (lastSample.originalDts + lastSample.duration));
if (distance <= 3) {
distance = 0;
}
let expectedDts = lastSample.dts + lastSample.duration + distance;
dtsCorrection = firstSampleOriginalDts - expectedDts;
} else { // lastSample == null, cannot found
dtsCorrection = 0;
}
}
}
if (insertPrefixSilentFrame) {
// align audio segment beginDts to match with current video segment's beginDts
let firstSampleDts = firstSampleOriginalDts - dtsCorrection;
let videoSegment = this._videoSegmentInfoList.getLastSegmentBefore(firstSampleOriginalDts);
if (videoSegment != null && videoSegment.beginDts < firstSampleDts) {
let silentUnit = AAC.getSilentFrame(this._audioMeta.originalCodec, this._audioMeta.channelCount);
if (silentUnit) {
let dts = videoSegment.beginDts;
let silentFrameDuration = firstSampleDts - videoSegment.beginDts;
Log.v(this.TAG, `InsertPrefixSilentAudio: dts: ${dts}, duration: ${silentFrameDuration}`);
samples.unshift({unit: silentUnit, dts: dts, pts: dts});
mdatBytes += silentUnit.byteLength;
} // silentUnit == null: Cannot generate, skip
} else {
insertPrefixSilentFrame = false;
}
}
let mp4Samples = [];
// Correct dts for each sample, and calculate sample duration. Then output to mp4Samples
for (let i = 0; i < samples.length; i++) {
let sample = samples[i];
let unit = sample.unit;
let originalDts = sample.dts - this._dtsBase;
let dts = originalDts - dtsCorrection;
if (firstDts === -1) {
firstDts = dts;
}
let sampleDuration = 0;
if (i !== samples.length - 1) {
let nextDts = samples[i + 1].dts - this._dtsBase - dtsCorrection;
sampleDuration = nextDts - dts;
} else { // the last sample
if (lastSample != null) { // use stashed sample's dts to calculate sample duration
let nextDts = lastSample.dts - this._dtsBase - dtsCorrection;
sampleDuration = nextDts - dts;
} else if (mp4Samples.length >= 1) { // use second last sample duration
sampleDuration = mp4Samples[mp4Samples.length - 1].duration;
} else { // the only one sample, use reference sample duration
sampleDuration = Math.floor(refSampleDuration);
}
}
let needFillSilentFrames = false;
let silentFrames = null;
// Silent frame generation, if large timestamp gap detected && config.fixAudioTimestampGap
if (sampleDuration > refSampleDuration * 1.5 && this._audioMeta.codec !== 'mp3' && this._fillAudioTimestampGap && !Browser.safari) {
// We need to insert silent frames to fill timestamp gap
needFillSilentFrames = true;
let delta = Math.abs(sampleDuration - refSampleDuration);
let frameCount = Math.ceil(delta / refSampleDuration);
let currentDts = dts + refSampleDuration; // Notice: in float
Log.w(this.TAG, 'Large audio timestamp gap detected, may cause AV sync to drift. ' +
'Silent frames will be generated to avoid unsync.\n' +
`dts: ${dts + sampleDuration} ms, expected: ${dts + Math.round(refSampleDuration)} ms, ` +
`delta: ${Math.round(delta)} ms, generate: ${frameCount} frames`);
let silentUnit = AAC.getSilentFrame(this._audioMeta.originalCodec, this._audioMeta.channelCount);
if (silentUnit == null) {
Log.w(this.TAG, 'Unable to generate silent frame for ' +
`${this._audioMeta.originalCodec} with ${this._audioMeta.channelCount} channels, repeat last frame`);
// Repeat last frame
silentUnit = unit;
}
silentFrames = [];
for (let j = 0; j < frameCount; j++) {
let intDts = Math.round(currentDts); // round to integer
if (silentFrames.length > 0) {
// Set previous frame sample duration
let previousFrame = silentFrames[silentFrames.length - 1];
previousFrame.duration = intDts - previousFrame.dts;
}
let frame = {
dts: intDts,
pts: intDts,
cts: 0,
unit: silentUnit,
size: silentUnit.byteLength,
duration: 0, // wait for next sample
originalDts: originalDts,
flags: {
isLeading: 0,
dependsOn: 1,
isDependedOn: 0,
hasRedundancy: 0
}
};
silentFrames.push(frame);
mdatBytes += unit.byteLength;
currentDts += refSampleDuration;
}
// last frame: align end time to next frame dts
let lastFrame = silentFrames[silentFrames.length - 1];
lastFrame.duration = dts + sampleDuration - lastFrame.dts;
// silentFrames.forEach((frame) => {
// Log.w(this.TAG, `SilentAudio: dts: ${frame.dts}, duration: ${frame.duration}`);
// });
// Set correct sample duration for current frame
sampleDuration = Math.round(refSampleDuration);
}
mp4Samples.push({
dts: dts,
pts: dts,
cts: 0,
unit: sample.unit,
size: sample.unit.byteLength,
duration: sampleDuration,
originalDts: originalDts,
flags: {
isLeading: 0,
dependsOn: 1,
isDependedOn: 0,
hasRedundancy: 0
}
});
if (needFillSilentFrames) {
// Silent frames should be inserted after wrong-duration frame
mp4Samples.push.apply(mp4Samples, silentFrames);
}
}
// allocate mdatbox
if (mpegRawTrack) {
// allocate for raw mpeg buffer
mdatbox = new Uint8Array(mdatBytes);
} else {
// allocate for fmp4 mdat box
mdatbox = new Uint8Array(mdatBytes);
// size field
mdatbox[0] = (mdatBytes >>> 24) & 0xFF;
mdatbox[1] = (mdatBytes >>> 16) & 0xFF;
mdatbox[2] = (mdatBytes >>> 8) & 0xFF;
mdatbox[3] = (mdatBytes) & 0xFF;
// type field (fourCC)
mdatbox.set(MP4.types.mdat, 4);
}
// Write samples into mdatbox
for (let i = 0; i < mp4Samples.length; i++) {
let unit = mp4Samples[i].unit;
mdatbox.set(unit, offset);
offset += unit.byteLength;
}
let latest = mp4Samples[mp4Samples.length - 1];
lastDts = latest.dts + latest.duration;
this._audioNextDts = lastDts;
// fill media segment info & add to info list
let info = new MediaSegmentInfo();
info.beginDts = firstDts;
info.endDts = lastDts;
info.beginPts = firstDts;
info.endPts = lastDts;
info.originalBeginDts = mp4Samples[0].originalDts;
info.originalEndDts = latest.originalDts + latest.duration;
info.firstSample = new SampleInfo(mp4Samples[0].dts,
mp4Samples[0].pts,
mp4Samples[0].duration,
mp4Samples[0].originalDts,
false);
info.lastSample = new SampleInfo(latest.dts,
latest.pts,
latest.duration,
latest.originalDts,
false);
if (!this._isLive) {
this._audioSegmentInfoList.append(info);
}
track.samples = mp4Samples;
track.sequenceNumber++;
let moofbox = null;
if (mpegRawTrack) {
// Generate empty buffer, because useless for raw mpeg
moofbox = new Uint8Array();
} else {
// Generate moof for fmp4 segment
moofbox = MP4.moof(track, firstDts);
}
track.samples = [];
track.length = 0;
let segment = {
type: 'audio',
data: this._mergeBoxes(moofbox, mdatbox).buffer,
sampleCount: mp4Samples.length,
info: info
};
if (mpegRawTrack && firstSegmentAfterSeek) {
// For MPEG audio stream in MSE, if seeking occurred, before appending new buffer
// We need explicitly set timestampOffset to the desired point in timeline for mpeg SourceBuffer.
segment.timestampOffset = firstDts;
}
this._onMediaSegment('audio', segment);
}
_remuxVideo(videoTrack, force) {
if (this._videoMeta == null) {
return;
}
let track = videoTrack;
let samples = track.samples;
let dtsCorrection = undefined;
let firstDts = -1, lastDts = -1;
let firstPts = -1, lastPts = -1;
if (!samples || samples.length === 0) {
return;
}
if (samples.length === 1 && !force) {
// If [sample count in current batch] === 1 && (force != true)
// Ignore and keep in demuxer's queue
return;
} // else if (force === true) do remux
let offset = 8;
let mdatbox = null;
let mdatBytes = 8 + videoTrack.length;
let lastSample = null;
// Pop the lastSample and waiting for stash
if (samples.length > 1) {
lastSample = samples.pop();
mdatBytes -= lastSample.length;
}
// Insert [stashed lastSample in the previous batch] to the front
if (this._videoStashedLastSample != null) {
let sample = this._videoStashedLastSample;
this._videoStashedLastSample = null;
samples.unshift(sample);
mdatBytes += sample.length;
}
// Stash the lastSample of current batch, waiting for next batch
if (lastSample != null) {
this._videoStashedLastSample = lastSample;
}
let firstSampleOriginalDts = samples[0].dts - this._dtsBase;
// calculate dtsCorrection
if (this._videoNextDts) {
dtsCorrection = firstSampleOriginalDts - this._videoNextDts;
} else { // this._videoNextDts == undefined
if (this._videoSegmentInfoList.isEmpty()) {
dtsCorrection = 0;
} else {
let lastSample = this._videoSegmentInfoList.getLastSampleBefore(firstSampleOriginalDts);
if (lastSample != null) {
let distance = (firstSampleOriginalDts - (lastSample.originalDts + lastSample.duration));
if (distance <= 3) {
distance = 0;
}
let expectedDts = lastSample.dts + lastSample.duration + distance;
dtsCorrection = firstSampleOriginalDts - expectedDts;
} else { // lastSample == null, cannot found
dtsCorrection = 0;
}
}
}
let info = new MediaSegmentInfo();
let mp4Samples = [];
// Correct dts for each sample, and calculate sample duration. Then output to mp4Samples
for (let i = 0; i < samples.length; i++) {
let sample = samples[i];
let originalDts = sample.dts - this._dtsBase;
let isKeyframe = sample.isKeyframe;
let dts = originalDts - dtsCorrection;
let cts = sample.cts;
let pts = dts + cts;
if (firstDts === -1) {
firstDts = dts;
firstPts = pts;
}
let sampleDuration = 0;
if (i !== samples.length - 1) {
let nextDts = samples[i + 1].dts - this._dtsBase - dtsCorrection;
sampleDuration = nextDts - dts;
} else { // the last sample
if (lastSample != null) { // use stashed sample's dts to calculate sample duration
let nextDts = lastSample.dts - this._dtsBase - dtsCorrection;
sampleDuration = nextDts - dts;
} else if (mp4Samples.length >= 1) { // use second last sample duration
sampleDuration = mp4Samples[mp4Samples.length - 1].duration;
} else { // the only one sample, use reference sample duration
sampleDuration = Math.floor(this._videoMeta.refSampleDuration);
}
}
if (isKeyframe) {
let syncPoint = new SampleInfo(dts, pts, sampleDuration, sample.dts, true);
syncPoint.fileposition = sample.fileposition;
info.appendSyncPoint(syncPoint);
}
mp4Samples.push({
dts: dts,
pts: pts,
cts: cts,
units: sample.units,
size: sample.length,
isKeyframe: isKeyframe,
duration: sampleDuration,
originalDts: originalDts,
flags: {
isLeading: 0,
dependsOn: isKeyframe ? 2 : 1,
isDependedOn: isKeyframe ? 1 : 0,
hasRedundancy: 0,
isNonSync: isKeyframe ? 0 : 1
}
});
}
// allocate mdatbox
mdatbox = new Uint8Array(mdatBytes);
mdatbox[0] = (mdatBytes >>> 24) & 0xFF;
mdatbox[1] = (mdatBytes >>> 16) & 0xFF;
mdatbox[2] = (mdatBytes >>> 8) & 0xFF;
mdatbox[3] = (mdatBytes) & 0xFF;
mdatbox.set(MP4.types.mdat, 4);
// Write samples into mdatbox
for (let i = 0; i < mp4Samples.length; i++) {
let units = mp4Samples[i].units;
while (units.length) {
let unit = units.shift();
let data = unit.data;
mdatbox.set(data, offset);
offset += data.byteLength;
}
}
let latest = mp4Samples[mp4Samples.length - 1];
lastDts = latest.dts + latest.duration;
lastPts = latest.pts + latest.duration;
this._videoNextDts = lastDts;
// fill media segment info & add to info list
info.beginDts = firstDts;
info.endDts = lastDts;
info.beginPts = firstPts;
info.endPts = lastPts;
info.originalBeginDts = mp4Samples[0].originalDts;
info.originalEndDts = latest.originalDts + latest.duration;
info.firstSample = new SampleInfo(mp4Samples[0].dts,
mp4Samples[0].pts,
mp4Samples[0].duration,
mp4Samples[0].originalDts,
mp4Samples[0].isKeyframe);
info.lastSample = new SampleInfo(latest.dts,
latest.pts,
latest.duration,
latest.originalDts,
latest.isKeyframe);
if (!this._isLive) {
this._videoSegmentInfoList.append(info);
}
track.samples = mp4Samples;
track.sequenceNumber++;
// workaround for chrome < 50: force first sample as a random access point
// see https://bugs.chromium.org/p/chromium/issues/detail?id=229412
if (this._forceFirstIDR) {
let flags = mp4Samples[0].flags;
flags.dependsOn = 2;
flags.isNonSync = 0;
}
let moofbox = MP4.moof(track, firstDts);
track.samples = [];
track.length = 0;
this._onMediaSegment('video', {
type: 'video',
data: this._mergeBoxes(moofbox, mdatbox).buffer,
sampleCount: mp4Samples.length,
info: info
});
}
_mergeBoxes(moof, mdat) {
let result = new Uint8Array(moof.byteLength + mdat.byteLength);
result.set(moof, 0);
result.set(mdat, moof.byteLength);
return result;
}
}
export default MP4Remuxer;

View File

@ -0,0 +1,128 @@
/*
* 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.
*/
let Browser = {};
function detect() {
// modified from jquery-browser-plugin
let ua = self.navigator.userAgent.toLowerCase();
let match = /(edge)\/([\w.]+)/.exec(ua) ||
/(opr)[\/]([\w.]+)/.exec(ua) ||
/(chrome)[ \/]([\w.]+)/.exec(ua) ||
/(iemobile)[\/]([\w.]+)/.exec(ua) ||
/(version)(applewebkit)[ \/]([\w.]+).*(safari)[ \/]([\w.]+)/.exec(ua) ||
/(webkit)[ \/]([\w.]+).*(version)[ \/]([\w.]+).*(safari)[ \/]([\w.]+)/.exec(ua) ||
/(webkit)[ \/]([\w.]+)/.exec(ua) ||
/(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) ||
/(msie) ([\w.]+)/.exec(ua) ||
ua.indexOf('trident') >= 0 && /(rv)(?::| )([\w.]+)/.exec(ua) ||
ua.indexOf('compatible') < 0 && /(firefox)[ \/]([\w.]+)/.exec(ua) ||
[];
let platform_match = /(ipad)/.exec(ua) ||
/(ipod)/.exec(ua) ||
/(windows phone)/.exec(ua) ||
/(iphone)/.exec(ua) ||
/(kindle)/.exec(ua) ||
/(android)/.exec(ua) ||
/(windows)/.exec(ua) ||
/(mac)/.exec(ua) ||
/(linux)/.exec(ua) ||
/(cros)/.exec(ua) ||
[];
let matched = {
browser: match[5] || match[3] || match[1] || '',
version: match[2] || match[4] || '0',
majorVersion: match[4] || match[2] || '0',
platform: platform_match[0] || ''
};
let browser = {};
if (matched.browser) {
browser[matched.browser] = true;
let versionArray = matched.majorVersion.split('.');
browser.version = {
major: parseInt(matched.majorVersion, 10),
string: matched.version
};
if (versionArray.length > 1) {
browser.version.minor = parseInt(versionArray[1], 10);
}
if (versionArray.length > 2) {
browser.version.build = parseInt(versionArray[2], 10);
}
}
if (matched.platform) {
browser[matched.platform] = true;
}
if (browser.chrome || browser.opr || browser.safari) {
browser.webkit = true;
}
// MSIE. IE11 has 'rv' identifer
if (browser.rv || browser.iemobile) {
if (browser.rv) {
delete browser.rv;
}
let msie = 'msie';
matched.browser = msie;
browser[msie] = true;
}
// Microsoft Edge
if (browser.edge) {
delete browser.edge;
let msedge = 'msedge';
matched.browser = msedge;
browser[msedge] = true;
}
// Opera 15+
if (browser.opr) {
let opera = 'opera';
matched.browser = opera;
browser[opera] = true;
}
// Stock android browsers are marked as Safari
if (browser.safari && browser.android) {
let android = 'android';
matched.browser = android;
browser[android] = true;
}
browser.name = matched.browser;
browser.platform = matched.platform;
for (let key in Browser) {
if (Browser.hasOwnProperty(key)) {
delete Browser[key];
}
}
Object.assign(Browser, browser);
}
detect();
export default Browser;

View File

@ -0,0 +1,73 @@
/*
* 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 class RuntimeException {
constructor(message) {
this._message = message;
}
get name() {
return 'RuntimeException';
}
get message() {
return this._message;
}
toString() {
return this.name + ': ' + this.message;
}
}
export class IllegalStateException extends RuntimeException {
constructor(message) {
super(message);
}
get name() {
return 'IllegalStateException';
}
}
export class InvalidArgumentException extends RuntimeException {
constructor(message) {
super(message);
}
get name() {
return 'InvalidArgumentException';
}
}
export class NotImplementedException extends RuntimeException {
constructor(message) {
super(message);
}
get name() {
return 'NotImplementedException';
}
}

View File

@ -0,0 +1,140 @@
/*
* 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';
class Log {
static e(tag, msg) {
if (!tag || Log.FORCE_GLOBAL_TAG)
tag = Log.GLOBAL_TAG;
let str = `[${tag}] > ${msg}`;
if (Log.ENABLE_CALLBACK) {
Log.emitter.emit('log', 'error', str);
}
if (!Log.ENABLE_ERROR) {
return;
}
if (console.error) {
console.error(str);
} else if (console.warn) {
console.warn(str);
} else {
console.log(str);
}
}
static i(tag, msg) {
if (!tag || Log.FORCE_GLOBAL_TAG)
tag = Log.GLOBAL_TAG;
let str = `[${tag}] > ${msg}`;
if (Log.ENABLE_CALLBACK) {
Log.emitter.emit('log', 'info', str);
}
if (!Log.ENABLE_INFO) {
return;
}
if (console.info) {
console.info(str);
} else {
console.log(str);
}
}
static w(tag, msg) {
if (!tag || Log.FORCE_GLOBAL_TAG)
tag = Log.GLOBAL_TAG;
let str = `[${tag}] > ${msg}`;
if (Log.ENABLE_CALLBACK) {
Log.emitter.emit('log', 'warn', str);
}
if (!Log.ENABLE_WARN) {
return;
}
if (console.warn) {
console.warn(str);
} else {
console.log(str);
}
}
static d(tag, msg) {
if (!tag || Log.FORCE_GLOBAL_TAG)
tag = Log.GLOBAL_TAG;
let str = `[${tag}] > ${msg}`;
if (Log.ENABLE_CALLBACK) {
Log.emitter.emit('log', 'debug', str);
}
if (!Log.ENABLE_DEBUG) {
return;
}
if (console.debug) {
console.debug(str);
} else {
console.log(str);
}
}
static v(tag, msg) {
if (!tag || Log.FORCE_GLOBAL_TAG)
tag = Log.GLOBAL_TAG;
let str = `[${tag}] > ${msg}`;
if (Log.ENABLE_CALLBACK) {
Log.emitter.emit('log', 'verbose', str);
}
if (!Log.ENABLE_VERBOSE) {
return;
}
console.log(str);
}
}
Log.GLOBAL_TAG = 'flv.js';
Log.FORCE_GLOBAL_TAG = false;
Log.ENABLE_ERROR = true;
Log.ENABLE_INFO = true;
Log.ENABLE_WARN = true;
Log.ENABLE_DEBUG = true;
Log.ENABLE_VERBOSE = true;
Log.ENABLE_CALLBACK = false;
Log.emitter = new EventEmitter();
export default Log;

View File

@ -0,0 +1,165 @@
/*
* 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 './logger.js';
class LoggingControl {
static get forceGlobalTag() {
return Log.FORCE_GLOBAL_TAG;
}
static set forceGlobalTag(enable) {
Log.FORCE_GLOBAL_TAG = enable;
LoggingControl._notifyChange();
}
static get globalTag() {
return Log.GLOBAL_TAG;
}
static set globalTag(tag) {
Log.GLOBAL_TAG = tag;
LoggingControl._notifyChange();
}
static get enableAll() {
return Log.ENABLE_VERBOSE
&& Log.ENABLE_DEBUG
&& Log.ENABLE_INFO
&& Log.ENABLE_WARN
&& Log.ENABLE_ERROR;
}
static set enableAll(enable) {
Log.ENABLE_VERBOSE = enable;
Log.ENABLE_DEBUG = enable;
Log.ENABLE_INFO = enable;
Log.ENABLE_WARN = enable;
Log.ENABLE_ERROR = enable;
LoggingControl._notifyChange();
}
static get enableDebug() {
return Log.ENABLE_DEBUG;
}
static set enableDebug(enable) {
Log.ENABLE_DEBUG = enable;
LoggingControl._notifyChange();
}
static get enableVerbose() {
return Log.ENABLE_VERBOSE;
}
static set enableVerbose(enable) {
Log.ENABLE_VERBOSE = enable;
LoggingControl._notifyChange();
}
static get enableInfo() {
return Log.ENABLE_INFO;
}
static set enableInfo(enable) {
Log.ENABLE_INFO = enable;
LoggingControl._notifyChange();
}
static get enableWarn() {
return Log.ENABLE_WARN;
}
static set enableWarn(enable) {
Log.ENABLE_WARN = enable;
LoggingControl._notifyChange();
}
static get enableError() {
return Log.ENABLE_ERROR;
}
static set enableError(enable) {
Log.ENABLE_ERROR = enable;
LoggingControl._notifyChange();
}
static getConfig() {
return {
globalTag: Log.GLOBAL_TAG,
forceGlobalTag: Log.FORCE_GLOBAL_TAG,
enableVerbose: Log.ENABLE_VERBOSE,
enableDebug: Log.ENABLE_DEBUG,
enableInfo: Log.ENABLE_INFO,
enableWarn: Log.ENABLE_WARN,
enableError: Log.ENABLE_ERROR,
enableCallback: Log.ENABLE_CALLBACK
};
}
static applyConfig(config) {
Log.GLOBAL_TAG = config.globalTag;
Log.FORCE_GLOBAL_TAG = config.forceGlobalTag;
Log.ENABLE_VERBOSE = config.enableVerbose;
Log.ENABLE_DEBUG = config.enableDebug;
Log.ENABLE_INFO = config.enableInfo;
Log.ENABLE_WARN = config.enableWarn;
Log.ENABLE_ERROR = config.enableError;
Log.ENABLE_CALLBACK = config.enableCallback;
}
static _notifyChange() {
let emitter = LoggingControl.emitter;
if (emitter.listenerCount('change') > 0) {
let config = LoggingControl.getConfig();
emitter.emit('change', config);
}
}
static registerListener(listener) {
LoggingControl.emitter.addListener('change', listener);
}
static removeListener(listener) {
LoggingControl.emitter.removeListener('change', listener);
}
static addLogListener(listener) {
Log.emitter.addListener('log', listener);
if (Log.emitter.listenerCount('log') > 0) {
Log.ENABLE_CALLBACK = true;
LoggingControl._notifyChange();
}
}
static removeLogListener(listener) {
Log.emitter.removeListener('log', listener);
if (Log.emitter.listenerCount('log') === 0) {
Log.ENABLE_CALLBACK = false;
LoggingControl._notifyChange();
}
}
}
LoggingControl.emitter = new EventEmitter();
export default LoggingControl;

View File

@ -0,0 +1,58 @@
/*
* 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 Polyfill {
static install() {
// ES6 Object.setPrototypeOf
Object.setPrototypeOf = Object.setPrototypeOf || function (obj, proto) {
obj.__proto__ = proto;
return obj;
};
// ES6 Object.assign
Object.assign = Object.assign || function (target) {
if (target === undefined || target === null) {
throw new TypeError('Cannot convert undefined or null to object');
}
let output = Object(target);
for (let i = 1; i < arguments.length; i++) {
let source = arguments[i];
if (source !== undefined && source !== null) {
for (let key in source) {
if (source.hasOwnProperty(key)) {
output[key] = source[key];
}
}
}
}
return output;
};
// ES6 Promise (missing support in IE11)
if (typeof self.Promise !== 'function') {
require('es6-promise').polyfill();
}
}
}
Polyfill.install();
export default Polyfill;

View File

@ -0,0 +1,84 @@
/*
* Copyright (C) 2016 Bilibili. All Rights Reserved.
*
* This file is derived from C++ project libWinTF8 (https://github.com/m13253/libWinTF8)
* @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.
*/
function checkContinuation(uint8array, start, checkLength) {
let array = uint8array;
if (start + checkLength < array.length) {
while (checkLength--) {
if ((array[++start] & 0xC0) !== 0x80)
return false;
}
return true;
} else {
return false;
}
}
function decodeUTF8(uint8array) {
let out = [];
let input = uint8array;
let i = 0;
let length = uint8array.length;
while (i < length) {
if (input[i] < 0x80) {
out.push(String.fromCharCode(input[i]));
++i;
continue;
} else if (input[i] < 0xC0) {
// fallthrough
} else if (input[i] < 0xE0) {
if (checkContinuation(input, i, 1)) {
let ucs4 = (input[i] & 0x1F) << 6 | (input[i + 1] & 0x3F);
if (ucs4 >= 0x80) {
out.push(String.fromCharCode(ucs4 & 0xFFFF));
i += 2;
continue;
}
}
} else if (input[i] < 0xF0) {
if (checkContinuation(input, i, 2)) {
let ucs4 = (input[i] & 0xF) << 12 | (input[i + 1] & 0x3F) << 6 | input[i + 2] & 0x3F;
if (ucs4 >= 0x800 && (ucs4 & 0xF800) !== 0xD800) {
out.push(String.fromCharCode(ucs4 & 0xFFFF));
i += 3;
continue;
}
}
} else if (input[i] < 0xF8) {
if (checkContinuation(input, i, 3)) {
let ucs4 = (input[i] & 0x7) << 18 | (input[i + 1] & 0x3F) << 12
| (input[i + 2] & 0x3F) << 6 | (input[i + 3] & 0x3F);
if (ucs4 > 0x10000 && ucs4 < 0x110000) {
ucs4 -= 0x10000;
out.push(String.fromCharCode((ucs4 >>> 10) | 0xD800));
out.push(String.fromCharCode((ucs4 & 0x3FF) | 0xDC00));
i += 4;
continue;
}
}
}
out.push(String.fromCharCode(0xFFFD));
++i;
}
return out.join('');
}
export default decodeUTF8;

View File

@ -0,0 +1,45 @@
import Player from 'xgplayer'
import Flv from './flv/flv'
class FlvJsPlayer extends Player {
constructor (options) {
super(options)
this.flvOpts = { type: 'flv' }
Player.util.deepCopy(this.flvOpts, options)
const player = this
player.once('complete', () => {
player.__flv__ = Flv.createPlayer(this.flvOpts)
player.createInstance(player.__flv__)
})
}
createInstance (flv) {
const player = this
const util = Player.util
flv.attachMediaElement(player.video)
flv.load()
flv.play()
if (this.flvOpts.isLive) {
util.addClass(player.root, 'xgplayer-is-live')
const live = util.createDom('xg-live', '正在直播', {}, 'xgplayer-live')
player.controls.appendChild(live)
}
flv.on(Flv.Events.ERROR, (e) => {
player.emit('error', new Player.Errors('other', player.config.url))
})
}
switchURL (url) {
const player = this
const flvPlayer = player.__flv__
player.config.url = url
if (!player.config.isLive) {
flvPlayer.onDefinitionChange(url, player.config.retryTimes)
} else {
const tempFlvPlayer = Flv.createPlayer(player.flvOpts)
flvPlayer.destroy()
player.createInstance(tempFlvPlayer)
player.__flv__ = tempFlvPlayer
}
}
}
FlvJsPlayer.isSupported = Flv.isSupported
export default FlvJsPlayer

View File

@ -0,0 +1,77 @@
const polyfill = []
const umd = {
entry: polyfill.concat(['./src/index.js']),
output: {
path: `${__dirname}/dist`,
filename: 'index.js',
library: 'xgplayer-flv.js',
libraryTarget: 'umd'
},
mode: 'production',
module: {
rules: [{
test: /\.js$/,
loader: 'babel-loader'
}, {
test: /\.scss$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 1,
minimize: true
}
},
'postcss-loader',
'sass-loader'
]
}]
},
externals: {
'xgplayer': 'xgplayer'
},
optimization: {
minimize: true
}
}
const client = {
entry: polyfill.concat(['./src/index.js']),
output: {
path: `${__dirname}/browser`,
filename: 'xgplayer-flvjs.js',
library: 'FlvJsPlayer',
libraryTarget: 'window'
},
module: {
rules: [{
test: /\.js$/,
loader: 'babel-loader'
}, {
test: /\.scss$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 1,
minimize: true
}
},
'postcss-loader',
'sass-loader'
]
}]
},
externals: {
'xgplayer': 'Player'
},
mode: 'production',
optimization: {
minimize: true
}
}
module.exports = [umd, client]

View File

@ -0,0 +1,13 @@
{
"presets": [
"env",
"react",
"stage-0"
],
"plugins": [
"transform-class-properties",
"transform-decorators",
"transform-react-constant-elements",
"transform-react-inline-elements"
]
}

4
packages/xgplayer-flv/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/node_modules
.idea/
npm-debug.log
.vscode/

File diff suppressed because one or more lines are too long

1
packages/xgplayer-flv/dist/index.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,30 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>flv转码到fmp4</title>
<style>
.container {
width: 100%;
min-height: 300px;
}
#start_btn {
width: 100px;
height: 30px;
}
#byted-player {
width: 480px;
height: 320px;
}
</style>
</head>
<body>
<div class="container">
<button type="button" id="start_btn">开始解析</button>
<div id="byted-player"></div>
</div>
</body>
</html>

9900
packages/xgplayer-flv/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,42 @@
{
"name": "xgplayer-flv",
"version": "1.0.2",
"description": "flv demuxer for xgplayer",
"main": "./dist/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"prepare": "npm run build",
"build": "webpack --progress --display-chunks -p",
"watch": "webpack --progress --display-chunks -p --watch"
},
"repository": {
"type": "git",
"url": "git+https://github.com/bytedance/xgplayer.git"
},
"keywords": [],
"author": "leo",
"license": "MIT",
"devDependencies": {
"babel": "^6.23.0",
"babel-env": "^2.4.1",
"babel-eslint": "^8.2.2",
"babel-loader": "^7.1.2",
"babel-plugin-transform-react-constant-elements": "^6.23.0",
"babel-preset-es2015": "^6.24.1",
"clean-webpack-plugin": "^0.1.17",
"css-loader": "^0.28.10",
"extract-text-webpack-plugin": "^3.0.2",
"html-loader": "^0.5.5",
"html-webpack-plugin": "^2.30.1",
"style-loader": "^0.20.2",
"webpack": "^4.12.0",
"webpack-dev-server": "^2.11.1"
},
"peerDependency": {
"xgplayer": "^0.1.0"
},
"dependencies": {
"concat-typed-array": "^1.0.2",
"event-emitter": "^0.3.5"
}
}

View File

@ -0,0 +1,7 @@
module.exports = {
plugins: {
'postcss-cssnext': {
browserslist: ['cover 99.5%'],
},
},
};

View File

@ -0,0 +1,9 @@
const defaultConf = {
preloadTime: 60,
minCachedTime: 5,
autoCleanSourceBuffer: true,
autoCleanMaxBackTime: 60,
isLive: false
}
export default Object.assign({}, defaultConf)

View File

@ -0,0 +1,85 @@
const fields = [{
name: 'duration',
type: Boolean,
parser (target, origin) {
target.mediaInfo.duration = origin.duration
}
}, {
name: 'hasAudio',
type: Boolean,
parser (target, origin) {
target.mediaInfo.hasAudio = origin.hasAudio
}
}, {
name: 'hasVideo',
type: Boolean,
parser (target, origin) {
target.mediaInfo.hasVideo = origin.hasVideo
}
}, {
name: 'audiodatarate',
type: Number,
parser (target, origin) {
target.mediaInfo.audioDataRate = origin.audiodatarate
}
}, {
name: 'videodatarate',
type: Number,
parser (target, origin) {
target.mediaInfo.videoDataRate = origin.videodatarate
}
}, {
name: 'width',
type: Number,
parser (target, origin) {
target.mediaInfo.width = origin.width
}
}, {
name: 'height',
type: Number,
parser (target, origin) {
target.mediaInfo.height = origin.height
}
}, {
name: 'duration',
type: Number,
parser (target, origin) {
if (!target.state.duration) {
const duration = Math.floor(origin.duration * target.state.timeScale)
target.state.duration = target.mediaInfo.duration = duration
}
},
onTypeErr (target) {
target.mediaInfo.duration = 0
}
}, {
name: 'framerate',
type: Number,
parser (target, origin) {
const fpsNum = Math.floor(origin.framerate * 1000)
if (fpsNum > 0) {
const fps = fpsNum / 1000
const { referFrameRate, mediaInfo } = target
referFrameRate.fixed = true
referFrameRate.fps = fps
referFrameRate.fpsNum = fpsNum
referFrameRate.fpsDen = 1000
mediaInfo.fps = fps
}
}
}, {
name: 'keyframes',
type: Object,
parser (target, origin) {
const { keyframes } = origin
target.mediaInfo.hasKeyframes = !!keyframes
if (keyframes) {
target.mediaInfo.keyframes = this._parseKeyframes(keyframes)
}
origin.keyframes = null
},
onTypeErr (target) {
target.mediaInfo.hasKeyframes = false
}
}]
export default fields

View File

@ -0,0 +1,2 @@
// export const videoUrl = 'http://yunxianchang.live.ujne7.com/vod-system-bj/TLaf2cc9d469939803949187b46da16c45.flv';
export const videoUrl = 'http://127.0.0.1:8000/TLaf2cc9d469939803949187b46da16c45.flv'

View File

@ -0,0 +1,73 @@
export const MetaTypes = {
NUMBER: 0,
BOOLEAN: 1,
STRING: 2,
OBJECT: 3,
MIX_ARRAY: 8,
OBJECT_END: 9,
STRICT_ARRAY: 10,
DATE: 11,
LONE_STRING: 12
}
export const EventTypes = {
DATA_READY: 'data_ready',
META_DATA_READY: 'meta_data_ready',
TRACK_META_READY: 'track_meta_ready',
MEDIA_INFO_READY: 'media_info_ready',
META_END_POSITION: 'meta_end_position',
ERROR: 'error'
}
export const soundRateTypes = [
5500,
11000,
22000,
4400
]
export const AudioObjectTypes = {
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'
}
export const samplingFrequencyTypes = [
96000, 88200,
64000, 48000,
44100, 32000,
24000, 22050,
16000, 12000,
11025, 8000
]
export const browserTypes = {
IE: 'ie',
FIRE_FOX: 'firefox',
CHROME: 'chrome',
OPERA: 'opera',
SAFARI: 'safari'
}
export const mp3Versions = {
V25: 0,
RESERVED: 1,
V20: 2,
V10: 3
}
export const audioSampleRate = {
V10: [44100, 48000, 32000, 0],
V20: [22050, 24000, 16000, 0],
V25: [11025, 12000, 8000, 0]
}
export const mp3BitRate = {
Layer1: [0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, -1],
Layer2: [0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, -1],
Layer3: [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, -1]
}

View File

@ -0,0 +1,149 @@
import MainParser from './parse/MainParser';
import Player from 'xgplayer';
import MSE from './parse/MSE';
import VodTask from './utils/VodTask';
import defaultConfig from './constants/config';
const isEnded = function (player, flv) {
if (flv.videoDuration - player.currentTime * flv.videoTimeScale < 2 * flv.videoTimeScale) {
const range = player.getBufferedRange();
if (player.currentTime - range[1] < 0.1) {
player.mse.endOfStream();
}
}
};
let tempCurrentTime = 0;
const flvPlayer = function () {
let player = this,
flv,
mse,
seekDataReceived,
requestSending = false,
_config = Object.assign(defaultConfig, player.config),
sniffer = Player.sniffer;
const { preloadTime, minCachedTime, isLive } = _config;
function progressChecker() {
const range = player.getBufferedRange();
if (flv.videoDuration - range[1] * flv.videoTimeScale < 0.1 * flv.videoTimeScale) { return; }
if (range[1] - player.currentTime < minCachedTime && !requestSending) {
requestSending = true;
flv.loadSegments(true, player.currentTime, preloadTime).then(() => {
requestSending = false;
});
}
}
const _start = player.start;
player.start = function (url = _config.url) {
if (!url) { return; }
flv = new MainParser(_config, player);
flv.on('ready', (ftyp_moov) => {
mse = player.mse = flv.mse = new MSE();
_start.call(player, mse.url);
mse.on('sourceopen', () => {
flv.isSourceOpen = true;
mse.appendBuffer(ftyp_moov.buffer);
mse.on('updateend', () => {
const { pendingFragments, hasPendingFragments } = flv;
seekDataReceived = true;
if (hasPendingFragments) {
const fragment = pendingFragments.shift();
if (!mse.appendBuffer(fragment.data)) {
pendingFragments.unshift(fragment);
} else {
player.emit('cacheupdate', player);
}
}
});
});
mse.on('error', (e) => {
player.emit('error', e);
});
});
flv.handleError = function (e) {
player.emit('error', e);
};
};
player.on('pause', () => {
!isLive && VodTask.clear();
});
//
// player.on('resume', function () {
//
// });
//
// player.on('waiting', function () {
// console.log('waiting');
// });
if (!isLive) {
player.on('seeking', () => {
VodTask.clear();
const { buffered, currentTime } = player;
let isBuffered = false;
if (buffered.length) {
for (let i = 0, len = buffered.length; i < len; i++) {
if (currentTime > buffered.start(i) && currentTime < buffered.end(i)) {
isBuffered = true;
break;
}
}
}
if (isBuffered) {
return;
}
if (!flv.isSeekable) {
player.currentTime = tempCurrentTime;
return;
}
flv.seek(player.currentTime, preloadTime, player);
seekDataReceived = false;
});
player.on('timeupdate', () => {
tempCurrentTime = player.currentTime;
if (seekDataReceived !== false) {
progressChecker();
}
isEnded(player, flv);
});
player.beforeReplay = function () {
VodTask.clear();
const mse = flv.mse = new MSE();
flv.replay();
mse.on('sourceopen', () => {
flv.isSourceOpen = true;
mse.appendBuffer(flv.ftyp_moof.buffer);
mse.on('updateend', () => {
const { pendingFragments, hasPendingFragments } = flv;
seekDataReceived = true;
if (hasPendingFragments) {
const fragment = pendingFragments.shift();
if (!mse.appendBuffer(fragment.data)) {
pendingFragments.unshift(fragment);
} else {
player.emit('cacheupdate', player);
}
}
});
});
mse.on('error', (e) => {
player.emit('error', e);
});
player.emit('cacheupdate', player);
player.src = mse.url;
player.mse = mse;
return true;
};
}
};
Player.install('flvplayer', flvPlayer);

View File

@ -0,0 +1,75 @@
export default class MediaInfo {
constructor (data) {
const _default = {
mimeType: null,
codec: '',
duration: null,
hasAudio: false,
hasVideo: false,
audioCodec: null,
videoCodec: null,
videoDataRate: null,
audioDataRate: null,
audioSampleRate: null,
audioChannelCount: null,
audioConfig: null,
width: null,
height: null,
fps: null,
profile: null,
level: null,
chromaFormat: null,
pixelRatio: [],
_metaData: null,
segments: [],
hasKeyframes: null,
keyframes: [],
};
const initData = Object.assign({}, _default, data);
Object.entries(initData).forEach(([k, v])=> {
this[k] = v;
});
}
get isComplete () {
const { mimeType, duration, hasKeyframes } = this;
return mimeType !== null
&& duration !== null
&& hasKeyframes !== null
&& this.isVideoInfoFilled
&& this.isAudioInfoFilled;
}
get isAudioInfoFilled () {
const { hasAudio,
audioCodec,
audioSampleRate,
audioChannelCount,
} = this;
return !!(!hasAudio || (hasAudio && audioCodec && audioSampleRate && audioChannelCount));
}
get isVideoInfoFilled () {
const notNullFields = [
'videoCodec',
'width',
'height',
'fps',
'profile',
'level',
'chromaFormat',
];
for (let i = 0, len = notNullFields.length; i < len; i++) {
if (this[notNullFields[i]] === null) { return false; }
}
return this.hasVideo;
}
}

View File

@ -0,0 +1,28 @@
export default class MediaSample {
constructor (info) {
let _default = MediaSample.getDefaultInf();
if (!info || Object.prototype.toString.call(info) !== '[object Object]') {
return _default;
}
let sample = Object.assign({}, _default, info);
Object.entries(sample).forEach(([k, v]) => {
this[k] = v;
});
}
static getDefaultInf () {
return {
dts: null,
pts: null,
duration: null,
position: null,
isRAP: false, // is Random access point
originDts: null,
};
}
}

View File

@ -0,0 +1,18 @@
export default class MediaSegment {
constructor () {
this.startDts = -1;
this.endDts = -1;
this.startPts = -1;
this.endPts = -1;
this.originStartDts = -1;
this.originEndDts = -1;
this.randomAccessPoints = [];
this.firstSample = null;
this.lastSample = null;
}
addRAP (sample) {
sample.isRAP = true;
this.randomAccessPoints.push(sample);
}
}

View File

@ -0,0 +1,115 @@
export default class MediaSegmentList {
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 (beginDts) {
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 (beginDts < list[0].originDts) {
idx = -1;
return idx;
}
while (lbound <= ubound) {
mid = lbound + Math.floor((ubound - lbound) / 2);
if (mid === last || (beginDts > list[mid].lastSample.originDts
&& (beginDts < list[mid + 1].originDts))) {
idx = mid;
break;
} else if (list[mid].originDts < beginDts) {
lbound = mid + 1;
} else {
ubound = mid - 1;
}
}
return idx;
}
_searchNearestSegmentAfter (beginDts) {
return this._searchNearestSegmentBefore(beginDts) + 1;
}
append (segment) {
let list = this._list;
let lastAppendIdx = this._lastAppendLocation;
let insertIdx = 0;
if (lastAppendIdx !== -1 && lastAppendIdx < list.length
&& segment.originStartDts >= list[lastAppendIdx].lastSample.originDts
&& ((lastAppendIdx === list.length - 1)
|| (lastAppendIdx < list.length - 1
&& segment.originStartDts < list[lastAppendIdx + 1].originStartDts))) {
insertIdx = lastAppendIdx + 1; // use cached location idx
} else {
if (list.length > 0) {
insertIdx = this._searchNearestSegmentBefore(segment.originStartDts) + 1;
}
}
this._lastAppendLocation = insertIdx;
this._list.splice(insertIdx, 0, segment);
}
getLastSegmentBefore (beginDts) {
let idx = this._searchNearestSegmentBefore(beginDts);
if (idx >= 0) {
return this._list[idx];
} else { // -1
return null;
}
}
getLastSampleBefore (beginDts) {
let segment = this.getLastSegmentBefore(beginDts);
if (segment !== null) {
return segment.lastSample;
} else {
return null;
}
}
getLastRAPBefore (beginDts) {
let segmentIdx = this._searchNearestSegmentBefore(beginDts);
let randomAccessPoints = this._list[segmentIdx].randomAccessPoints;
while (randomAccessPoints.length === 0 && segmentIdx > 0) {
segmentIdx--;
randomAccessPoints = this._list[segmentIdx].randomAccessPoints;
}
if (randomAccessPoints.length > 0) {
return randomAccessPoints[randomAccessPoints.length - 1];
} else {
return null;
}
}
}

View File

@ -0,0 +1,25 @@
export default class FlvTag {
constructor () {
this.tagType = -1;
this.bodySize = -1;
this.tagSize = -1;
this.position = -1;
this.Timestamp = -1;
this.StreamID = -1;
this.body = -1;
this.time = -1;
this.arr = [];
}
getTime () {
this.arr = [];
for (let i = 0; i < this.Timestamp.length; i++) {
this.arr.push((this.Timestamp[i].toString(16).length === 1 ? '0' + this.Timestamp[i].toString(16) : this.Timestamp[i].toString(16)));
}
this.arr.pop();
const time = this.arr.join('');
this.time = parseInt(time, 16);
return parseInt(time, 16);
}
}

View File

@ -0,0 +1,128 @@
import Demuxer from './demux/Demuxer';
import Buffer from '../write/Buffer';
import Tag from '../models/Tag';
export default class FlvParser extends Demuxer {
constructor (store) {
super(store);
this.CLASS_NAME = this.constructor.name;
this.temp_u8a = null;
this.dataLen = 0;
this.stop = false;
this.index = 0; // record the position in single round
this.offset = 0;
this.filePosition = 0; // record current file position
this.firstFlag = true;
}
seek () {
this.offset = 0;
}
setFlv (flv_u8a) {
this.stop = false;
this.index = 0;
this.offset = 0;
const temp_u8a = this.temp_u8a = flv_u8a;
this.dataLen = this.temp_u8a.length;
if (!this.firstFlag) {
return this.parseData();
} else if (temp_u8a.length > 13 && FlvParser.isFlvHead(temp_u8a)) {
this.parseHead();
this.readData(9); // 跳过头部
this.readData(4); // 跳过下一个记录头部size的 int32
this.parseData();
this.firstFlag = false;
this.filePosition += this.offset;
return this.offset;
} else {
return this.offset;
}
}
parseData () {
const { length: u8a_length } = this.temp_u8a;
while (this.index < u8a_length && !this.stop) {
this.offset = this.index;
const tag = new Tag();
if (this.unreadLength >= 11) {
// 可以读出头部信息
tag.position = this.filePosition + this.offset;
tag.tagType = this.readData(1)[0];
tag.bodySize = this.readData(3);
tag.Timestamp = this.readData(4);
tag.StramId = this.readData(3);
} else {
this.stop = true;
continue;
}
if (this.unreadLength >= this.getBodySize(tag.bodySize) + 4) {
tag.body = this.readData(this.getBodySize(tag.bodySize));
tag.tagSize = this.readData(4);
const { tags, _hasVideo, _hasAudio } = this._store.state;
switch (tag.tagType) {
case 9:
_hasVideo && tags.push(tag);
break;
case 8:
_hasAudio && tags.push(tag);
break;
case 18:
tags.push(tag);
break;
}
} else {
this.stop = true;
continue;
}
this.offset = this.index;
}
this.filePosition += this.offset;
this.temp_u8a = null;
return this.offset;
}
/**
* @param sizeArr
* @return
*/
getBodySize (sizeArr) {
return Buffer.readAsInt(sizeArr);
}
parseHead () {
const { temp_u8a, _store } = this;
const result = {
match: false,
};
if (temp_u8a[3] !== 1) {
return result;
}
const flag = temp_u8a[4];
const hasAudio = ((flag & 4) >>> 2) !== 0;
const hasVideo = (flag & 1) !== 0;
if (!hasAudio && !hasVideo) {
return result;
}
_store.hasAudio = hasAudio;
_store.hasVideo = hasVideo;
}
readData (length) {
const _index = this.index;
this.index += length;
return this.temp_u8a.slice(_index, _index + length);
}
get unreadLength () {
return this.dataLen - this.index;
}
static isFlvHead(temp_u8a) {
let firstThreeChars = [temp_u8a[0], temp_u8a[1], temp_u8a[2]];
return String.fromCharCode.apply(String, firstThreeChars) === 'FLV';
}
}

View File

@ -0,0 +1,7 @@
import EventEmitter from '../utils/EventEmitter';
export default class LevelController {
constructor (url) {
// TODO
}
}

View File

@ -0,0 +1,94 @@
import EventEmitter from 'event-emitter';
class MSE {
constructor (codecs = 'video/mp4; codecs="avc1.64001E, mp4a.40.5"') {
let self = this;
EventEmitter(this);
this.codecs = codecs;
this.mediaSource = new window.MediaSource();
this.url = window.URL.createObjectURL(this.mediaSource);
this.mediaSource.addEventListener('sourceopen', function () {
self.sourceBuffer = self.mediaSource.addSourceBuffer(self.codecs);
self.sourceBuffer.addEventListener('error', function (e) {
self.emit('error', {
type: 'sourceBuffer',
error: e,
});
});
self.sourceBuffer.addEventListener('updateend', function (e) {
self.emit('updateend');
});
self.emit('sourceopen');
self.sourceBuffer.addEventListener('error', function (e) {
self.emit('error', {
type: 'mediaSource',
error: e,
});
});
});
this.mediaSource.addEventListener('sourceclose', function () {
self.emit('sourceclose');
});
}
get state () {
return this.mediaSource.readyState;
}
get duration () {
return this.mediaSource.duration;
}
set duration (value) {
this.mediaSource.duration = value;
}
appendBuffer (buffer) {
let sourceBuffer = this.sourceBuffer;
if (sourceBuffer.updating === false && this.state === 'open') {
sourceBuffer.appendBuffer(buffer);
return true;
} else {
if (this.state === 'closed') {
this.emit('error', {
type: 'sourceBuffer',
error: new Error('mediaSource is not attached to video or mediaSource is closed'),
});
} else if (this.state === 'ended') {
this.emit('error', {
type: 'sourceBuffer',
error: new Error('mediaSource is closed'),
});
} else {
if (sourceBuffer.updating === true) {
this.emit('warn', {
type: 'sourceBuffer',
error: new Error('mediaSource is busy'),
});
}
return false;
}
}
}
removeBuffer (start, end) {
this.sourceBuffer.remove(start, end);
}
endOfStream () {
if (this.mediaSource.readyState === 'open') {
this.mediaSource.endOfStream();
}
}
static isSupported (codecs) {
return window.MediaSource && window.MediaSource.isTypeSupported(codecs);
}
}
export default MSE;

View File

@ -0,0 +1,326 @@
import Mp4Remuxer from './remux/Mp4remux';
import Demuxer from './demux/Demuxer';
import flvParser from './FlvParser';
import tagDemuxer from './demux/TagDemuxer';
import Store from '../utils/Store';
import VodTask from '../utils/VodTask';
import LiveTask from '../utils/LiveTask';
import { EventTypes } from '../constants/types';
import Buffer from '../write/Buffer';
export default class MainParser extends Demuxer {
constructor (config, player) {
super();
this.CLASS_NAME = this.constructor.name;
this._config = config;
this._player = player;
this._tempBaseTime = 0;
this.firstFlag = true;
this._store = new Store();
this._store.isLive = config.isLive || false;
this.flvParser = new flvParser(this._store);
this.tagDemuxer = new tagDemuxer(this._store);
this._mp4remuxer = new Mp4Remuxer();
this._mp4remuxer.isLive = config.isLive || false;
this.buffer = new Buffer();
this.bufferKeyframes = new Set();
this.META_CHUNK_SIZE = Math.pow(10, 5);
this.CHUNK_SIZE = Math.pow(10, 6);
this.ftyp_moof = null;
this.isSourceOpen = false;
this._isNewSegmentsArrival = false;
this.range = {
start: -1,
end: -1,
};
this._isMediaInfoInited = false;
this._pendingFragments = [];
this._pendingRemoveRange = [];
if (!config.isLive) {
this.initMeta();
} else {
this.initLiveStream();
}
this.initEventBind();
}
initLiveStream () {
new LiveTask(this._config.url, {}).run(this.loadLiveData.bind(this));
}
loadLiveData (buffer) {
if (buffer === undefined) {
this.emit('live-end');
}
this.buffer.write(new Uint8Array(buffer));
let offset = this.setFlv(this.buffer.buffer);
this.buffer.buffer = this.buffer.buffer.slice(offset);
}
initMeta () {
const self = this;
const Resolver = {
resolveChunk (buffer) {
self.buffer.write(new Uint8Array(buffer));
let offset = self.setFlv(self.buffer.buffer);
self.buffer.buffer = self.buffer.buffer.slice(offset);
if (!self.isMediaInfoReady) {
self.initMeta();
}
},
};
this.range = {
start: this.range.end + 1,
end: this.range.end + this.META_CHUNK_SIZE,
};
this.loadMetaData(this.range.start, this.range.end).then(Resolver.resolveChunk);
}
loadSegments (changeRange, currentTime = 0, preloadTime) {
this._isNewSegmentsArrival = false;
const resolveChunk = (buffer) => {
this.buffer.write(new Uint8Array(buffer));
let offset = this.setFlv(this.buffer.buffer);
this.buffer.buffer = this.buffer.buffer.slice(offset);
if (!this._isNewSegmentsArrival) {
this.loadSegments(true);
}
};
if (changeRange) {
let _range = this.range;
if (this.getNextRangeEnd(currentTime, preloadTime) <= _range.end) {
return Promise.resolve();
}
this.range = {
start: this.range.end + 1,
end: currentTime === undefined ? this.range.end + this.CHUNK_SIZE : this.getNextRangeEnd(currentTime, preloadTime),
};
if (this.range.start >= this.range.end || !this.range.end) {
this.range = _range;
return Promise.resolve();
}
}
return this._loadSegmentsData(this.range.start, this.range.end).then(resolveChunk);
}
getNextRangeEnd (start, preloadTime) {
const { keyframes: { times, filePositions }, videoTimeScale } = this._store;
if (!times || !filePositions) { return this.range.end + this.CHUNK_SIZE; }
start *= videoTimeScale;
let expectEnd = start + (preloadTime * videoTimeScale);
if (expectEnd > times[times.length - 1]) { return times[times.length - 1]; }
let left = 0, right = times.length, index;
while (left <= right) {
let mid = Math.floor((right + left) / 2);
if (times[mid] <= expectEnd && expectEnd <= times[mid + 1]) {
index = mid;
break;
} else if (expectEnd < times[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return index ? filePositions[index] : '';
}
_loadSegmentsData (start = 0, end = start + this.CHUNK_SIZE) {
return new VodTask(this._config.url, [start, end]).promise;
}
loadMetaData (start = 0, end = start + this.META_CHUNK_SIZE) {
return new VodTask(this._config.url, [start, end]).promise;
}
setFlvFirst (arrayBuff, baseTime) {
const offset = this.flvParser.setFlv(new Uint8Array(arrayBuff));
const { tags } = this._store.state;
if (tags.length) {
if (tags[0].tagType !== 18) {
throw new Error('flv file without metadata tag');
}
if (this._tempBaseTime !== 0 && this._tempBaseTime === tags[0].getTime()) {
this._store.state._timestampBase = 0;
}
this.tagDemuxer.resolveTags(tags);
}
this.firstFlag = false;
return offset;
}
setFlvUsually (arrayBuff, baseTime) {
const offset = this.flvParser.setFlv(new Uint8Array(arrayBuff));
const { tags } = this._store.state;
if (tags.length) {
this.tagDemuxer.resolveTags(tags);
}
return offset;
}
handleDataReady (audioTrack, videoTrack) {
this._mp4remuxer.remux(audioTrack, videoTrack);
}
handleMetaDataReady (type, meta) {
this._mp4remuxer.onMetaDataReady(type, meta);
}
handleError (e) {
this.error(e);
}
handleMediaInfoReady (mediaInfo) {
if (this._isMediaInfoInited) { return; }
const ftyp_moof = this._mp4remuxer.onMediaInfoReady(mediaInfo);
if (!this.ftyp_moof) {
this.ftyp_moof = ftyp_moof;
this.emit('ready', ftyp_moof);
}
this._isMediaInfoInited = true;
}
initEventBind () {
const prefix = 'demuxer_';
const {
handleError,
handleDataReady,
handleMetaDataReady,
handleMediaInfoReady,
} = this;
this.on('mediaFragment', (newFrag) => {
this._isNewSegmentsArrival = true;
this._pendingFragments.push(newFrag);
const { randomAccessPoints } = newFrag.fragment;
if (randomAccessPoints && randomAccessPoints.length) {
randomAccessPoints.forEach(rap => {
this.bufferKeyframes.add(rap.dts);
});
}
if (!this.isSourceOpen) { return; }
if (this._pendingFragments.length) {
const fragment = this._pendingFragments.shift();
if (!this.mse.appendBuffer(fragment.data)) {
this._pendingFragments.unshift(fragment);
} else {
this._player.emit('cacheupdate', this._player);
}
}
});
this.on(EventTypes.ERROR, handleError.bind(this));
this.on(`${prefix}data_ready`, handleDataReady.bind(this));
this.on(`${prefix}meta_data_ready`, handleMetaDataReady.bind(this));
this.on(`${prefix}media_info_ready`, handleMediaInfoReady.bind(this));
}
replay () {
this.range = {
start: this._store.metaEndPosition,
end: this.getNextRangeEnd(0, this._config.preloadTime),
};
// this.firstFlag = true;
this._mp4remuxer.seek();
this.flvParser.seek();
this.clearBuffer();
this.loadSegments(false);
}
clearBuffer () {
this._pendingFragments = [];
this._pendingRemoveRange = [];
}
seek (target) {
const { keyframes = {}, videoTimeScale } = this._store;
let seekStart = target * videoTimeScale, startFilePos, endFilePos;
const length = Math.min(keyframes.filePositions.length, keyframes.times.length);
let { preloadTime } = this._config;
function getEndFilePos (time, idx) {
if (idx === keyframes.times.length) {
endFilePos = idx;
return false;
}
if (time <= preloadTime && preloadTime <= keyframes.times[idx + 1]) {
endFilePos = idx;
return false;
// 需要处理EOF的情况
}
return true;
}
let lo = 0, hi = length - 2;
while (lo < hi) {
let mid = Math.floor((lo + hi) / 2);
let currentTime = keyframes.times[mid], nextTime = keyframes.times[mid + 1] ? keyframes.times[mid + 1] : Number.MAX_SAFE_INTEGER;
if (currentTime <= seekStart && seekStart <= nextTime) {
startFilePos = mid;
preloadTime = preloadTime * videoTimeScale + seekStart;
keyframes.times.every(getEndFilePos);
break;
} else if (seekStart < currentTime) {
hi = mid - 1;
} else {
lo = mid + 1;
}
}
this.buffer = new Buffer();
this._pendingFragments = [];
this._mp4remuxer.seek();
this.flvParser.seek();
this.range = {
start: keyframes.filePositions[startFilePos],
end: keyframes.filePositions[endFilePos] || '',
};
this.loadSegments(false);
}
get setFlv () {
return this.firstFlag ? this.setFlvFirst : this.setFlvUsually;
}
get isMediaInfoReady () {
return this._store.mediaInfo.isComplete;
}
get videoDuration () {
return this._store.mediaInfo.duration;
}
get hasPendingFragments () {
return !!this._pendingFragments.length;
}
get pendingFragments () {
return this._pendingFragments;
}
get videoTimeScale () {
return this._store.videoTimeScale;
}
get hasPendingRemoveRanges () {
return this._pendingRemoveRange.length;
}
get pendingRemoveRanges () {
return this._pendingRemoveRange;
}
get isSeekable () {
return this._store.isSeekable;
}
}

View File

@ -0,0 +1,75 @@
import ExpGolomb from '../utils/ExpGolomb';
export default class SPSParser {
static getProfileStr (profileIdc) {
switch (profileIdc) {
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 getLevelStr (levelIdc) {
return (levelIdc / 10).toFixed(1);
}
static getChromaFormatStr (chroma) {
switch (chroma) {
case 420:
return '4:2:0';
case 422:
return '4:2:2';
case 444:
return '4:4:4';
default:
return 'Unknown';
}
}
/**
* read SPS
* @param originArr
*/
static parseSPS (originArr) {
let rbsp = SPSParser._ebsp2rbsp(originArr);
const stream = new ExpGolomb(rbsp);
const spsConfig = stream.readSPS();
const { chromaFormat, levelIdc, profileIdc } = spsConfig;
spsConfig.profileString = SPSParser.getProfileStr(profileIdc);
spsConfig.levelString = SPSParser.getLevelStr(levelIdc);
spsConfig.chromaFormatString = SPSParser.getChromaFormatStr(chromaFormat);
return spsConfig;
}
//
static _ebsp2rbsp (originArr) {
const originLen = originArr.byteLength;
const dist = new Uint8Array(originArr.byteLength);
let distSize = 0;
for (let i = 0, len = originLen; i < len; i++) {
if (i > 2 && originArr[i] === 3 && originArr[i - 1] === 0 && originArr[i - 2] === 0) {
continue;
}
dist[distSize++] = originArr[i];
}
return new Uint8Array(dist.buffer, 0, distSize);
}
}

View File

@ -0,0 +1,237 @@
// refrence: https://github.com/video-dev/hls.js/blob/master/src/demux/adts.js
import Demuxer from './Demuxer';
import DataView4Read from '../../utils/DataView4Read';
// import { mp3Versions, mp3BitRate, audioSampleRate } from '../../constants/types';
import {
soundRateTypes,
samplingFrequencyTypes,
EventTypes,
browserTypes,
} from '../../constants/types';
import sniffer from '../../utils/sniffer';
import Buffer from '../../write/Buffer';
export default class AudioDemuxer extends Demuxer {
constructor (store) {
super(store);
this.CLASS_NAME = this.constructor.name;
this.currentTag = null;
this.data = new Uint8Array(0);
this.readOffset = 0;
this._store.audioMetaData = null;
}
resolve (tag) {
const { _store: store } = this;
const { audioTrack: track } = store;
this.currentTag = tag;
this.data = tag.body;
let {
audioMetaData: meta,
} = store;
if (!meta) {
meta = store.audioMetaData = {};
store.audioMetaData = this.initAudioMeta(meta);
}
const dv = new DataView4Read(tag.body.buffer, this);
const sound = dv.getUint8();
const soundFormatIdx = sound >>> 4, // UInt4
soundRate = (sound & 12) >>> 2, // UInt2
soundSize = (sound & 2) >>> 1, // UInt1
soundType = (sound % 1); // UInt1
meta.audioSampleRate = soundRateTypes[soundRate];
meta.channelCount = soundType === 0 ? 1 : 2;
if (soundFormatIdx !== 10 && soundFormatIdx !== 2) {
this.error('only support AAC Audio format so far');
return;
} else if (soundFormatIdx === 10) { // AAC
const aacInfo = this._parseAACAudio();
if (!aacInfo) {
return;
}
const { data: aacData, data: { sampleFreq } } = aacInfo;
if (aacInfo.packetType === 0) { // AAC sequence header
meta.sampleRate = sampleFreq;
meta.channelCount = aacData.channelCount;
meta.codec = aacData.codec;
meta.manifestCodec = aacData.manifestCodec;
meta.config = aacData.config;
meta.refSampleDuration = 1024 / sampleFreq * meta.timeScale;
if (store.hasInitialMetaDispatched) {
if (store.videoTrack.length || store.audioTrack.length) {
this.dispatch(EventTypes.DATA_READY, store.videoTrack, store.audioTrack);
}
} else {
store.state._audioInitialMetadataDispatched = true;
}
this.dispatch(EventTypes.META_DATA_READY, 'audio', meta);
const { mediaInfo: mi } = store;
mi.audioCodec = meta.codec;
mi.audioSampleRate = meta.sampleRate;
mi.audioChannelCount = meta.channelCount;
mi.audioConfig = meta.config;
if (mi.hasVideo) {
if (mi.videoCodec) {
mi.mimeType = `video/x-flv; codecs="${mi.videoCodec},${mi.audioCodec}"`;
mi.codec = mi.mimeType.replace('x-flv', 'mp4');
}
} else {
mi.mimeType = `video/x-flv; codecs="${mi.audioCodec}"`;
mi.codec = mi.mimeType.replace('x-flv', 'mp4');
}
if (mi.isComplete) {
this.dispatch(EventTypes.MEDIA_INFO_READY, mi);
}
} else if (aacInfo.packetType === 1) { // AAC raw frame data
let dts = store.state.timeStampBase + this.currentTag.getTime();
let aacSample = { unit: aacInfo.data, length: aacInfo.data.byteLength, dts: dts, pts: dts };
track.samples.push(aacSample);
track.length += aacInfo.data.length;
}
}
this.resetStatus();
}
_parseAACAudio () {
if (this.unreadLength <= 1) {
return;
}
const aacData = {};
let aacArray = new Uint8Array(this.data.buffer, this.readOffset, this.unreadLength);
const packetType = aacArray[0];
this.readOffset += 1;
aacData.packetType = packetType;
if (!packetType) {
const { position, tagSize } = this.currentTag;
this._store.metaEndPosition = position + Buffer.readAsInt(tagSize) + 4;
aacData.data = this._parseAACAudioSpecificConfig(); // AAC Sequence header
} else {
aacData.data = aacArray.slice(1);
}
return aacData;
}
_parseAACAudioSpecificConfig () {
const dv = new DataView4Read(this.data.buffer, this);
const { getAndNum } = DataView4Read;
let resultObj = {
samplingFrequency: null,
extAudioObjectType: null,
extAudioSamplingIdx: null,
},
config = {};
const UInt0 = dv.getUint8(),
UInt1 = dv.getUint8();
let tempAudioObjectType;
let audioObjectType = tempAudioObjectType = UInt0 >>> 3; // UInt5
let samplingIdx = ((UInt0 & getAndNum(5, 7)) << 1) | (UInt1 >>> 7); // UInt4
if (samplingIdx < 0 || samplingIdx > samplingFrequencyTypes.length) {
this.dispatch(EventTypes.ERROR, `error samplingFrequencyIndex: ${samplingIdx}`);
return;
}
resultObj.samplingFrequency = samplingFrequencyTypes[samplingIdx];
let channelCount = resultObj.channelCount = (UInt1 & getAndNum(1, 4)) >>> 3;
if (channelCount < 0 || channelCount > 7) {
this.dispatch(EventTypes.ERROR, `error Audio Channel Count: ${channelCount}`);
return;
}
if (audioObjectType === 5) { // HE-AAC
const UInt2 = dv.getUint8();
resultObj.extAudioSamplingIdx = ((UInt1 & getAndNum(5, 7)) << 1) | UInt2 >>> 7;
resultObj.extAudioObjectType = (UInt2 & getAndNum(1, 5)) >>> 2;
}
if (sniffer.browser === browserTypes.FIRE_FOX) {
if (samplingIdx >= 6) {
// HE-AAC uses SBR, high frequencies are constructed from low frequencies
audioObjectType = 5;
config = new Array(4);
resultObj.extAudioSamplingIdx = samplingIdx - 3;
} else {
audioObjectType = 2;
config = new Array(2);
resultObj.extAudioSamplingIdx = samplingIdx;
}
} else if (sniffer.os.isAndroid) {
// Android : always use AAC
audioObjectType = 2;
config = new Array(2);
resultObj.extAudioSamplingIdx = samplingIdx;
} else {
/* for other browsers (Chrome/Vivaldi/Opera ...)
always force audio type to be HE-AAC SBR, as some browsers do not support audio codec switch properly (like Chrome ...)
*/
audioObjectType = 5;
resultObj.extensionSamplingIndex = samplingIdx;
config = new Array(4);
if (samplingIdx >= 6) {
resultObj.extensionSamplingIdx = samplingIdx - 3;
} else if (channelCount === 1) {
audioObjectType = 2;
config = new Array(2);
resultObj.extensionSamplingIndex = samplingIdx;
}
}
config[0] = audioObjectType << 3;
config[0] |= (samplingIdx & 0x0E) >> 1;
config[1] |= (samplingIdx & 0x01) << 7;
config[1] |= channelCount << 3;
if (audioObjectType === 5) {
config[1] |= (resultObj.extAudioSamplingIdx & 0x0E) >> 1;
config[2] = (resultObj.extensionSamplingIdx & 0x01) << 7;
// adtsObjectType (force to 2, chrome is checking that object type is less than 5 ???
// https://chromium.googlesource.com/chromium/src.git/+/master/media/formats/mp4/aac.cc
config[2] |= 2 << 2;
config[3] = 0;
}
return {
config,
sampleFreq: resultObj.samplingFrequency,
channelCount,
codec: `mp4a.40.${audioObjectType}`,
manifestCodec: `mp4a.40.${tempAudioObjectType}`,
};
}
initAudioMeta (meta) {
const { state, audioTrack: track } = this._store;
meta.duration = state.duration;
meta.timeScale = state.timeScale;
meta.type = 'audio';
meta.id = track.id;
return meta;
}
resetStatus () {
this.currentTag = null;
this.data = new Uint8Array(0);
this.readOffset = 0;
}
get dataSize () {
return this.data.length;
}
get unreadLength () {
return this.dataSize - this.readOffset;
}
}

View File

@ -0,0 +1,37 @@
import Log from '../../utils/Log';
import emitter from '../../utils/EventEmitter';
export default class Demuxer {
constructor (store) {
if (store) {
this._store = store;
}
this._emitter = emitter;
this.on = emitter.on.bind(emitter);
this.emit = emitter.emit.bind(emitter);
}
dispatch (type, ...payload) {
const prefix = 'demuxer_';
this._emitter.emit(`${prefix}${type}`, ...payload);
}
error (message) {
const { CLASS_NAME = 'Demuxer' } = this;
Log.error(`[${CLASS_NAME} error] `, message);
}
info (message) {
const { CLASS_NAME = 'Demuxer' } = this;
Log.info(`[${CLASS_NAME} info] `, message);
}
log (message) {
const { CLASS_NAME = 'Demuxer' } = this;
Log.log(`[${CLASS_NAME} log] `, message);
}
warn (message) {
const { CLASS_NAME = 'Demuxer' } = this;
Log.warn(`[${CLASS_NAME} warn] `, message);
}
}

View File

@ -0,0 +1,241 @@
import { MetaTypes } from '../../constants/types';
import UTF8 from '../../utils/UTF8';
import Demuxer from './Demuxer';
/**
* meta信息解析
*/
export default class MetaDemuxer extends Demuxer {
constructor (store) {
super(store);
this.offset = 0;
this.readOffset = this.offset;
}
get isLe () {
return this._store.isLe;
}
resolve (meta, size) {
if (size < 3) {
throw 'not enough data for metainfo';
}
const metaData = {};
const name = this.parseValue(meta);
const value = this.parseValue(meta, size - name.bodySize);
metaData[name.data] = value.data;
this.resetStatus();
return metaData;
}
resetStatus () {
this.offset = 0;
this.readOffset = this.offset;
}
parseString (buffer) {
const dv = new DataView(buffer, this.readOffset);
const strLen = dv.getUint16(0, !this.isLe);
let str = '';
if (strLen > 0) {
str = UTF8.decode(new Uint8Array(buffer, this.readOffset + 2, strLen));
} else {
str = '';
}
let size = strLen + 2;
this.readOffset += size;
return {
data: str,
bodySize: strLen + 2,
};
}
parseDate (buffer, size) {
const { isLe } = this;
const dv = new DataView(buffer, this.readOffset, size);
let ts = dv.getFloat64(0, !isLe);
const timeOffset = dv.getInt16(8, !isLe);
ts += timeOffset * 60 * 1000;
this.readOffset += 10;
return {
data: new Date(ts),
bodySize: 10,
};
}
parseObject (buffer, size) {
const name = this.parseString(buffer, size);
const value = this.parseValue(buffer, size - name.bodySize);
return {
data: {
name: name.data,
value: value.data,
},
bodySize: name.bodySize + value.bodySize,
isObjEnd: value.isObjEnd,
};
}
parseLongString (buffer) {
const dv = new DataView(buffer, this.readOffset);
const strLen = dv.getUint32(0, !this.isLe);
let str = '';
if (strLen > 0) {
str = UTF8.decode(new Uint8Array(buffer, this.readOffset + 2, strLen));
} else {
str = '';
}
// const size = strLen + 4;
this.readOffset += strLen + 4;
return {
data: str,
bodySize: strLen + 4,
};
}
/**
* 解析meta中的变量
*/
parseValue (data, size) {
let buffer = new ArrayBuffer();
if (data instanceof ArrayBuffer) {
buffer = data;
} else {
buffer = data.buffer;
}
const { isLe } = this;
const {
NUMBER,
BOOLEAN,
STRING,
OBJECT,
MIX_ARRAY,
OBJECT_END,
STRICT_ARRAY,
DATE,
LONE_STRING,
} = MetaTypes;
const dataView = new DataView(buffer, this.readOffset, size);
let isObjEnd = false;
const type = dataView.getUint8(0);
let offset = 1;
this.readOffset += 1;
let value = null;
switch (type) {
case NUMBER: {
value = dataView.getFloat64(1, !isLe);
this.readOffset += 8;
offset += 8;
break;
}
case BOOLEAN: {
const boolNum = dataView.getUint8(1);
value = !!boolNum;
this.readOffset += 1;
offset += 1;
break;
}
case STRING: {
const str = this.parseString(buffer);
value = str.data;
offset += str.bodySize;
break;
}
case OBJECT: {
value = {};
let objEndSize = 0;
if (dataView.getUint32(size - 4, !isLe) & 0x00FFFFFF) {
objEndSize = 3;
}
// this.readOffset += offset - 1;
while (offset < size - 4) {
const amfObj = this.parseObject(buffer, size - offset - objEndSize);
if (amfObj.isObjectEnd) { break; }
value[amfObj.data.name] = amfObj.data.value;
offset += amfObj.bodySize;
}
if (offset <= size - 3) {
const mark = dataView.getUint32(offset - 1, !isLe) & 0x00FFFFFF;
if (mark === 9) {
this.readOffset += 3;
offset += 3;
}
}
break;
}
case MIX_ARRAY: {
value = {};
offset += 4;
this.readOffset += 4;
let objEndSize = 0;
if ((dataView.getUint32(size - 4, !isLe) & 0x00FFFFFF) === 9) {
objEndSize = 3;
}
while (offset < size - 8) {
const amfVar = this.parseObject(buffer, size - offset - objEndSize);
if (amfVar.isObjectEnd) { break; }
value[amfVar.data.name] = amfVar.data.value;
offset += amfVar.bodySize;
}
if (offset <= size - 3) {
const marker = dataView.getUint32(offset - 1, !isLe) & 0x00FFFFFF;
if (marker === 9) {
offset += 3;
this.readOffset += 3;
}
}
break;
}
case OBJECT_END: {
value = null;
isObjEnd = true;
break;
}
case STRICT_ARRAY: {
value = [];
const arrLength = dataView.getUint32(1, !isLe);
offset += 4;
this.readOffset += 4;
for (let i = 0; i < arrLength; i++) {
const script = this.parseValue(buffer, size - offset);
value.push(script.data);
offset += script.bodySize;
}
break;
}
case DATE: {
const date = this.parseDate(buffer, size - 1);
value = date.data;
offset += date.bodySize;
break;
}
case LONE_STRING: {
const longStr = this.parseLongString(buffer, size - 1);
value = longStr.data;
offset += longStr.bodySize;
break;
}
default: {
offset = size;
}
}
return {
data: value,
bodySize: offset,
isObjEnd: isObjEnd,
};
}
}

View File

@ -0,0 +1,126 @@
import Demuxer from './Demuxer';
import MetaDemuxer from './MetaDemuxer';
import VideoDemuxer from './VideoDemuxer';
import AudioDemuxer from './AudioDemuxer';
import Logger from '../../utils/Log';
import metaFields from '../../constants/metaFields';
import { EventTypes } from '../../constants/types';
const nativeHasProp = Object.prototype.hasOwnProperty;
export default class Tagdemux extends Demuxer {
constructor (store) {
super(store);
this.CLASS_NAME = this.constructor.name;
this._metaDemuxer = new MetaDemuxer(store);
this._videoDemuxer = new VideoDemuxer(store);
this._audioDemuxer = new AudioDemuxer(store);
this._firstParse = true;
this._dataOffset = 0;
}
resolveTags () {
const { tags } = this._store.state;
const { _store: store } = this;
const { videoTrack, audioTrack } = store;
tags.forEach((tag) => {
this.resolveTag(tag);
});
if (this._store.hasInitialMetaDispatched) {
if (videoTrack.length || audioTrack.length) {
this.dispatch(EventTypes.DATA_READY, audioTrack, videoTrack);
}
}
this._store.state.tags = [];
}
resolveTag (tag) {
switch (String(tag.tagType)) {
case '8': // audio
this._resolveAudioTag(tag);
break;
case '9': // video
this._resolveVideoTag(tag);
break;
case '18': // metadata
this._resolveMetaTag(tag);
break;
}
}
_resolveAudioTag (tag) {
if (tag.bodySize <= 1) {
this.warn('Not enough data for audio tag body');
}
this._audioDemuxer.resolve(tag);
}
_resolveVideoTag (tag) {
if (tag.bodySize <= 1) {
this.error('Not enough data for video tag body');
return;
}
const { _hasVideo, hasVideoFlagOverrided } = this;
if (hasVideoFlagOverrided && !_hasVideo) {
return;
}
this._videoDemuxer.resolve(tag);
}
_initMetaData (metaData) {
const { _store: s } = this;
if (nativeHasProp.call(metaData, 'onMetaData')) {
if (s.hasMetaData) {
Logger.log(`[${this.CLASS_NAME}]`, 'found another meta tag');
}
s.metaData = metaData;
const onMetaData = metaData.onMetaData;
metaFields.forEach(field => {
const { name, type, parser, onTypeErr } = field;
if (Object(onMetaData[name]) instanceof type) {
parser.call(this, s, onMetaData);
} else {
if (onTypeErr && onTypeErr instanceof Function) {
onTypeErr(s, onMetaData);
}
}
});
this._store.mediaInfo._metaData = metaData;
// 同步到共享store
if (this._store.mediaInfo.isComplete) {
this.dispatch(EventTypes.MEDIA_INFO_READY, this._store.mediaInfo);
}
}
}
_resolveMetaTag (tag) {
const { body } = tag;
const metaObj = this._metaDemuxer.resolve(body, body.length);
this._initMetaData(metaObj);
}
_parseKeyframes (keyframes) {
let times = [], filePositions = [];
const { videoTimeScale, state } = this._store;
for (let i = 1; i < keyframes.times.length; i++) {
times[times.length] = state.timeStampBase + Math.floor(keyframes.times[i] * videoTimeScale);
filePositions[filePositions.length] = keyframes.filepositions[i];
}
return {
times,
filePositions,
};
}
}

View File

@ -0,0 +1,320 @@
import Demuxer from './Demuxer';
import SPSParser from '../SPSParser';
import DataView4Read from '../../utils/DataView4Read';
import { EventTypes } from '../../constants/types';
import Buffer from '../../write/Buffer';
export default class VideoDemuxer extends Demuxer {
constructor (store) {
super(store);
this.CLASS_NAME = this.constructor.name;
this.readOffset = 0;
this.data = new Uint8Array(0);
this.currentTag = null;
this._store.videoMetaData = null;
}
resetStatus () {
this.readOffset = 0;
this.data = new Uint8Array(0);
this.currentTag = null;
}
resolve (tag) {
this.data = tag.body;
this.currentTag = tag;
const firstUI8 = this.readData(1)[0];
const frameType = (firstUI8 & 0xF0) >>> 4;
const codecId = firstUI8 & 0x0F;
if (codecId !== 7) {
/** 1: JPEG
* 2: H263
* 3: Screen video
* 4: On2 VP6
* 5: On2 VP6
* 6: Screen videoversion 2
* 7: AVC
*/
this.error(`unsupported codecId: ${codecId}`);
return;
}
this._parseAVCPacket(frameType);
this.resetStatus();
}
_parseAVCPacket (frameType) {
if (this.unreadLength < 4) {
this.error('Invalid Avc Tag');
}
const isLe = this._store.isLe;
const { buffer } = this.data;
const dv = new DataView(buffer, this.readOffset, this.unreadLength);
const packageType = dv.getUint8(0);
let cpsTime = dv.getUint32(0, !isLe) & 0x00FFFFFF;
cpsTime = (cpsTime << 8) >> 8;
this.readOffset += 4;
switch (packageType) {
case 0: {
const { position, tagSize } = this.currentTag;
this._store.metaEndPosition = position + Buffer.readAsInt(tagSize) + 4; // 缓存scriptTag结束的位置replay使用
this._parseAVCDecoderConfigurationRecord();
break;
}
case 1: {
this._parseAVCVideoData(frameType, cpsTime);
break;
}
case 2: {
break;
}
default: {
// 报错
}
}
}
_parseAVCDecoderConfigurationRecord () {
if (this.unreadLength < 7) {
this.error('Invalid AVCDecoderConfigurationRecord, lack of data!');
return;
}
const { mediaInfo: mi } = this._store;
// stash offset&unreadSize before parsing sps&pps
const tempOffset = this.readOffset;
const tempUnreadLength = this.unreadLength;
const { _store: store } = this;
let meta = this._store.videoMetaData;
let track = this._store.videoTrack;
const dv = new DataView4Read(this.data.buffer, this);
if (meta) {
if (meta.avcc !== undefined) {
this.error('found another AVCDecoderConfigurationRecord!');
}
} else {
if (!store.state._hasVideo && !store.state.hasVideoFlagOverrided) {
store.state._hasVideo = true;
store._mediaInfo.hasVideo = true;
}
meta = store.videoMetaData = {};
meta.type = 'video';
meta.id = track.id;
meta.timeScale = store.videoTimeScale;
meta.duration = store.state.duration;
mi.timescale = store.videoTimeScale;
}
const version = dv.getUint8();
const avcProfile = dv.getUint8();
dv.getUint8();
dv.getUint8();
if (version !== 1 || avcProfile === 0) {
// 处理错误
return;
}
const naluLengthSize = store.state.naluLengthSize = dv.getUint(2, this.readOffset, false) + 1;
if (naluLengthSize !== 3 && naluLengthSize !== 4) {
// 处理错误
return;
}
const spsLength = dv.getUint(5, null, false);
if (spsLength === 0) {
// 处理错误
return;
} else if (spsLength > 1) {
this.warn('AVCDecoderConfigurationRecord: spsLength > 1');
}
let sps;
for (let i = 0; i < spsLength; i++) {
const len = dv.getUint16();
if (len === 0) {
continue;
}
sps = new Uint8Array(this.data.buffer, this.readOffset, len);
this.readOffset += len;
const spsConfig = SPSParser.parseSPS(sps);
if (i !== 0) {
continue;
}
const {
codecSize,
presentSize,
profileString,
levelString,
chromaFormat,
pixelRatio,
frameRate,
refFrames,
bitDepth,
} = spsConfig;
meta.width = codecSize.width;
meta.height = codecSize.height;
meta.presentWidth = presentSize.width;
meta.presentHeight = presentSize.height;
meta.profile = profileString;
meta.level = levelString;
// meta.profileCompatibility = profileCompatibility;
// meta.naluLengthSize = naluLengthSize;
meta.bitDepth = bitDepth;
meta.chromaFormat = chromaFormat;
meta.pixelRatio = pixelRatio;
meta.frameRate = frameRate;
if (!frameRate.fixed
|| frameRate.fpsNum === 0
|| frameRate.fpsDen === 0) {
meta.frameRate = store.referFrameRate;
}
let { fpsDen, fpsNum } = meta.frameRate;
meta.refSampleDuration = meta.timeScale * (fpsDen / fpsNum);
let codecArr = sps.subarray(1, 4);
let codecStr = 'avc1.';
for (let j = 0; j < 3; j++) {
let hex = codecArr[j].toString(16);
hex = hex.padStart(2, '0');
codecStr += hex;
}
meta.codec = codecStr;
const { mediaInfo: mi } = this._store;
mi.width = meta.width;
mi.height = meta.height;
mi.fps = meta.frameRate.fps;
mi.profile = meta.profile;
mi.level = meta.level;
mi.refFrames = refFrames;
mi.pixelRatio = pixelRatio;
mi.videoCodec = codecStr;
mi.chromaFormat = chromaFormat;
if (mi.hasAudio) {
if (mi.audioCodec) {
mi.mimeType = `video/x-flv; codecs="${mi.videoCodec},${mi.audioCodec}"`;
mi.codec = mi.mimeType.replace('x-flv', 'mp4');
}
} else {
mi.mimeType = `video/x-flv; codecs="${mi.videoCodec}"`;
mi.codec = mi.mimeType.replace('x-flv', 'mp4');
}
if (mi.isComplete) {
this.dispatch(EventTypes.MEDIA_INFO_READY, mi);
}
}
let pps;
const ppsCount = dv.getUint8();
if (!ppsCount) {
this.dispatch(EventTypes.ERROR, 'no pps in AVCDecoderConfigurationRecord');
return;
} else if (ppsCount > 1) {
this.warn(`AVCDecoderConfigurationRecord has ppsCount: ${ppsCount}`);
}
for (let i = 0; i < ppsCount; i++) {
let ppsSize = dv.getUint16();
if (!ppsSize) {
continue;
}
pps = new Uint8Array(this.data.buffer, this.readOffset, ppsSize);
this.readOffset += ppsSize;
}
mi.sps = meta.sps = sps;
mi.pps = meta.pps = pps;
if (store.hasInitialMetaDispatched) {
if (store.videoTrack.length || store.audioTrack.length) {
this.dispatch(EventTypes.DATA_READY, store.videoTrack, store.audioTrack);
}
} else {
store.state._videoInitialMetadataDispatched = true;
}
this.dispatch(EventTypes.META_DATA_READY, 'video', meta);
}
_parseAVCVideoData (frameType, cpsTime) {
let dv = new DataView4Read(this.data.buffer, this);
let naluList = [], dataLen = 0;
const { naluLengthSize: naluLenSize } = this._store.state;
let ts = this._store.state.timeStampBase + this.currentTag.getTime();
let isKeyframe = (frameType === 1);
while (this.unreadLength > 0) {
if (this.unreadLength < 4) {
this.warn('not enough data for parsing AVC');
break;
}
const tempReadOffset = this.readOffset;
let naluSize = naluLenSize === 4 ? dv.getUint32() : dv.getUint24();
if (naluSize > this.unreadLength) {
return;
}
let unitType = dv.getUint(5, this.readOffset, false);
if (unitType === 5) {
isKeyframe = true;
}
let data = new Uint8Array(this.data.buffer, tempReadOffset, naluLenSize + naluSize);
this.readOffset = tempReadOffset + naluLenSize + naluSize;
const naluUnit = {
type: unitType,
data,
};
naluList.push(naluUnit);
dataLen += data.byteLength;
}
dv = null;
if (naluList.length) {
const { videoTrack } = this._store;
const videoSample = {
units: naluList,
length: dataLen,
dts: ts,
cps: cpsTime,
pts: (ts + cpsTime),
isKeyframe,
position: isKeyframe ? this.currentTag.position : undefined,
};
videoTrack.samples.push(videoSample);
videoTrack.length += dataLen;
}
}
readData (num) {
const { data, readOffset } = this;
if (this.dataSize > readOffset + num) {
this.readOffset += num;
return data.slice(readOffset, num);
}
return [];
}
get dataSize () {
return this.data.length;
}
get unreadLength () {
return this.dataSize - this.readOffset;
}
}

View File

@ -0,0 +1,596 @@
import Buffer from '../../write/Buffer';
// const UINT32_MAX = Math.pow(2, 32) - 1;
import { cacheWrapper } from '../../utils/funcUtils';
class FMP4 {
static size (value) {
return Buffer.writeUint32(value);
}
static initBox (size, name, ...content) {
const buffer = new Buffer();
buffer.write(FMP4.size(size), FMP4.type(name), ...content);
return buffer.buffer;
}
static extension (version, flag) {
return new Uint8Array([
version,
(flag >> 16) & 0xff,
(flag >> 8) & 0xff,
flag & 0xff,
]);
}
static ftyp () {
return FMP4.initBox(24, 'ftyp', new Uint8Array([
0x69, 0x73, 0x6F, 0x6D, // isom,
0x0, 0x0, 0x00, 0x01, // minor_version: 0x01
0x69, 0x73, 0x6F, 0x6D, // isom
0x61, 0x76, 0x63, 0x31, // avc1
]));
}
static moov (data) {
let size = 8;
let mvhd = FMP4.mvhd(data.duration, data.timescale);
let trak1 = FMP4.videoTrak(data);
let trak2 = FMP4.audioTrak(data);
let mvex = FMP4.mvex(data.duration, data.timescale);
[mvhd, trak1, trak2, mvex].forEach(item=>{
size += item.byteLength;
});
return FMP4.initBox(size, 'moov', mvhd, trak1, trak2, mvex);
}
static mvhd (duration, timeScale) {
let timescale = timeScale || 1000;
// duration *= timescale;
let bytes = new Uint8Array([
0x00, 0x00, 0x00, 0x00, // version(0) + flags 1位的box版本+3位flags box版本0或1一般为0。以下字节数均按version=0
0x00, 0x00, 0x00, 0x00, // creation_time 创建时间 相对于UTC时间1904-01-01零点的秒数
0x00, 0x00, 0x00, 0x00, // modification_time 修改时间
/**
* timescale: 4 bytes文件媒体在1秒时间内的刻度值可以理解为1秒长度
*/
(timescale >>> 24) & 0xFF,
(timescale >>> 16) & 0xFF,
(timescale >>> 8) & 0xFF,
(timescale) & 0xFF,
/**
* duration: 4 bytes该track的时间长度用duration和time scale值可以计算track时长比如audio track的time scale = 8000,
* duration = 560128时长为70.016video track的time scale = 600, duration = 42000时长为70
*/
(duration >>> 24) & 0xFF,
(duration >>> 16) & 0xFF,
(duration >>> 8) & 0xFF,
(duration) & 0xFF,
0x00, 0x01, 0x00, 0x00, // Preferred rate: 1.0 推荐播放速率高16位和低16位分别为小数点整数部分和小数部分即[16.16] 格式该值为1.00x00010000表示正常前向播放
/**
* PreferredVolume(1.0, 2bytes) + reserved(2bytes)
* 与rate类似[8.8] 格式1.00x0100表示最大音量
*/
0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, // reserved: 4 + 4 bytes保留位
0x00, 0x00, 0x00, 0x00,
0x00, 0x01, 0x00, 0x00, // ----begin composition matrix----
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, // 视频变换矩阵 线性代数
0x00, 0x00, 0x00, 0x00,
0x00, 0x01, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x40, 0x00, 0x00, 0x00, // ----end composition matrix----
0x00, 0x00, 0x00, 0x00, // ----begin pre_defined 6 * 4 bytes----
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, // pre-defined 保留位
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, // ----end pre_defined 6 * 4 bytes----
0xFF, 0xFF, 0xFF, 0xFF, // next_track_ID 下一个track使用的id号
]);
return FMP4.initBox(8 + bytes.length, 'mvhd', new Uint8Array(bytes));
}
static videoTrak (data) {
let size = 8;
let tkhd = FMP4.tkhd({
id: 1,
duration: data.duration,
timescale: data.timescale,
width: data.width,
height: data.height,
type: 'video',
});
let mdia = FMP4.mdia({
type: 'video',
timescale: data.timescale,
duration: data.duration,
sps: data.sps,
pps: data.pps,
pixelRatio: data.pixelRatio,
width: data.width,
height: data.height,
});
[tkhd, mdia].forEach(item=>{
size += item.byteLength;
});
return FMP4.initBox(size, 'trak', tkhd, mdia);
}
static audioTrak (data) {
let size = 8;
let tkhd = FMP4.tkhd({
id: 2,
duration: data.duration,
timescale: data.timescale,
width: 0,
height: 0,
type: 'audio',
});
let mdia = FMP4.mdia({
type: 'audio',
timescale: data.timescale,
duration: data.duration,
channelCount: data.audioChannelCount,
samplerate: data.audioSampleRate,
config: data.audioConfig,
});
[tkhd, mdia].forEach(item=>{
size += item.byteLength;
});
return FMP4.initBox(size, 'trak', tkhd, mdia);
}
static tkhd (data) {
let id = data.id,
duration = data.duration,
width = data.width,
height = data.height;
let content = new Uint8Array([
0x00, 0x00, 0x00, 0x07, // version(0) + flags 1位版本 box版本0或1一般为0。以下字节数均按version=0按位或操作结果值预定义如下
// 0x000001 track_enabled否则该track不被播放
// 0x000002 track_in_movie表示该track在播放中被引用
// 0x000004 track_in_preview表示该track在预览时被引用。
// 一般该值为71+2+4 如果一个媒体所有track均未设置track_in_movie和track_in_preview将被理解为所有track均设置了这两项对于hint track该值为0
// hint track 这个特殊的track并不包含媒体数据而是包含了一些将其他数据track打包成流媒体的指示信息。
0x00, 0x00, 0x00, 0x00, // creation_time创建时间相对于UTC时间1904-01-01零点的秒数
0x00, 0x00, 0x00, 0x00, // modification time 修改时间
(id >>> 24) & 0xFF, // track_ID: 4 bytes id号不能重复且不能为0
(id >>> 16) & 0xFF,
(id >>> 8) & 0xFF,
(id) & 0xFF,
0x00, 0x00, 0x00, 0x00, // reserved: 4 bytes 保留位
(duration >>> 24) & 0xFF, // duration: 4 bytes track的时间长度
(duration >>> 16) & 0xFF,
(duration >>> 8) & 0xFF,
(duration) & 0xFF,
0x00, 0x00, 0x00, 0x00, // reserved: 2 * 4 bytes 保留位
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, // layer(2bytes) + alternate_group(2bytes) 视频层默认为0值小的在上层.track分组信息默认为0表示该track未与其他track有群组关系
0x00, 0x00, 0x00, 0x00, // volume(2bytes) + reserved(2bytes) [8.8] 格式如果为音频track1.00x0100表示最大音量否则为0 +保留位
0x00, 0x01, 0x00, 0x00, // ----begin composition matrix----
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x01, 0x00, 0x00, // 视频变换矩阵
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x40, 0x00, 0x00, 0x00, // ----end composition matrix----
(width >>> 8) & 0xFF, // //宽度
(width) & 0xFF,
0x00, 0x00,
(height >>> 8) & 0xFF, // 高度
(height) & 0xFF,
0x00, 0x00,
]);
return FMP4.initBox(8 + content.byteLength, 'tkhd', content);
}
static edts (data) {
let buffer = new Buffer(), duration = data.duration, mediaTime = data.mediaTime;
buffer.write(FMP4.size(36), FMP4.type('edts'));
// elst
buffer.write(FMP4.size(28), FMP4.type('elst'));
buffer.write(new Uint8Array([
0x00, 0x00, 0x00, 0x01, // entry count
(duration >> 24) & 0xff, (duration >> 16) & 0xff, (duration >> 8) & 0xff, duration & 0xff,
(mediaTime >> 24) & 0xff, (mediaTime >> 16) & 0xff, (mediaTime >> 8) & 0xff, mediaTime & 0xff,
0x00, 0x00, 0x00, 0x01, // media rate
]));
return buffer.buffer;
}
static mdia (data) {
let size = 8;
let mdhd = FMP4.mdhd(data.timescale, data.duration);
let hdlr = FMP4.hdlr(data.type);
let minf = FMP4.minf(data);
[mdhd, hdlr, minf].forEach(item=>{
size += item.byteLength;
});
return FMP4.initBox(size, 'mdia', mdhd, hdlr, minf);
}
static mdhd (timescale, duration) {
let content = new Uint8Array([
0x00, 0x00, 0x00, 0x00, // creation_time 创建时间
0x00, 0x00, 0x00, 0x00, // modification_time修改时间
(timescale >>> 24) & 0xFF, // timescale: 4 bytes 文件媒体在1秒时间内的刻度值可以理解为1秒长度
(timescale >>> 16) & 0xFF,
(timescale >>> 8) & 0xFF,
(timescale) & 0xFF,
(duration >>> 24) & 0xFF, // duration: 4 bytes track的时间长度
(duration >>> 16) & 0xFF,
(duration >>> 8) & 0xFF,
(duration) & 0xFF,
0x55, 0xC4, // language: und (undetermined) 媒体语言码。最高位为0后面15位为3个字符见ISO 639-2/T标准中定义
0x00, 0x00, // pre_defined = 0
]);
return FMP4.initBox(12 + content.byteLength, 'mdhd', FMP4.extension(0, 0), content);
}
static hdlr (type) {
let buffer = new Buffer();
let value = [0x00, // version 0
0x00, 0x00, 0x00, // flags
0x00, 0x00, 0x00, 0x00, // pre_defined
0x76, 0x69, 0x64, 0x65, // handler_type: 'vide'
0x00, 0x00, 0x00, 0x00, // reserved
0x00, 0x00, 0x00, 0x00, // reserved
0x00, 0x00, 0x00, 0x00, // reserved
0x56, 0x69, 0x64, 0x65,
0x6f, 0x48, 0x61, 0x6e,
0x64, 0x6c, 0x65, 0x72, 0x00, // name: 'VideoHandler'
];
if (type === 'audio') {
value.splice(8, 4, ...[0x73, 0x6f, 0x75, 0x6e]);
value.splice(24, 13, ...[0x53, 0x6f, 0x75, 0x6e,
0x64, 0x48, 0x61, 0x6e,
0x64, 0x6c, 0x65, 0x72, 0x00]);
}
return FMP4.initBox(8 + value.length, 'hdlr', new Uint8Array(value));
}
static minf (data) {
let buffer = new Buffer(), size = 8;
let vmhd = data.type === 'video' ? FMP4.vmhd() : FMP4.smhd();
let dinf = FMP4.dinf();
let stbl = FMP4.stbl(data);
[vmhd, dinf, stbl].forEach(item=>{
size += item.byteLength;
});
return FMP4.initBox(size, 'minf', vmhd, dinf, stbl);
}
static vmhd () {
return FMP4.initBox(20, 'vmhd', new Uint8Array([
0x00, // version
0x00, 0x00, 0x01, // flags
0x00, 0x00, // graphicsmode
0x00, 0x00,
0x00, 0x00,
0x00, 0x00, // opcolor
]));
}
static smhd () {
return FMP4.initBox(16, 'smhd', new Uint8Array([
0x00, // version
0x00, 0x00, 0x00, // flags
0x00, 0x00, // balance
0x00, 0x00, // reserved
]));
}
static dinf () {
let buffer = new Buffer();
let dref = [0x00, // version 0
0x00, 0x00, 0x00, // flags
0x00, 0x00, 0x00, 0x01, // entry_count
0x00, 0x00, 0x00, 0x0c, // entry_size
0x75, 0x72, 0x6c, 0x20, // 'url' type
0x00, // version 0
0x00, 0x00, 0x01, // entry_flags
];
buffer.write(FMP4.size(36), FMP4.type('dinf'), FMP4.size(28), FMP4.type('dref'), new Uint8Array(dref));
return buffer.buffer;
}
static stbl (data) {
let size = 8;
let stsd = FMP4.stsd(data);
let stts = FMP4.stts();
let stsc = FMP4.stsc();
let stsz = FMP4.stsz();
let stco = FMP4.stco();
[stsd, stts, stsc, stsz, stco].forEach(item=>{
size += item.byteLength;
});
return FMP4.initBox(size, 'stbl', stsd, stts, stsc, stsz, stco);
}
static stsd (data) {
let content;
if (data.type === 'audio') {
// if (!data.isAAC && data.codec === 'mp4') {
// content = FMP4.mp3(data);
// } else {
//
// }
// 支持mp4a
content = FMP4.mp4a(data);
} else {
content = FMP4.avc1(data);
}
return FMP4.initBox(16 + content.byteLength, 'stsd', FMP4.extension(0, 0), new Uint8Array([0x00, 0x00, 0x00, 0x01]), content);
}
static mp4a (data) {
let buffer = new Buffer();
let content = new Uint8Array([
0x00, 0x00, 0x00, // reserved
0x00, 0x00, 0x00, // reserved
0x00, 0x01, // data_reference_index
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, // reserved
0x00, data.channelCount, // channelcount
0x00, 0x10, // sampleSize:16bits
0x00, 0x00, 0x00, 0x00, // reserved2
(data.samplerate >> 8) & 0xff,
data.samplerate & 0xff, //
0x00, 0x00,
]);
let esds = FMP4.esds(data.config);
return FMP4.initBox(8 + content.byteLength + esds.byteLength, 'mp4a', content, esds);
}
static esds (config = [43, 146, 8, 0]) {
const configlen = config.length;
let buffer = new Buffer();
let content = new Uint8Array([
0x00, // version 0
0x00, 0x00, 0x00, // flags
0x03, // descriptor_type
0x17 + configlen, // length
0x00, 0x01, // es_id
0x00, // stream_priority
0x04, // descriptor_type
0x0f + configlen, // length
0x40, // codec : mpeg4_audio
0x15, // stream_type
0x00, 0x00, 0x00, // buffer_size
0x00, 0x00, 0x00, 0x00, // maxBitrate
0x00, 0x00, 0x00, 0x00, // avgBitrate
0x05, // descriptor_type
].concat([configlen]).concat(config).concat([0x06, 0x01, 0x02]));
buffer.write(FMP4.size(8 + content.byteLength), FMP4.type('esds'), content);
return buffer.buffer;
}
static avc1 (data) {
let buffer = new Buffer(), size = 40;// 8(avc1)+8(avcc)+8(btrt)+16(pasp)
let sps = data.sps, pps = data.pps, width = data.width, height = data.height, hSpacing = data.pixelRatio[0], vSpacing = data.pixelRatio[1];
let avccBuffer = new Buffer();
avccBuffer.write(new Uint8Array([
0x01, // version
sps[1], // profile
sps[2], // profile compatible
sps[3], // level
0xfc | 3,
0xE0 | 1, // 目前只处理一个sps
].concat([sps.length >>> 8 & 0xff, sps.length & 0xff])));
avccBuffer.write(sps, new Uint8Array([1, pps.length >>> 8 & 0xff, pps.length & 0xff]), pps);
let avcc = avccBuffer.buffer;
let avc1 = new Uint8Array([
0x00, 0x00, 0x00, // reserved
0x00, 0x00, 0x00, // reserved
0x00, 0x01, // data_reference_index
0x00, 0x00, // pre_defined
0x00, 0x00, // reserved
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, // pre_defined
(width >> 8) & 0xff,
width & 0xff, // width
(height >> 8) & 0xff,
height & 0xff, // height
0x00, 0x48, 0x00, 0x00, // horizresolution
0x00, 0x48, 0x00, 0x00, // vertresolution
0x00, 0x00, 0x00, 0x00, // reserved
0x00, 0x01, // frame_count
0x12,
0x64, 0x61, 0x69, 0x6C, // dailymotion/hls.js
0x79, 0x6D, 0x6F, 0x74,
0x69, 0x6F, 0x6E, 0x2F,
0x68, 0x6C, 0x73, 0x2E,
0x6A, 0x73, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, // compressorname
0x00, 0x18, // depth = 24
0x11, 0x11]); // pre_defined = -1
let btrt = new Uint8Array([
0x00, 0x1c, 0x9c, 0x80, // bufferSizeDB
0x00, 0x2d, 0xc6, 0xc0, // maxBitrate
0x00, 0x2d, 0xc6, 0xc0, // avgBitrate
]);
let pasp = new Uint8Array([
(hSpacing >> 24), // hSpacing
(hSpacing >> 16) & 0xff,
(hSpacing >> 8) & 0xff,
hSpacing & 0xff,
(vSpacing >> 24), // vSpacing
(vSpacing >> 16) & 0xff,
(vSpacing >> 8) & 0xff,
vSpacing & 0xff,
]);
buffer.write(
FMP4.size(size + avc1.byteLength + avcc.byteLength + btrt.byteLength), FMP4.type('avc1'), avc1,
FMP4.size(8 + avcc.byteLength), FMP4.type('avcC'), avcc,
FMP4.size(20), FMP4.type('btrt'), btrt,
FMP4.size(16), FMP4.type('pasp'), pasp
);
return buffer.buffer;
}
static stts () {
let content = new Uint8Array([
0x00, // version
0x00, 0x00, 0x00, // flags
0x00, 0x00, 0x00, 0x00, // entry_count
]);
return FMP4.initBox(16, 'stts', content);
}
static stsc () {
let content = new Uint8Array([
0x00, // version
0x00, 0x00, 0x00, // flags
0x00, 0x00, 0x00, 0x00, // entry_count
]);
return FMP4.initBox(16, 'stsc', content);
}
static stco () {
let content = new Uint8Array([
0x00, // version
0x00, 0x00, 0x00, // flags
0x00, 0x00, 0x00, 0x00, // entry_count
]);
return FMP4.initBox(16, 'stco', content);
}
static stsz () {
let content = new Uint8Array([
0x00, // version
0x00, 0x00, 0x00, // flags
0x00, 0x00, 0x00, 0x00, // sample_size
0x00, 0x00, 0x00, 0x00, // sample_count
]);
return FMP4.initBox(20, 'stsz', content);
}
static mvex (duration) {
let buffer = new Buffer();
let mehd = Buffer.writeUint32(duration);
buffer.write(FMP4.size(88), FMP4.type('mvex'), FMP4.size(16), FMP4.type('mehd'), FMP4.extension(0, 0), mehd, FMP4.trex(1), FMP4.trex(2));
return buffer.buffer;
}
static trex (id) {
let content = new Uint8Array([
0x00, // version 0
0x00, 0x00, 0x00, // flags
(id >> 24),
(id >> 16) & 0xff,
(id >> 8) & 0xff,
(id & 0xff), // track_ID
0x00, 0x00, 0x00, 0x01, // default_sample_description_index
0x00, 0x00, 0x00, 0x00, // default_sample_duration
0x00, 0x00, 0x00, 0x00, // default_sample_size
0x00, 0x01, 0x00, 0x01, // default_sample_flags
]);
return FMP4.initBox(8 + content.byteLength, 'trex', content);
}
static moof (data) {
let size = 8;
let mfhd = FMP4.mfhd();
let traf = FMP4.traf(data);
[mfhd, traf].forEach(item=>{
size += item.byteLength;
});
return FMP4.initBox(size, 'moof', mfhd, traf);
}
static mfhd () {
let content = Buffer.writeUint32(FMP4.sequence);
FMP4.sequence += 1;
return FMP4.initBox(16, 'mfhd', FMP4.extension(0, 0), content);
}
static traf (data) {
let size = 8;
let tfhd = FMP4.tfhd(data.id);
let tfdt = FMP4.tfdt(data.time);
let sdtp = FMP4.sdtp(data);
let trun = FMP4.trun(data, sdtp.byteLength);
[tfhd, tfdt, sdtp, trun].forEach(item=>{
size += item.byteLength;
});
return FMP4.initBox(size, 'traf', tfhd, tfdt, sdtp, trun);
}
static tfhd (id) {
let content = Buffer.writeUint32(id);
return FMP4.initBox(16, 'tfhd', FMP4.extension(0, 0), content);
}
static tfdt (time) {
// let upper = Math.floor(time / (UINT32_MAX + 1)),
// lower = Math.floor(time % (UINT32_MAX + 1));
return FMP4.initBox(16, 'tfdt', FMP4.extension(0, 0), Buffer.writeUint32(time));
}
static trun (data, sdtpLength) {
// let id = data.id;
// let ceil = id === 1 ? 16 : 12;
let buffer = new Buffer();
let sampleCount = Buffer.writeUint32(data.samples.length);
// mdat-header 8
// moof-header 8
// mfhd 16
// traf-header 8
// thhd 16
// tfdt 20
// trun-header 12
// sampleCount 4
// data-offset 4
// samples.length
let offset = Buffer.writeUint32(8 + 8 + 16 + 8 + 16 + 16 + 12 + 4 + 4 + 16 * data.samples.length + sdtpLength);
buffer.write(FMP4.size(20 + 16 * data.samples.length), FMP4.type('trun'), new Uint8Array([0x00, 0x00, 0x0F, 0x01]), sampleCount, offset);
let size = buffer.buffer.byteLength, writeOffset = 0;
data.samples.forEach(() => {
size += 16;
});
let trunBox = new Uint8Array(size);
trunBox.set(buffer.buffer, 0);
writeOffset += buffer.buffer.byteLength;
data.samples.forEach((item)=>{
trunBox.set(Buffer.writeUint32(item.duration), writeOffset);
writeOffset += 4;
trunBox.set(Buffer.writeUint32(item.size), writeOffset);
writeOffset += 4;
if (data.id === 1) {
trunBox.set(Buffer.writeUint32(item.isKeyframe ? 0x02000000 : 0x01010000), writeOffset);
writeOffset += 4;
trunBox.set(Buffer.writeUint32(item.cps), writeOffset);
writeOffset += 4;
} else {
trunBox.set(Buffer.writeUint32(0x01000000), writeOffset);
writeOffset += 4;
trunBox.set(Buffer.writeUint32(0), writeOffset);
writeOffset += 4;
}
// buffer.write(Buffer.writeUint32(0));
});
return trunBox;
}
static sdtp (data) {
let buffer = new Buffer();
buffer.write(FMP4.size(12 + data.samples.length), FMP4.type('sdtp'), FMP4.extension(0, 0));
data.samples.forEach(item=>{
buffer.write(new Uint8Array(data.id === 1 ? [item.key ? 32 : 16] : [16]));
});
return buffer.buffer;
}
static mdat (data) {
let buffer = new Buffer(), size = 8;
data.samples.forEach(item=>{
size += item.size;
});
buffer.write(FMP4.size(size), FMP4.type('mdat'));
let mdatBox = new Uint8Array(size);
let offset = 0;
mdatBox.set(buffer.buffer, offset);
offset += 8;
data.samples.forEach(item=>{
item.buffer.forEach((unit) => {
mdatBox.set(unit.data, offset);
offset += unit.data.byteLength;
// buffer.write(unit.data);
});
});
return mdatBox;
}
}
FMP4.type = cacheWrapper((name) => {
return new Uint8Array([name.charCodeAt(0), name.charCodeAt(1), name.charCodeAt(2), name.charCodeAt(3)]);
});
FMP4.sequence = 1;
export default FMP4;

View File

@ -0,0 +1,422 @@
import MediaSegmentList from '../../models/MediaSegmentList';
import MediaSegment from '../../models/MediaSegment';
import MediaSample from '../../models/MediaSample';
import sniffer from '../../utils/sniffer';
import Buffer from '../../write/Buffer';
import FMP4 from './Fmp4';
import emitter from '../../utils/EventEmitter';
export default class Mp4Remuxer {
constructor () {
this._dtsBase = 0;
this.isLive = false;
this._isDtsBaseInited = false;
this._videoMeta = null;
this._audioMeta = null;
this._audioNextDts = null;
this._videoNextDts = null;
this._videoSegmentList = new MediaSegmentList('video');
this._audioSegmentList = new MediaSegmentList('audio');
const { browser } = sniffer;
this._emitter = emitter;
this._fillSilenceFrame = browser === 'ie';
}
destroy () {
this._dtsBase = -1;
this._dtsBaseInited = false;
this._audioMeta = null;
this._videoMeta = null;
this._videoSegmentList.clear();
this._audioSegmentList.clear();
this._videoSegmentList = null;
this._audioSegmentList = null;
}
remux (audioTrack, videoTrack) {
!this._isDtsBaseInited && this.calcDtsBase(audioTrack, videoTrack);
this._remuxVideo(videoTrack);
this._remuxAudio(audioTrack);
}
seek () {
this._videoNextDts = null;
this._audioNextDts = null;
this._videoSegmentList.clear();
this._audioSegmentList.clear();
}
onMetaDataReady (type, meta) {
this[`_${type}Meta`] = meta;
}
onMediaInfoReady (mediaInfo) {
let ftyp_moov = new Buffer();
let ftyp = FMP4.ftyp();
let moov = FMP4.moov(mediaInfo);
ftyp_moov.write(ftyp, moov);
return ftyp_moov.buffer;
}
calcDtsBase (audioTrack, videoTrack) {
let audioBase = Infinity, videoBase = Infinity;
if (audioTrack.samples && audioTrack.samples.length) {
audioBase = audioTrack.samples[0].dts;
}
if (videoTrack.samples && videoTrack.samples.length) {
videoBase = videoTrack.samples[0].dts;
}
this._dtsBase = Math.min(audioBase, videoBase);
this._isDtsBaseInited = true;
}
_remuxVideo (videoTrack) {
if (!this._videoMeta) {
return;
}
const track = videoTrack;
if (!videoTrack.samples || !videoTrack.samples.length) {
return;
}
let { samples } = track, dtsCorrection,
firstDts = -1,
lastDts = -1,
firstPts = -1,
lastPts = -1;
const mp4Samples = [];
const mdatBox = {
samples: [],
};
const videoSegment = new MediaSegment();
while (samples.length) {
const avcSample = samples.shift();
const { isKeyframe, cps } = avcSample;
let dts = avcSample.dts - this._dtsBase;
if (dtsCorrection === undefined) {
if (!this._videoNextDts) {
const lastSegment = this._videoSegmentList.getLastSegmentBefore(dts);
if (lastSegment) {
let gap;
const { lastDts, gap: lastGap } = lastSegment;
gap = dts - (lastDts + lastGap) > 3 ? dts - (lastDts + lastGap) : 0;
dtsCorrection = dts - (lastDts + gap);
} else {
dtsCorrection = 0;
}
} else {
dtsCorrection = dts - this._videoNextDts;
}
}
const originDts = dts;
dts -= dtsCorrection;
const pts = dts + cps;
if (firstDts === -1) {
firstDts = dts;
firstPts = pts;
}
let _units = [];
while (avcSample.units.length) {
let mdatSample = {
buffer: [],
size: 0,
};
const unit = avcSample.units.shift();
_units.push(unit);
mdatSample.buffer.push(unit);
mdatSample.size += unit.data.byteLength;
mdatBox.samples.push(mdatSample);
}
let sampleDuration = 0;
if (samples.length >= 1) {
const nextDts = samples[0].dts - this._dtsBase - dtsCorrection;
sampleDuration = nextDts - dts;
} else {
if (mp4Samples.length >= 1) { // lastest sample, use second last duration
sampleDuration = mp4Samples[mp4Samples.length - 1].duration;
} else { // the only one sample, use reference duration
sampleDuration = this._videoMeta.refSampleDuration;
}
}
if (isKeyframe) {
const rap = new MediaSample({
dts,
pts,
duration: sampleDuration,
originDts: avcSample.dts,
position: avcSample.position,
isRAP: true,
});
videoSegment.addRAP(rap);
}
mp4Samples.push({
dts,
cps,
pts,
units: _units,
size: avcSample.length,
isKeyframe,
duration: sampleDuration,
originDts,
});
}
const first = mp4Samples[0],
last = mp4Samples[mp4Samples.length - 1];
lastDts = last.dts + last.duration;
lastPts = last.pts + last.duration;
this._videoNextDts = lastDts;
videoSegment.startDts = firstDts;
videoSegment.endDts = lastDts;
videoSegment.startPts = firstPts;
videoSegment.endPts = lastPts;
videoSegment.originStartDts = first.originDts;
videoSegment.originEndDts = last.originDts + last.duration;
videoSegment.gap = dtsCorrection;
const firstSample = new MediaSample({
dts: first.dts,
pts: first.pts,
duration: first.duration,
isKeyframe: first.isKeyframe,
originDts: first.originDts,
});
const lastSample = new MediaSample({
dts: last.dts,
pts: last.pts,
duration: last.duration,
isKeyframe: last.isKeyframe,
originDts: last.originDts,
});
videoSegment.firstSample = firstSample;
videoSegment.lastSample = lastSample;
let moof_mdat = new Buffer();
track.samples = mp4Samples;
track.time = firstDts;
const moof = FMP4.moof(track);
const mdat = FMP4.mdat(mdatBox);
moof_mdat.write(moof, mdat);
if (!this.isLive) {
this._videoSegmentList.append(videoSegment);
}
track.samples = [];
track.length = 0;
this._emitter.emit('mediaFragment', {
type: 'video',
data: moof_mdat.buffer.buffer,
sampleCount: mp4Samples.length,
fragment: videoSegment,
});
}
_remuxAudio (track) {
if (!this._audioMeta) {
return;
}
const { samples } = track;
let dtsCorrection,
firstDts = -1,
lastDts = -1,
firstPts = -1,
lastPts = -1,
silentDuration,
mp4Samples = [];
const mdatBox = {
samples: [],
};
if (!samples || !samples.length) {
return;
}
let isFirstDtsInited = false;
while (samples.length) {
let sample = samples.shift();
const { unit } = sample;
let dts = sample.dts - this._dtsBase;
let needSilentFrame = false;
if (dtsCorrection === undefined) {
if (!this._audioNextDts) {
const lastSegment = this._audioSegmentList.getLastSegmentBefore(dts);
if (lastSegment) {
let gap;
const { lastDts, gap: lastGap } = lastSegment;
gap = dts - (lastDts + lastGap) > 3 ? dts - (lastDts + lastGap) : 0;
dtsCorrection = dts - (lastDts + gap);
} else {
needSilentFrame = this._fillSilenceFrame && !this._videoSegmentList.isEmpty();
dtsCorrection = 0;
}
} else {
dtsCorrection = dts - this._audioNextDts;
}
}
const originDts = dts;
dts -= dtsCorrection;
if (needSilentFrame) {
const videoSegment = this._videoSegmentList.getLastSampleBefore(originDts);
if (videoSegment && videoSegment.startDts < dts) {
silentDuration = dts - videoSegment.startDts;
dts = videoSegment.startDts;
} else {
needSilentFrame = false;
}
}
if (!isFirstDtsInited) {
firstDts = dts;
isFirstDtsInited = true;
}
if (needSilentFrame) {
samples.unshift(sample);
const silentFrame = this.initSilentAudio(dts, silentDuration);
mp4Samples.push(silentFrame);
let mdatSample = {
buffer: [],
size: 0,
};
mdatSample.buffer.push({
data: silentFrame.unit,
});
mdatSample.size += silentFrame.unit.byteLength;
mdatBox.samples.push(mdatSample);
continue;
}
let sampleDuration = 0;
if (samples.length >= 1) {
const nextDts = samples[0].dts - this._dtsBase - dtsCorrection;
sampleDuration = nextDts - dts;
} else {
if (mp4Samples.length >= 1) { // use second last sample duration
sampleDuration = mp4Samples[mp4Samples.length - 1].duration;
} else { // the only one sample, use reference sample duration
sampleDuration = this._audioMeta.refSampleDuration;
}
}
const mp4Sample = {
dts,
pts: dts,
cts: 0,
size: unit.byteLength,
duration: sampleDuration,
originDts,
};
let mdatSample = {
buffer: [],
size: 0,
};
mdatSample.buffer.push({
data: unit,
});
mdatSample.size += unit.byteLength;
mdatBox.samples.push(mdatSample);
mp4Samples.push(mp4Sample);
}
const last = mp4Samples[mp4Samples.length - 1];
lastDts = last.dts + last.duration;
this._audioNextDts = lastDts;
const audioSegment = new MediaSegment();
audioSegment.startDts = firstDts;
audioSegment.endDts = lastDts;
audioSegment.startPts = firstDts;
audioSegment.endPts = lastDts;
audioSegment.originStartDts = mp4Samples[0].originDts;
audioSegment.originEndDts = last.originDts + last.duration;
audioSegment.gap = dtsCorrection;
audioSegment.firstSample = new MediaSample({
dts: mp4Samples[0].dts,
pts: mp4Samples[0].pts,
duration: mp4Samples[0].duration,
originDts: mp4Samples[0].originDts,
});
audioSegment.lastSample = new MediaSample({
dts: last.dts,
pts: last.pts,
duration: last.duration,
originDts: last.originDts,
});
track.samples = mp4Samples;
const ftyp_moof = new Buffer();
track.time = firstDts;
const moof = FMP4.moof(track, firstDts);
const mdat = FMP4.mdat(mdatBox);
ftyp_moof.write(moof, mdat);
if (!this.isLive) {
this._audioSegmentList.append(audioSegment);
}
track.samples = [];
track.length = 0;
this._emitter.emit('mediaFragment', {
type: 'audio',
data: ftyp_moof.buffer.buffer,
sampleCount: mp4Samples.length,
fragment: audioSegment,
});
}
initSilentAudio (dts, duration) {
const unit = Mp4Remuxer.getSilentFrame(this._audioMeta.channelCount);
return {
dts,
pts: dts,
cps: 0,
duration,
unit,
size: unit.byteLength,
originDts: dts,
};
}
static getSilentFrame (channelCount) {
if (channelCount === 1) {
return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x23, 0x80]);
} else if (channelCount === 2) {
return new Uint8Array([0x21, 0x00, 0x49, 0x90, 0x02, 0x19, 0x00, 0x23, 0x80]);
} else if (channelCount === 3) {
return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x20, 0x84, 0x01, 0x26, 0x40, 0x08, 0x64, 0x00, 0x8e]);
} else if (channelCount === 4) {
return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x20, 0x84, 0x01, 0x26, 0x40, 0x08, 0x64, 0x00, 0x80, 0x2c, 0x80, 0x08, 0x02, 0x38]);
} else if (channelCount === 5) {
return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x20, 0x84, 0x01, 0x26, 0x40, 0x08, 0x64, 0x00, 0x82, 0x30, 0x04, 0x99, 0x00, 0x21, 0x90, 0x02, 0x38]);
} else if (channelCount === 6) {
return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x20, 0x84, 0x01, 0x26, 0x40, 0x08, 0x64, 0x00, 0x82, 0x30, 0x04, 0x99, 0x00, 0x21, 0x90, 0x02, 0x00, 0xb2, 0x00, 0x20, 0x08, 0xe0]);
}
return null;
}
}

View File

@ -0,0 +1,72 @@
export default class DataView4Read {
constructor (buffer, context) {
this._dv = new DataView(buffer);
this._context = context;
this.initProxy();
}
initProxy () {
const sizeArr = [8, 16, 32];
const self = this;
const { _store } = this._context;
sizeArr.forEach(size => {
this[`getUint${size}`] = function (offset) {
if (!offset) { offset = self._context.readOffset; }
if (offset === self._context.readOffset) {
self._context.readOffset += size / 8;
}
return self._dv[`getUint${size}`](offset, !_store.isLe);
};
});
/**
* 显式声明一个比其它位数更常用读取24位整数方法
* @param offset
* @param isHigh
*/
this.getUint24 = function (offset) {
const result = this.getUint(24, offset, false); // 会读取Uint32,做 and 操作之后回退一位。
self._context.readOffset -= 1;
return result;
};
this.getUint = function (size, offset, isHigh = true) {
if (size > 32) {
throw 'not supported read size';
}
let readSize = 32;
if (!this[`getUint${size}`]) {
for (let i = 0, len = sizeArr.length; i < len; i++) {
if (size < sizeArr[i]) {
readSize = sizeArr[i];
break;
}
}
const numToAnd = isHigh ? DataView4Read.getAndNum(0, size - 1, readSize) : DataView4Read.getAndNum(readSize - size, readSize - 1, readSize);
return self[`getUint${readSize}`](offset, !_store.isLe) & numToAnd;
} else {
return self[`getUint${readSize}`](offset, !_store.isLe);
}
};
}
static getAndNum (begin, end, size = 8) {
let result = 0;
let index = --size;
while (index > 0) {
if (index > end || index < begin) {
index--;
continue;
} else {
result += Math.pow(2, size - index);
index--;
}
}
return result;
}
}

View File

@ -0,0 +1,3 @@
const events = require('events');
export default new events.EventEmitter();

View File

@ -0,0 +1,398 @@
import Logger from './Log';
export default class ExpGolomb {
constructor (data) {
this.data = data;
// the number of bytes left to examine in this.data
this.bytesAvailable = data.byteLength;
// the current word being examined
this.word = 0; // :uint
// the number of bits left to examine in the current word
this.bitsAvailable = 0; // :uint
}
// ():void
loadWord () {
let data = this.data,
bytesAvailable = this.bytesAvailable,
position = data.byteLength - bytesAvailable,
workingBytes = new Uint8Array(4),
availableBytes = Math.min(4, bytesAvailable);
if (availableBytes === 0) {
throw new Error('no bytes available');
}
workingBytes.set(data.subarray(position, position + availableBytes));
this.word = new DataView(workingBytes.buffer).getUint32(0);
// track the amount of this.data that has been processed
this.bitsAvailable = availableBytes * 8;
this.bytesAvailable -= availableBytes;
}
// (count:int):void
skipBits (count) {
var skipBytes; // :int
if (this.bitsAvailable > count) {
this.word <<= count;
this.bitsAvailable -= count;
} else {
count -= this.bitsAvailable;
skipBytes = count >> 3;
count -= (skipBytes >> 3);
this.bytesAvailable -= skipBytes;
this.loadWord();
this.word <<= count;
this.bitsAvailable -= count;
}
return skipBytes;
}
// (size:int):uint
readBits (size) {
let bits = Math.min(this.bitsAvailable, size), // :uint
valu = this.word >>> (32 - bits); // :uint
if (size > 32) {
Logger.error('Cannot read more than 32 bits at a time');
}
this.bitsAvailable -= bits;
if (this.bitsAvailable > 0) {
this.word <<= bits;
} else if (this.bytesAvailable > 0) {
this.loadWord();
}
bits = size - bits;
if (bits > 0 && this.bitsAvailable) {
return valu << bits | this.readBits(bits);
} else {
return valu;
}
}
// ():uint
skipLZ () {
var leadingZeroCount; // :uint
for (leadingZeroCount = 0; leadingZeroCount < this.bitsAvailable; ++leadingZeroCount) {
if (0 !== (this.word & (0x80000000 >>> leadingZeroCount))) {
// the first bit of working word is 1
this.word <<= leadingZeroCount;
this.bitsAvailable -= leadingZeroCount;
return leadingZeroCount;
}
}
// we exhausted word and still have not found a 1
this.loadWord();
return leadingZeroCount + this.skipLZ();
}
// ():void
skipUEG () {
this.skipBits(1 + this.skipLZ());
}
// ():void
skipEG () {
this.skipBits(1 + this.skipLZ());
}
// ():uint
readUEG () {
var clz = this.skipLZ(); // :uint
return this.readBits(clz + 1) - 1;
}
// ():int
readEG () {
var valu = this.readUEG(); // :int
if (0x01 & valu) {
// the number is odd if the low order bit is set
return (1 + valu) >>> 1; // add 1 to make it even, and divide by 2
} else {
return -1 * (valu >>> 1); // divide by two then make it negative
}
}
// Some convenience functions
// :Boolean
readBoolean () {
return 1 === this.readBits(1);
}
// ():int
readUByte () {
return this.readBits(8);
}
// ():int
readUShort () {
return this.readBits(16);
}
// ():int
readUInt () {
return this.readBits(32);
}
/**
* Advance the ExpGolomb decoder past a scaling list. The scaling
* list is optionally transmitted as part of a sequence parameter
* set and is not relevant to transmuxing.
* @param count {number} the number of entries in this scaling list
* @see Recommendation ITU-T H.264, Section 7.3.2.1.1.1
*/
skipScalingList (count) {
var lastScale = 8,
nextScale = 8,
j,
deltaScale;
for (j = 0; j < count; j++) {
if (nextScale !== 0) {
deltaScale = this.readEG();
nextScale = (lastScale + deltaScale + 256) % 256;
}
lastScale = (nextScale === 0)
? lastScale
: nextScale;
}
}
/**
* Read a sequence parameter set and return some interesting video
* properties. A sequence parameter set is the H264 metadata that
* describes the properties of upcoming video frames.
* @param data {Uint8Array} the bytes of a sequence parameter set
* @return {object} an object with configuration parsed from the
* sequence parameter set, including the dimensions of the
* associated video frames.
*/
readSPS () {
var frameCropLeftOffset = 0,
frameCropRightOffset = 0,
frameCropTopOffset = 0,
frameCropBottomOffset = 0,
profileIdc,
// profileCompat,
levelIdc,
codecWidth,
codecHeight,
presentWidth,
numRefFramesInPicOrderCntCycle,
picWidthInMbsMinus1,
picHeightInMapUnitsMinus1,
frameMbsOnlyFlag,
scalingListCount,
i,
readUByte = this.readUByte.bind(this),
readBits = this.readBits.bind(this),
readUEG = this.readUEG.bind(this),
readBoolean = this.readBoolean.bind(this),
skipBits = this.skipBits.bind(this),
skipEG = this.skipEG.bind(this),
skipUEG = this.skipUEG.bind(this),
skipScalingList = this.skipScalingList.bind(this);
readUByte();
profileIdc = readUByte(); // profile_idc
readBits(5); // profileCompat constraint_set[0-4]_flag, u(5)
skipBits(3); // reserved_zero_3bits u(3),
levelIdc = readUByte(); // level_idc u(8)
skipUEG(); // seq_parameter_set_id
let chromaFormatIdc = 1;
let chromaFormat = 420;
let chromaFormats = [0, 420, 422, 444];
let bitDepthLuma = 8;
const profileIdcs = [100, 110, 122, 244, 44, 83, 86, 118, 128];
// some profiles have more optional data we don't need
if (profileIdcs.includes(profileIdc)) {
chromaFormatIdc = readUEG();
if (chromaFormatIdc === 3) {
skipBits(1); // separate_colour_plane_flag
}
if (chromaFormatIdc <= 3) {
chromaFormat = chromaFormats[chromaFormatIdc];
}
bitDepthLuma = readUEG() + 8; // bit_depth_luma_minus8
skipUEG(); // bit_depth_chroma_minus8
skipBits(1); // qpprime_y_zero_transform_bypass_flag
if (readBoolean()) { // seq_scaling_matrix_present_flag
scalingListCount = (chromaFormatIdc !== 3)
? 8
: 12;
for (i = 0; i < scalingListCount; i++) {
if (readBoolean()) { // seq_scaling_list_present_flag[ i ]
i < 6 ? skipScalingList(16) : skipScalingList(64);
}
}
}
}
skipUEG(); // log2_max_frame_num_minus4
var picOrderCntType = readUEG();
if (picOrderCntType === 0) {
readUEG(); // log2_max_pic_order_cnt_lsb_minus4
} else if (picOrderCntType === 1) {
skipBits(1); // delta_pic_order_always_zero_flag
skipEG(); // offset_for_non_ref_pic
skipEG(); // offset_for_top_to_bottom_field
numRefFramesInPicOrderCntCycle = readUEG();
for (i = 0; i < numRefFramesInPicOrderCntCycle; i++) {
skipEG(); // offset_for_ref_frame[ i ]
}
}
let refFrames = readUEG(); // max_num_ref_frames
skipBits(1); // gaps_in_frame_num_value_allowed_flag
picWidthInMbsMinus1 = readUEG();
picHeightInMapUnitsMinus1 = readUEG();
frameMbsOnlyFlag = readBits(1);
if (frameMbsOnlyFlag === 0) {
skipBits(1); // mb_adaptive_frame_field_flag
}
skipBits(1); // direct_8x8_inference_flag
if (readBoolean()) { // frame_cropping_flag
frameCropLeftOffset = readUEG();
frameCropRightOffset = readUEG();
frameCropTopOffset = readUEG();
frameCropBottomOffset = readUEG();
}
let frameRate = {
fps: 0,
fpsFixed: true,
fpsNum: 0,
fpsDen: 0,
};
let pixelRatio = [1, 1];
if (readBoolean()) {
// vui_parameters_present_flag
if (readBoolean()) {
// aspect_ratio_info_present_flag
const aspectRatioIdc = readUByte();
switch (aspectRatioIdc) {
case 1:
pixelRatio = [1, 1];
break;
case 2:
pixelRatio = [12, 11];
break;
case 3:
pixelRatio = [10, 11];
break;
case 4:
pixelRatio = [16, 11];
break;
case 5:
pixelRatio = [40, 33];
break;
case 6:
pixelRatio = [24, 11];
break;
case 7:
pixelRatio = [20, 11];
break;
case 8:
pixelRatio = [32, 11];
break;
case 9:
pixelRatio = [80, 33];
break;
case 10:
pixelRatio = [18, 11];
break;
case 11:
pixelRatio = [15, 11];
break;
case 12:
pixelRatio = [64, 33];
break;
case 13:
pixelRatio = [160, 99];
break;
case 14:
pixelRatio = [4, 3];
break;
case 15:
pixelRatio = [3, 2];
break;
case 16:
pixelRatio = [2, 1];
break;
case 255:
{
pixelRatio = [
readUByte() << 8 | readUByte(),
readUByte() << 8 | readUByte(),
];
break;
}
}
}
if (readBoolean()) { // overscan_info_present_flag
readBoolean(); // overscan_appropriate_flag
}
if (readBoolean()) { // video_signal_type_present_flag
readBits(4); // video_format & video_full_range_flag
if (readBoolean()) { // colour_description_present_flag
readBits(24); // colour_primaries & transfer_characteristics & matrix_coefficients
}
}
if (readBoolean()) { // chroma_loc_info_present_flag
readUEG(); // chroma_sample_loc_type_top_field
readUEG(); // chroma_sample_loc_type_bottom_field
}
if (readBoolean()) { // timing_info_present_flag
const numUnitInTick = (readBits(32));
frameRate.fpsNum = (readBits(32));
frameRate.fixed = this.readBoolean();
frameRate.fpsDen = numUnitInTick * 2;
frameRate.fps = frameRate.fpsNum / frameRate.fpsDen;
}
let cropUnitX = 0, cropUnitY = 0;
if (chromaFormatIdc === 0) {
cropUnitX = 1;
cropUnitX = 2 - frameMbsOnlyFlag;
} else {
let subWc = chromaFormatIdc === 3 ? 1 : 2;
let subHc = chromaFormatIdc === 1 ? 2 : 1;
cropUnitX = subWc;
cropUnitY = subHc * (2 - frameMbsOnlyFlag);
}
codecWidth = (picWidthInMbsMinus1 + 1) * 16;
codecHeight = (2 - frameMbsOnlyFlag) * ((picHeightInMapUnitsMinus1 + 1) * 16);
codecWidth -= (frameCropLeftOffset + frameCropRightOffset) * cropUnitX;
codecHeight -= (frameCropTopOffset + frameCropBottomOffset) * cropUnitY;
const pixelScale = pixelRatio[0] === 1 || pixelRatio[1] === 1
? 1
: pixelRatio[0] / pixelRatio[1];
presentWidth = pixelScale * codecWidth;
}
return {
profileIdc,
levelIdc,
refFrames,
chromaFormat,
bitDepth: bitDepthLuma,
frameRate,
codecSize: {
width: codecWidth,
height: codecHeight,
},
presentSize: {
width: presentWidth,
height: codecHeight,
},
width: Math.ceil((((picWidthInMbsMinus1 + 1) * 16) - frameCropLeftOffset * 2 - frameCropRightOffset * 2)),
height: ((2 - frameMbsOnlyFlag) * (picHeightInMapUnitsMinus1 + 1) * 16) - ((
frameMbsOnlyFlag
? 2
: 4) * (frameCropTopOffset + frameCropBottomOffset)),
pixelRatio: pixelRatio,
};
}
readSliceType () {
// skip NALu type
this.readUByte();
// discard first_mb_in_slice
this.readUEG();
// return slice_type
return this.readUEG();
}
}

View File

@ -0,0 +1,29 @@
class LiveTask {
constructor(url, config) {
const _headers = new window.Headers();
const _config = {
headers: Object.assign({}, _headers),
method: 'GET',
cache: 'default',
mode: 'cors',
};
this.request = new Request(url, Object.assign({}, _config, config));
}
run (callback) {
function resolve (reader) {
reader.read().then(result => {
callback(result.done ? undefined : result.value);
resolve(reader);
});
}
fetch(this.request).then(res => {
const reader = res.body.getReader();
resolve(reader);
});
}
}
export default LiveTask;

View File

@ -0,0 +1,17 @@
export default class Logger {
static log (...args) {
window.console.log.apply(window, args);
}
static info (...args) {
window.console.info.apply(window, args);
}
static error (...args) {
window.console.error.apply(window, args);
}
static warn (...args) {
window.console.warn.apply(window, args);
}
}

View File

@ -0,0 +1,190 @@
import MediaInfo from '../models/MediaInfo';
class Store {
constructor () {
const isLe = (function () {
const 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
})();
this.state = {
isLe: isLe,
_hasAudio: false,
_hasVideo: false,
_mediaInfo: new MediaInfo(),
_metaData: null,
_videoTrack: {type: 'video', id: 1, samples: [], length: 0},
_audioTrack: {type: 'audio', id: 2, samples: [], length: 0},
_videoMetaData: null,
_audioMetaData: null,
_audioInitialMetadataDispatched: false,
_videoInitialMetadataDispatched: false,
tags: [],
timeStampBase: 0,
hasVideoFlagOverrided: false,
hasAudioFlagOverrided: false,
timeScale: 1000,
duration: 0,
isLive: false,
durationOverrided: false,
naluLengthSize: 4,
_referFrameRate: {
fixed: true,
fps: 23.976,
fpsNum: 23976,
fpsDen: 1000,
},
metaEndPosition: -1,
};
this.methods = {
_isInitialMetadataDispatched: function () {
const {
_hasAudio,
_hasVideo,
_audioInitialMetadataDispatched,
_videoInitialMetadataDispatched,
} = this.state;
if (_hasAudio && _hasVideo) { // both audio & video
return _audioInitialMetadataDispatched && _videoInitialMetadataDispatched;
}
if (_hasAudio && !_hasVideo) { // audio only
return this._audioInitialMetadataDispatched;
}
if (!_hasAudio && _hasVideo) { // video only
return _videoInitialMetadataDispatched;
}
return false;
}.bind(this),
};
}
get referFrameRate () {
return this.state._referFrameRate;
}
set referFrameRate (val) {
this.state._referFrameRate = val;
}
set mediaInfo (mediaInfo) {
this.state._mediaInfo = mediaInfo;
}
get mediaInfo () {
return this.state._mediaInfo;
}
get metaData () {
return this.state._metaData;
}
get hasMetaData () {
return this.state._metaData !== null;
}
set metaData (v) {
this.state._metaData = v;
}
set audioTrack (val) {
this.state._audioTrack = val;
}
get audioTrack () {
return this.state._audioTrack;
}
set videoTrack (val) {
this.state._videoTrack = val;
}
get videoTrack () {
return this.state._videoTrack;
}
set hasAudio (val) {
this.state._hasAudio = val;
this.state._mediaInfo.hasAudio = val;
}
get hasAudio () {
return this.state._hasAudio;
}
set hasVideo (val) {
this.state._hasVideo = val;
this.state._mediaInfo.hasVideo = val;
}
get hasVideo () {
return this.state._hasVideo;
}
set videoMetaData (val) {
this.state._videoMetaData = val;
}
get videoMetaData () {
return this.state._videoMetaData;
}
set audioMetaData (val) {
this.state._audioMetaData = val;
}
get audioMetaData () {
return this.state._audioMetaData;
}
get keyframes () {
return this.state._mediaInfo.keyframes;
}
get isSeekable () {
return this.state._mediaInfo.hasKeyframes;
}
get isLe () {
return this.state.isLe;
}
get hasInitialMetaDispatched () {
const {
_hasAudio,
_hasVideo,
_audioInitialMetadataDispatched,
_videoInitialMetadataDispatched,
} = this.state;
if (_hasAudio && _hasVideo) {
return _audioInitialMetadataDispatched && _videoInitialMetadataDispatched;
}
if (_hasAudio && !_hasVideo) {
return this._audioInitialMetadataDispatched;
}
if (!_hasAudio && _hasVideo) {
return _videoInitialMetadataDispatched;
}
return false;
}
get videoTimeScale () {
return this.state.timeScale;
}
get metaEndPosition () {
return this.state.metaEndPosition;
}
set metaEndPosition (pos) {
this.state.metaEndPosition = pos;
}
get isLive () {
return this.state.isLive;
}
set isLive (val) {
this.state.isLive = val;
}
}
export default Store;

View File

@ -0,0 +1,68 @@
/* eslint-disable */
class UTF8 {
static decode(uint8array) {
const out = [];
const input = uint8array;
let i = 0;
const length = uint8array.length;
while (i < length) {
if (input[i] < 0x80) {
out.push(String.fromCharCode(input[i]));
++i;
continue;
} else if (input[i] < 0xC0) {
// fallthrough
} else if (input[i] < 0xE0) {
if (UTF8._checkContinuation(input, i, 1)) {
const ucs4 = (input[i] & 0x1F) << 6 | (input[i + 1] & 0x3F);
if (ucs4 >= 0x80) {
out.push(String.fromCharCode(ucs4 & 0xFFFF));
i += 2;
continue;
}
}
} else if (input[i] < 0xF0) {
if (UTF8._checkContinuation(input, i, 2)) {
const ucs4 = (input[i] & 0xF) << 12 | (input[i + 1] & 0x3F) << 6 | input[i + 2] & 0x3F;
if (ucs4 >= 0x800 && (ucs4 & 0xF800) !== 0xD800) {
out.push(String.fromCharCode(ucs4 & 0xFFFF));
i += 3;
continue;
}
}
} else if (input[i] < 0xF8) {
if (UTF8._checkContinuation(input, i, 3)) {
let ucs4 = (input[i] & 0x7) << 18 | (input[i + 1] & 0x3F) << 12 |
(input[i + 2] & 0x3F) << 6 | (input[i + 3] & 0x3F);
if (ucs4 > 0x10000 && ucs4 < 0x110000) {
ucs4 -= 0x10000;
out.push(String.fromCharCode((ucs4 >>> 10) | 0xD800));
out.push(String.fromCharCode((ucs4 & 0x3FF) | 0xDC00));
i += 4;
continue;
}
}
}
out.push(String.fromCharCode(0xFFFD));
++i;
}
return out.join('');
}
static _checkContinuation(uint8array, start, checkLength) {
let array = uint8array;
if (start + checkLength < array.length) {
while (checkLength--) {
if ((array[++start] & 0xC0) !== 0x80)
return false;
}
return true;
} else {
return false;
}
}
}
export default UTF8;

Some files were not shown because too many files have changed in this diff Show More