fix: 🐛 (xgplayer-flv.js) 修复flv.js在ES环境下启动失败的问题 fixes #953

This commit is contained in:
gemstone 2023-06-09 17:24:52 +08:00
parent 8ab0b1965c
commit d4ab2b578c
48 changed files with 214 additions and 9011 deletions

36
fixtures/flvjs/index.html Normal file
View File

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FLV.js 测试</title>
<link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
</head>
<body class="text-base">
<div class="container p-3">
<div id="player"></div>
</div>
<div id="log" class="container p-3">
<div class="flex flex-center">
<h3 class="text-lg font-semibold text-indigo-500">日志</h3>
<label class="ml-3 p-1 bg-gray-200">暂停事件<input id="log-pause" type="checkbox" /></label>
</div>
<div class="flex flex-wrap">
<div class="mr-2">
<h4>事件</h4>
<div id="event" class="h-40 bg-gray-200" style="resize: both; overflow: scroll; width: 500px;"></div>
</div>
<div>
<h4>错误</h4>
<div id="error" class="h-40 bg-gray-200" style="resize: both; overflow: scroll; width: 500px;"></div>
</div>
</div>
</div>
<script type="module" src="./index.js"></script>
</body>
</html>

157
fixtures/flvjs/index.js Normal file
View File

@ -0,0 +1,157 @@
import Player from '../../packages/xgplayer/src'
import FlvPlayer from '../../packages/xgplayer-flv.js/src'
localStorage.setItem('xgd', 1)
function defaultOpt() {
return {
isLive: true,
autoplay: false,
autoplayMuted: false,
retryTimes: 3,
retryCount: 3,
retryDelay: 1000,
analyzeDuration: 5000,
loadTimeout: 10000,
bufferBehind: 10,
maxJumpDistance: 3,
maxReaderInterval: 5000,
seamlesslyReload: false
}
}
var cachedOpt = localStorage.getItem('xg:test:flv:opt')
try {
cachedOpt = JSON.parse(cachedOpt)
} catch (error) {
cachedOpt = undefined
}
var opts = Object.assign(
{
// url: 'https://1011.hlsplay.aodianyun.com/demo/game.flv',
url: 'https://pull-flv-l1.douyincdn.com/stage/stream-399911386870710302_ld.flv?keeptime=00093a80&wsSecret=84c8c84e064fb6c6aaad6ec54c5c8247&wsTime=63315a10&abr_pts=1950715'
},
defaultOpt(),
cachedOpt
)
var testPoint = Number(localStorage.getItem('xg:test:flv:point'))
if (isNaN(testPoint)) testPoint = 0
window.onload = function () {
var dlEvent = document.getElementById('event')
var dlError = document.getElementById('error')
var dlLogPause = document.getElementById('log-pause')
function inp(d) {
return d.getElementsByTagName('input')[0]
}
var player
function initPlayer() {
if (player) {
player.destroy()
setTimeout(init, 100)
} else {
init()
}
function init() {
window.player = player = new Player({
el: document.getElementById('player'),
plugins: [FlvPlayer],
url: opts.url,
isLive: opts.isLive,
autoplay: opts.autoplay,
autoplayMuted: opts.autoplayMuted,
flv: opts
})
dlEvent.innerHTML = ''
dlError.innerHTML = ''
function pushEvent(name, value, container) {
container = container || dlEvent
if (container === dlEvent && dlLogPause.checked) return
console.debug('[test]', name, value)
if (container === dlEvent) {
return
}
try {
value = JSON.stringify(value)
} catch (error) {}
var record = document.createElement('div')
record.innerHTML =
'<div class="mb-2"><span class="text-base pr-2 bg-green-500 text-white">' +
name +
' / ' +
player.video.currentTime +
'</span>' +
value +
'</div>'
container.prepend(record)
}
player.on('loadstart', function (event) {
pushEvent('loadstart', event)
})
player.on('loadeddata', function (event) {
pushEvent('loadeddata', event)
})
player.on('play', function (event) {
pushEvent('play', event)
})
player.on('pause', function (event) {
pushEvent('pause', event)
})
player.on('ended', function (event) {
pushEvent('ended', event)
})
player.on('autoplay_was_prevented', function (event) {
pushEvent('autoplay_was_prevented', event)
})
player.on('playing', function (event) {
pushEvent('playing', event)
})
player.on('seeking', function (event) {
pushEvent('seeking', event)
})
player.on('seeked', function (event) {
pushEvent('seeked', event)
})
player.on('waiting', function (event) {
pushEvent('waiting', event)
})
player.on('canplay', function (event) {
pushEvent('canplay', event)
})
player.on('durationchange', function (event) {
pushEvent('durationchange', event)
})
player.on('ready', function (event) {
pushEvent('ready', event)
})
player.on('complete', function (event) {
pushEvent('complete', event)
})
player.on('urlchange', function (event) {
pushEvent('urlchange', event)
})
player.on('destroy', function (event) {
pushEvent('destroy', event)
})
player.on('replay', function (event) {
pushEvent('replay', event)
})
player.on('retry', function (event) {
pushEvent('retry', event)
})
player.on('core_event', function (event) {
pushEvent(event.eventName, event)
})
player.on('error', function (event) {
pushEvent(event.errorType, event, dlError)
})
}
}
initPlayer()
}

View File

@ -9,6 +9,7 @@
"dev:xgplayer": "yarn libd dev fixtures/xgplayer",
"dev:hls": "yarn libd dev fixtures/hls",
"dev:flv": "yarn libd dev fixtures/flv",
"dev:flvjs": "yarn libd dev fixtures/flvjs",
"dev:mp4": "yarn libd dev fixtures/mp4",
"dev:music": "yarn libd dev fixtures/music",
"dev:pano": "yarn libd dev fixtures/pano",

View File

@ -31,11 +31,12 @@
},
"dependencies": {
"es6-promise": "^4.2.4",
"flv.js": "^1.6.2",
"glob": "^7.1.2",
"webworkify": "^1.5.0"
},
"peerDependencies": {
"xgplayer": ">=3.0.0-next.6",
"core-js": ">=3.12.1"
"core-js": ">=3.12.1",
"xgplayer": ">=3.0.0"
}
}

View File

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

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

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

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

@ -1,535 +0,0 @@
/*
* Copyright (C) 2016 Bilibili. All Rights Reserved.
*
* @author zheng qian <xqq@xqq.im>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import EventEmitter from 'events';
import Log from '../utils/logger.js';
import Browser from '../utils/browser.js';
import MSEEvents from './mse-events.js';
import {SampleInfo, IDRSampleList} from './media-segment-info.js';
import {IllegalStateException} from '../utils/exception.js';
// Media Source Extensions controller
class MSEController {
constructor(config) {
this.TAG = 'MSEController';
this._config = config;
this._emitter = new EventEmitter();
if (this._config.isLive && this._config.autoCleanupSourceBuffer == undefined) {
// For live stream, do auto cleanup by default
this._config.autoCleanupSourceBuffer = true;
}
this.e = {
onSourceOpen: this._onSourceOpen.bind(this),
onSourceEnded: this._onSourceEnded.bind(this),
onSourceClose: this._onSourceClose.bind(this),
onSourceBufferError: this._onSourceBufferError.bind(this),
onSourceBufferUpdateEnd: this._onSourceBufferUpdateEnd.bind(this)
};
this._mediaSource = null;
this._mediaSourceObjectURL = null;
this._mediaElement = null;
this._isBufferFull = false;
this._hasPendingEos = false;
this._requireSetMediaDuration = false;
this._pendingMediaDuration = 0;
this._pendingSourceBufferInit = [];
this._mimeTypes = {
video: null,
audio: null
};
this._sourceBuffers = {
video: null,
audio: null
};
this._lastInitSegments = {
video: null,
audio: null
};
this._pendingSegments = {
video: [],
audio: []
};
this._pendingRemoveRanges = {
video: [],
audio: []
};
this._idrList = new IDRSampleList();
}
destroy() {
if (this._mediaElement || this._mediaSource) {
this.detachMediaElement();
}
this.e = null;
this._emitter.removeAllListeners();
this._emitter = null;
}
on(event, listener) {
this._emitter.addListener(event, listener);
}
off(event, listener) {
this._emitter.removeListener(event, listener);
}
attachMediaElement(mediaElement) {
if (this._mediaSource) {
throw new IllegalStateException('MediaSource has been attached to an HTMLMediaElement!');
}
let ms = this._mediaSource = new window.MediaSource();
ms.addEventListener('sourceopen', this.e.onSourceOpen);
ms.addEventListener('sourceended', this.e.onSourceEnded);
ms.addEventListener('sourceclose', this.e.onSourceClose);
this._mediaElement = mediaElement;
this._mediaSourceObjectURL = window.URL.createObjectURL(this._mediaSource);
mediaElement.src = this._mediaSourceObjectURL;
}
detachMediaElement() {
if (this._mediaSource) {
let ms = this._mediaSource;
for (let type in this._sourceBuffers) {
// pending segments should be discard
let ps = this._pendingSegments[type];
ps.splice(0, ps.length);
this._pendingSegments[type] = null;
this._pendingRemoveRanges[type] = null;
this._lastInitSegments[type] = null;
// remove all sourcebuffers
let sb = this._sourceBuffers[type];
if (sb) {
if (ms.readyState !== 'closed') {
ms.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;
}
}
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);
// console.log(`${initSegment.type}`, initSegment)
return;
}
let is = initSegment;
let mimeType = `${is.container}`;
if (is.codec && is.codec.length > 0) {
mimeType += `;codecs=${is.codec}`;
}
let firstInitSegment = false;
Log.v(this.TAG, 'Received Initialization Segment, mimeType: ' + mimeType);
this._lastInitSegments[is.type] = is;
if (mimeType !== this._mimeTypes[is.type]) {
if (!this._mimeTypes[is.type]) { // empty, first chance create sourcebuffer
firstInitSegment = true;
try {
let sb = this._sourceBuffers[is.type] = this._mediaSource.addSourceBuffer(mimeType);
sb.addEventListener('error', this.e.onSourceBufferError);
sb.addEventListener('updateend', this.e.onSourceBufferUpdateEnd);
} catch (error) {
Log.e(this.TAG, error.message);
this._emitter.emit(MSEEvents.ERROR, {code: error.code, msg: error.message});
return;
}
} else {
Log.v(this.TAG, `Notice: ${is.type} mimeType changed, origin: ${this._mimeTypes[is.type]}, target: ${mimeType}`);
}
this._mimeTypes[is.type] = mimeType;
}
if (!deferred) {
// deferred means this InitSegment has been pushed to pendingSegments queue
this._pendingSegments[is.type].push(is);
}
if (!firstInitSegment) { // append immediately only if init segment in subsequence
if (this._sourceBuffers[is.type] && !this._sourceBuffers[is.type].updating) {
this._doAppendSegments();
}
}
if (Browser.safari && is.container === 'audio/mpeg' && is.mediaDuration > 0) {
// 'audio/mpeg' track under Safari may cause MediaElement's duration to be NaN
// Manually correct MediaSource.duration to make progress bar seekable, and report right duration
this._requireSetMediaDuration = true;
this._pendingMediaDuration = is.mediaDuration / 1000; // in seconds
this._updateMediaSourceDuration();
}
}
appendMediaSegment(mediaSegment) {
let ms = mediaSegment;
this._pendingSegments[ms.type].push(ms);
if (this._config.autoCleanupSourceBuffer && this._needCleanupSourceBuffer()) {
this._doCleanupSourceBuffer();
}
let sb = this._sourceBuffers[ms.type];
if (sb && !sb.updating && !this._hasPendingRemoveRanges()) {
this._doAppendSegments();
}
}
seek(seconds) {
// remove all appended buffers
for (let type in this._sourceBuffers) {
if (!this._sourceBuffers[type]) {
continue;
}
// abort current buffer append algorithm
let sb = this._sourceBuffers[type];
if (this._mediaSource.readyState === 'open') {
try {
// If range removal algorithm is running, InvalidStateError will be throwed
// Ignore it.
sb.abort();
} catch (error) {
Log.e(this.TAG, error.message);
}
}
// IDRList should be clear
this._idrList.clear();
// pending segments should be discard
let ps = this._pendingSegments[type];
ps.splice(0, ps.length);
if (this._mediaSource.readyState === 'closed') {
// Parent MediaSource object has been detached from HTMLMediaElement
continue;
}
// record ranges to be remove from SourceBuffer
for (let i = 0; i < sb.buffered.length; i++) {
let start = sb.buffered.start(i);
let end = sb.buffered.end(i);
this._pendingRemoveRanges[type].push({start, end});
}
// if sb is not updating, let's remove ranges now!
if (!sb.updating) {
this._doRemoveRanges();
}
// Safari 10 may get InvalidStateError in the later appendBuffer() after SourceBuffer.remove() call
// Internal parser's state may be invalid at this time. Re-append last InitSegment to workaround.
// Related issue: https://bugs.webkit.org/show_bug.cgi?id=159230
if (Browser.safari) {
let lastInitSegment = this._lastInitSegments[type];
if (lastInitSegment) {
this._pendingSegments[type].push(lastInitSegment);
if (!sb.updating) {
this._doAppendSegments();
}
}
}
}
}
endOfStream() {
let ms = this._mediaSource;
let sb = this._sourceBuffers;
if (!ms || ms.readyState !== 'open') {
if (ms && ms.readyState === 'closed' && this._hasPendingSegments()) {
// If MediaSource hasn't turned into open state, and there're pending segments
// Mark pending endOfStream, defer call until all pending segments appended complete
this._hasPendingEos = true;
}
return;
}
if (sb.video && sb.video.updating || sb.audio && sb.audio.updating) {
// If any sourcebuffer is updating, defer endOfStream operation
// See _onSourceBufferUpdateEnd()
this._hasPendingEos = true;
} else {
this._hasPendingEos = false;
// Notify media data loading complete
// This is helpful for correcting total duration to match last media segment
// Otherwise MediaElement's ended event may not be triggered
ms.endOfStream();
}
}
getNearestKeyframe(dts) {
return this._idrList.getLastSyncPointBeforeDts(dts);
}
_needCleanupSourceBuffer() {
if (!this._config.autoCleanupSourceBuffer) {
return false;
}
let currentTime = this._mediaElement.currentTime;
for (let type in this._sourceBuffers) {
let sb = this._sourceBuffers[type];
if (sb) {
let buffered = sb.buffered;
if (buffered.length >= 1) {
if (currentTime - buffered.start(0) >= this._config.autoCleanupMaxBackwardDuration) {
return true;
}
}
}
}
return false;
}
_doCleanupSourceBuffer() {
let currentTime = this._mediaElement.currentTime;
for (let type in this._sourceBuffers) {
let sb = this._sourceBuffers[type];
if (sb) {
let buffered = sb.buffered;
let doRemove = false;
for (let i = 0; i < buffered.length; i++) {
let start = buffered.start(i);
let end = buffered.end(i);
if (start <= currentTime && currentTime < end + 3) { // padding 3 seconds
if (currentTime - start >= this._config.autoCleanupMaxBackwardDuration) {
doRemove = true;
let removeEnd = currentTime - this._config.autoCleanupMinBackwardDuration;
this._pendingRemoveRanges[type].push({start: start, end: removeEnd});
}
} else if (end < currentTime) {
doRemove = true;
this._pendingRemoveRanges[type].push({start: start, end: end});
}
}
if (doRemove && !sb.updating) {
this._doRemoveRanges();
}
}
}
}
_updateMediaSourceDuration() {
let sb = this._sourceBuffers;
if (this._mediaElement.readyState === 0 || this._mediaSource.readyState !== 'open') {
return;
}
if ((sb.video && sb.video.updating) || (sb.audio && sb.audio.updating)) {
return;
}
let current = this._mediaSource.duration;
let target = this._pendingMediaDuration;
if (target > 0 && (isNaN(current) || target > current)) {
Log.v(this.TAG, `Update MediaSource duration from ${current} to ${target}`);
this._mediaSource.duration = target;
}
this._requireSetMediaDuration = false;
this._pendingMediaDuration = 0;
}
_doRemoveRanges() {
for (let type in this._pendingRemoveRanges) {
if (!this._sourceBuffers[type] || this._sourceBuffers[type].updating) {
continue;
}
let sb = this._sourceBuffers[type];
let ranges = this._pendingRemoveRanges[type];
while (ranges.length && !sb.updating) {
let range = ranges.shift();
sb.remove(range.start, range.end);
}
}
}
_doAppendSegments() {
let pendingSegments = this._pendingSegments;
for (let type in pendingSegments) {
if (!this._sourceBuffers[type] || this._sourceBuffers[type].updating) {
continue;
}
if (pendingSegments[type].length > 0) {
let segment = pendingSegments[type].shift();
if (segment.timestampOffset) {
// For MPEG audio stream in MSE, if unbuffered-seeking occurred
// We need explicitly set timestampOffset to the desired point in timeline for mpeg SourceBuffer.
let currentOffset = this._sourceBuffers[type].timestampOffset;
let targetOffset = segment.timestampOffset / 1000; // in seconds
let delta = Math.abs(currentOffset - targetOffset);
if (delta > 0.1) { // If time delta > 100ms
Log.v(this.TAG, `Update MPEG audio timestampOffset from ${currentOffset} to ${targetOffset}`);
this._sourceBuffers[type].timestampOffset = targetOffset;
}
delete segment.timestampOffset;
}
if (!segment.data || segment.data.byteLength === 0) {
// Ignore empty buffer
continue;
}
try {
this._sourceBuffers[type].appendBuffer(segment.data);
this._isBufferFull = false;
if (type === 'video' && segment.hasOwnProperty('info')) {
this._idrList.appendArray(segment.info.syncPoints);
}
} catch (error) {
this._pendingSegments[type].unshift(segment);
if (error.code === 22) { // QuotaExceededError
/* Notice that FireFox may not throw QuotaExceededError if SourceBuffer is full
* Currently we can only do lazy-load to avoid SourceBuffer become scattered.
* SourceBuffer eviction policy may be changed in future version of FireFox.
*
* Related issues:
* https://bugzilla.mozilla.org/show_bug.cgi?id=1279885
* https://bugzilla.mozilla.org/show_bug.cgi?id=1280023
*/
// report buffer full, abort network IO
if (!this._isBufferFull) {
this._emitter.emit(MSEEvents.BUFFER_FULL);
}
this._isBufferFull = true;
} else {
Log.e(this.TAG, error.message);
this._emitter.emit(MSEEvents.ERROR, {code: error.code, msg: error.message});
}
}
}
}
}
_onSourceOpen() {
Log.v(this.TAG, 'MediaSource onSourceOpen');
this._mediaSource.removeEventListener('sourceopen', this.e.onSourceOpen);
// deferred sourcebuffer creation / initialization
if (this._pendingSourceBufferInit.length > 0) {
let pendings = this._pendingSourceBufferInit;
while (pendings.length) {
let segment = pendings.shift();
this.appendInitSegment(segment, true);
}
}
// there may be some pending media segments, append them
if (this._hasPendingSegments()) {
this._doAppendSegments();
}
this._emitter.emit(MSEEvents.SOURCE_OPEN);
}
_onSourceEnded() {
// fired on endOfStream
Log.v(this.TAG, 'MediaSource onSourceEnded');
}
_onSourceClose() {
// fired on detaching from media element
Log.v(this.TAG, 'MediaSource onSourceClose');
if (this._mediaSource && this.e != null) {
this._mediaSource.removeEventListener('sourceopen', this.e.onSourceOpen);
this._mediaSource.removeEventListener('sourceended', this.e.onSourceEnded);
this._mediaSource.removeEventListener('sourceclose', this.e.onSourceClose);
}
}
_hasPendingSegments() {
let ps = this._pendingSegments;
return ps.video.length > 0 || ps.audio.length > 0;
}
_hasPendingRemoveRanges() {
let prr = this._pendingRemoveRanges;
return prr.video.length > 0 || prr.audio.length > 0;
}
_onSourceBufferUpdateEnd() {
if (this._requireSetMediaDuration) {
this._updateMediaSourceDuration();
} else if (this._hasPendingRemoveRanges()) {
this._doRemoveRanges();
} else if (this._hasPendingSegments()) {
this._doAppendSegments();
} else if (this._hasPendingEos) {
this.endOfStream();
}
this._emitter.emit(MSEEvents.UPDATE_END);
}
_onSourceBufferError(e) {
Log.e(this.TAG, `SourceBuffer Error: ${e}`);
// this error might not always be fatal, just ignore it
}
}
export default MSEController;

View File

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

@ -1,248 +0,0 @@
/*
* 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();
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));
ctl.on(TransmuxingEvents.LOADED_SEI, this._onLoadedSei.bind(this));
}
}
destroy() {
if (this._worker) {
if (!this._workerDestroying) {
this._workerDestroying = true;
this._worker.postMessage({cmd: 'destroy'});
LoggingControl.removeListener(this.e.onLoggingConfigChanged);
this.e = null;
}
} else {
this._controller.destroy();
this._controller = null;
}
this._emitter.removeAllListeners();
this._emitter = null;
}
on(event, listener) {
this._emitter.addListener(event, listener);
}
off(event, listener) {
this._emitter.removeListener(event, listener);
}
hasWorker() {
return this._worker != null;
}
open() {
if (this._worker) {
this._worker.postMessage({cmd: 'start'});
} else {
this._controller.start();
}
}
close() {
if (this._worker) {
this._worker.postMessage({cmd: 'stop'});
} else {
this._controller.stop();
}
}
seek(milliseconds) {
if (this._worker) {
this._worker.postMessage({cmd: 'seek', param: milliseconds});
} else {
this._controller.seek(milliseconds);
}
}
pause() {
if (this._worker) {
this._worker.postMessage({cmd: 'pause'});
} else {
this._controller.pause();
}
}
resume() {
if (this._worker) {
this._worker.postMessage({cmd: 'resume'});
} else {
this._controller.resume();
}
}
_onInitSegment(type, initSegment) {
// do async invoke
Promise.resolve().then(() => {
this._emitter.emit(TransmuxingEvents.INIT_SEGMENT, type, initSegment);
});
}
_onMediaSegment(type, mediaSegment) {
Promise.resolve().then(() => {
this._emitter.emit(TransmuxingEvents.MEDIA_SEGMENT, type, mediaSegment);
});
}
_onLoadingComplete() {
Promise.resolve().then(() => {
this._emitter.emit(TransmuxingEvents.LOADING_COMPLETE);
});
}
_onRecoveredEarlyEof() {
Promise.resolve().then(() => {
this._emitter.emit(TransmuxingEvents.RECOVERED_EARLY_EOF);
});
}
_onMediaInfo(mediaInfo) {
Promise.resolve().then(() => {
this._emitter.emit(TransmuxingEvents.MEDIA_INFO, mediaInfo);
});
}
_onStatisticsInfo(statisticsInfo) {
Promise.resolve().then(() => {
this._emitter.emit(TransmuxingEvents.STATISTICS_INFO, statisticsInfo);
});
}
_onIOError(type, info) {
Promise.resolve().then(() => {
this._emitter.emit(TransmuxingEvents.IO_ERROR, type, info);
});
}
_onDemuxError(type, info) {
Promise.resolve().then(() => {
this._emitter.emit(TransmuxingEvents.DEMUX_ERROR, type, info);
});
}
_onRecommendSeekpoint(milliseconds) {
Promise.resolve().then(() => {
this._emitter.emit(TransmuxingEvents.RECOMMEND_SEEKPOINT, milliseconds);
});
}
_onLoadedSei(timestamp,data) {
Promise.resolve().then(() => {
this._emitter.emit(TransmuxingEvents.LOADED_SEI, timestamp, data);
});
}
_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

@ -1,435 +0,0 @@
/*
* Copyright (C) 2016 Bilibili. All Rights Reserved.
*
* @author zheng qian <xqq@xqq.im>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import EventEmitter from 'events';
import Log from '../utils/logger.js';
import Browser from '../utils/browser.js';
import MediaInfo from './media-info.js';
import FLVDemuxer from '../demux/flv-demuxer.js';
import MP4Remuxer from '../remux/mp4-remuxer.js';
import DemuxErrors from '../demux/demux-errors.js';
import IOController from '../io/io-controller.js';
import TransmuxingEvents from './transmuxing-events.js';
import {LoaderStatus, LoaderErrors} from '../io/loader.js';
// Transmuxing (IO, Demuxing, Remuxing) controller, with multipart support
class TransmuxingController {
constructor(mediaDataSource, config) {
this.TAG = 'TransmuxingController';
this._emitter = new EventEmitter();
this._config = config;
// treat single part media as multipart media, which has only one segment
if (!mediaDataSource.segments) {
mediaDataSource.segments = [{
duration: mediaDataSource.duration,
filesize: mediaDataSource.filesize,
url: mediaDataSource.url
}];
}
// fill in default IO params if not exists
if (typeof mediaDataSource.cors !== 'boolean') {
mediaDataSource.cors = true;
}
if (typeof mediaDataSource.withCredentials !== 'boolean') {
mediaDataSource.withCredentials = false;
}
this._mediaDataSource = mediaDataSource;
this._currentSegmentIndex = 0;
let totalDuration = 0;
this._mediaDataSource.segments.forEach((segment) => {
// timestampBase for each segment, and calculate total duration
segment.timestampBase = totalDuration;
totalDuration += segment.duration;
// params needed by IOController
segment.cors = mediaDataSource.cors;
segment.withCredentials = mediaDataSource.withCredentials;
// referrer policy control, if exist
if (config.referrerPolicy) {
segment.referrerPolicy = config.referrerPolicy;
}
});
if (!isNaN(totalDuration) && this._mediaDataSource.duration !== totalDuration) {
this._mediaDataSource.duration = totalDuration;
}
this._mediaInfo = null;
this._demuxer = null;
this._remuxer = null;
this._ioctl = null;
this._pendingSeekTime = null;
this._pendingResolveSeekPoint = null;
this._statisticsReporter = null;
}
destroy() {
this._mediaInfo = null;
this._mediaDataSource = null;
if (this._statisticsReporter) {
this._disableStatisticsReporter();
}
if (this._ioctl) {
this._ioctl.destroy();
this._ioctl = null;
}
if (this._demuxer) {
this._demuxer.destroy();
this._demuxer = null;
}
if (this._remuxer) {
this._remuxer.destroy();
this._remuxer = null;
}
this._emitter.removeAllListeners();
this._emitter = null;
}
on(event, listener) {
this._emitter.addListener(event, listener);
}
off(event, listener) {
this._emitter.removeListener(event, listener);
}
start() {
this._loadSegment(0);
this._enableStatisticsReporter();
}
_loadSegment(segmentIndex, optionalFrom) {
this._currentSegmentIndex = segmentIndex;
let dataSource = this._mediaDataSource.segments[segmentIndex];
let ioctl = this._ioctl = new IOController(dataSource, this._config, segmentIndex);
ioctl.onError = this._onIOException.bind(this);
ioctl.onSeeked = this._onIOSeeked.bind(this);
ioctl.onComplete = this._onIOComplete.bind(this);
ioctl.onRedirect = this._onIORedirect.bind(this);
ioctl.onRecoveredEarlyEof = this._onIORecoveredEarlyEof.bind(this);
if (optionalFrom) {
this._demuxer.bindDataSource(this._ioctl);
} else {
ioctl.onDataArrival = this._onInitChunkArrival.bind(this);
}
ioctl.open(optionalFrom);
}
stop() {
this._internalAbort();
this._disableStatisticsReporter();
}
_internalAbort() {
if (this._ioctl) {
this._ioctl.destroy();
this._ioctl = null;
}
}
pause() { // take a rest
if (this._ioctl && this._ioctl.isWorking()) {
this._ioctl.pause();
this._disableStatisticsReporter();
}
}
resume() {
if (this._ioctl && this._ioctl.isPaused()) {
this._ioctl.resume();
this._enableStatisticsReporter();
}
}
seek(milliseconds) {
if (this._mediaInfo == null || !this._mediaInfo.isSeekable()) {
return;
}
let targetSegmentIndex = this._searchSegmentIndexContains(milliseconds);
if (targetSegmentIndex === this._currentSegmentIndex) {
// intra-segment seeking
let segmentInfo = this._mediaInfo.segments[targetSegmentIndex];
if (segmentInfo == undefined) {
// current segment loading started, but mediainfo hasn't received yet
// wait for the metadata loaded, then seek to expected position
this._pendingSeekTime = milliseconds;
} else {
let keyframe = segmentInfo.getNearestKeyframe(milliseconds);
this._remuxer.seek(keyframe.milliseconds);
this._ioctl.seek(keyframe.fileposition);
// Will be resolved in _onRemuxerMediaSegmentArrival()
this._pendingResolveSeekPoint = keyframe.milliseconds;
}
} else {
// cross-segment seeking
let targetSegmentInfo = this._mediaInfo.segments[targetSegmentIndex];
if (targetSegmentInfo == undefined) {
// target segment hasn't been loaded. We need metadata then seek to expected time
this._pendingSeekTime = milliseconds;
this._internalAbort();
this._remuxer.seek();
this._remuxer.insertDiscontinuity();
this._loadSegment(targetSegmentIndex);
// Here we wait for the metadata loaded, then seek to expected position
} else {
// We have target segment's metadata, direct seek to target position
let keyframe = targetSegmentInfo.getNearestKeyframe(milliseconds);
this._internalAbort();
this._remuxer.seek(milliseconds);
this._remuxer.insertDiscontinuity();
this._demuxer.resetMediaInfo();
this._demuxer.timestampBase = this._mediaDataSource.segments[targetSegmentIndex].timestampBase;
this._loadSegment(targetSegmentIndex, keyframe.fileposition);
this._pendingResolveSeekPoint = keyframe.milliseconds;
this._reportSegmentMediaInfo(targetSegmentIndex);
}
}
this._enableStatisticsReporter();
}
_searchSegmentIndexContains(milliseconds) {
let segments = this._mediaDataSource.segments;
let idx = segments.length - 1;
for (let i = 0; i < segments.length; i++) {
if (milliseconds < segments[i].timestampBase) {
idx = i - 1;
break;
}
}
return idx;
}
_onInitChunkArrival(data, byteStart) {
let probeData = null;
let consumed = 0;
if (byteStart > 0) {
// IOController seeked immediately after opened, byteStart > 0 callback may received
this._demuxer.bindDataSource(this._ioctl);
this._demuxer.timestampBase = this._mediaDataSource.segments[this._currentSegmentIndex].timestampBase;
consumed = this._demuxer.parseChunks(data, byteStart);
} else if ((probeData = FLVDemuxer.probe(data)).match) {
// Always create new FLVDemuxer
this._demuxer = new FLVDemuxer(probeData, this._config);
if (!this._remuxer) {
this._remuxer = new MP4Remuxer(this._config);
}
let mds = this._mediaDataSource;
if (mds.duration != undefined && !isNaN(mds.duration)) {
this._demuxer.overridedDuration = mds.duration;
}
if (typeof mds.hasAudio === 'boolean') {
this._demuxer.overridedHasAudio = mds.hasAudio;
}
if (typeof mds.hasVideo === 'boolean') {
this._demuxer.overridedHasVideo = mds.hasVideo;
}
this._demuxer.timestampBase = mds.segments[this._currentSegmentIndex].timestampBase;
this._demuxer.onError = this._onDemuxException.bind(this);
this._demuxer.onMediaInfo = this._onMediaInfo.bind(this);
this._demuxer.onLoadedSei = this._onLoadedSei.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);
});
}
}
_onLoadedSei(timestamp, data) {
this._emitter.emit(TransmuxingEvents.LOADED_SEI,timestamp,data);
}
_onIOSeeked() {
this._remuxer.insertDiscontinuity();
}
_onIOComplete(extraData) {
let segmentIndex = extraData;
let nextSegmentIndex = segmentIndex + 1;
if (nextSegmentIndex < this._mediaDataSource.segments.length) {
this._internalAbort();
this._remuxer.flushStashedSamples();
this._loadSegment(nextSegmentIndex);
} else {
this._remuxer.flushStashedSamples();
this._emitter.emit(TransmuxingEvents.LOADING_COMPLETE);
this._disableStatisticsReporter();
}
}
_onIORedirect(redirectedURL) {
let segmentIndex = this._ioctl.extraData;
this._mediaDataSource.segments[segmentIndex].redirectedURL = redirectedURL;
}
_onIORecoveredEarlyEof() {
this._emitter.emit(TransmuxingEvents.RECOVERED_EARLY_EOF);
}
_onIOException(type, info) {
Log.e(this.TAG, `IOException: type = ${type}, code = ${info.code}, msg = ${info.msg}`);
this._emitter.emit(TransmuxingEvents.IO_ERROR, type, info);
this._disableStatisticsReporter();
}
_onDemuxException(type, info) {
Log.e(this.TAG, `DemuxException: type = ${type}, info = ${info}`);
this._emitter.emit(TransmuxingEvents.DEMUX_ERROR, type, info);
}
_onRemuxerInitSegmentArrival(type, initSegment) {
this._emitter.emit(TransmuxingEvents.INIT_SEGMENT, type, initSegment);
}
_onRemuxerMediaSegmentArrival(type, mediaSegment) {
if (this._pendingSeekTime != null) {
// Media segments after new-segment cross-seeking should be dropped.
return;
}
this._emitter.emit(TransmuxingEvents.MEDIA_SEGMENT, type, mediaSegment);
// Resolve pending seekPoint
if (this._pendingResolveSeekPoint != null && type === 'video') {
let syncPoints = mediaSegment.info.syncPoints;
let seekpoint = this._pendingResolveSeekPoint;
this._pendingResolveSeekPoint = null;
// Safari: Pass PTS for recommend_seekpoint
if (Browser.safari && syncPoints.length > 0 && syncPoints[0].originalDts === seekpoint) {
seekpoint = syncPoints[0].pts;
}
// else: use original DTS (keyframe.milliseconds)
this._emitter.emit(TransmuxingEvents.RECOMMEND_SEEKPOINT, seekpoint);
}
}
_enableStatisticsReporter() {
if (this._statisticsReporter == null) {
this._statisticsReporter = self.setInterval(
this._reportStatisticsInfo.bind(this),
this._config.statisticsInfoReportInterval);
}
}
_disableStatisticsReporter() {
if (this._statisticsReporter) {
self.clearInterval(this._statisticsReporter);
this._statisticsReporter = null;
}
}
_reportSegmentMediaInfo(segmentIndex) {
let segmentInfo = this._mediaInfo.segments[segmentIndex];
let exportInfo = Object.assign({}, segmentInfo);
exportInfo.duration = this._mediaInfo.duration;
exportInfo.segmentCount = this._mediaInfo.segmentCount;
delete exportInfo.segments;
delete exportInfo.keyframesIndex;
this._emitter.emit(TransmuxingEvents.MEDIA_INFO, exportInfo);
}
_reportStatisticsInfo() {
let info = {};
info.url = this._ioctl.currentURL;
info.hasRedirect = this._ioctl.hasRedirect;
if (info.hasRedirect) {
info.redirectedURL = this._ioctl.currentRedirectedURL;
}
info.speed = this._ioctl.currentSpeed;
info.loaderType = this._ioctl.loaderType;
info.currentSegmentIndex = this._currentSegmentIndex;
info.totalSegmentCount = this._mediaDataSource.segments.length;
this._emitter.emit(TransmuxingEvents.STATISTICS_INFO, info);
}
}
export default TransmuxingController;

View File

@ -1,32 +0,0 @@
/*
* 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',
LOADED_SEI: 'loaded_sei'
};
export default TransmuxingEvents;

View File

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

@ -1,246 +0,0 @@
/*
* 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;
}
let maxLoop = 10000
while (offset < dataSize - 4 && maxLoop-- > 0) { // 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;
}
let maxLoop = 10000
while (offset < dataSize - 8 && maxLoop-- > 0) { // 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

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

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

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

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

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

View File

@ -1,233 +0,0 @@
/*
* 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) {
// First check received length
if (this._contentLength !== null && this._receivedLength < this._contentLength) {
// Report Early-EOF
this._status = LoaderStatus.kError;
let type = LoaderErrors.EARLY_EOF;
let info = {code: -1, msg: 'Fetch stream meet Early-EOF'};
if (this._onError) {
this._onError(type, info);
} else {
throw new RuntimeException(info.msg);
}
} else {
// OK. Download complete
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

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

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

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

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

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

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

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

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

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

@ -1,611 +0,0 @@
/*
* 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;
}
}
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;
}
}
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.on(TransmuxingEvents.LOADED_SEI, (timestamp, data) => {
this._emitter.emit(PlayerEvents.LOADED_SEI, timestamp, data);
});
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() {
let playPromise = this._mediaElement.play()
if (playPromise !== undefined && playPromise) {
return playPromise.catch(function() {});
} else return undefined;
}
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();
}
}
export default FlvPlayer;

View File

@ -1,259 +0,0 @@
/*
* 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() {
let playPromise = this._mediaElement.play()
if (playPromise !== undefined && playPromise) {
return playPromise.catch(function() {});
} else return undefined;
}
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

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

@ -1,28 +0,0 @@
/*
* 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',
LOADED_SEI: 'loaded_sei'
};
export default PlayerEvents;

View File

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

@ -1,596 +0,0 @@
/*
* 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)
if (trackId === 1) {
// console.log(trun)
}
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++) {
// console.log(samples[i].duration)
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

@ -1,767 +0,0 @@
/*
* 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;
let needFillSilentFrames = false;
let silentFrames = null;
let sampleDuration = 0;
if (originalDts < -0.001) {
continue; //pass the first sample with the invalid dts
}
if (this._audioMeta.codec !== 'mp3') {
// for AAC codec, we need to keep dts increase based on refSampleDuration
let curRefDts = originalDts;
const maxAudioFramesDrift = 3;
if (this._audioNextDts) {
curRefDts = this._audioNextDts;
}
dtsCorrection = originalDts - curRefDts;
if (dtsCorrection <= -maxAudioFramesDrift * refSampleDuration) {
// If we're overlapping by more than maxAudioFramesDrift number of frame, drop this sample
Log.w(this.TAG, `Dropping 1 audio frame (originalDts: ${originalDts} ms ,curRefDts: ${curRefDts} ms) due to dtsCorrection: ${dtsCorrection} ms overlap.`);
continue;
}
else if (dtsCorrection >= maxAudioFramesDrift * refSampleDuration && this._fillAudioTimestampGap && !Browser.safari) {
// Silent frame generation, if large timestamp gap detected && config.fixAudioTimestampGap
needFillSilentFrames = true;
// We need to insert silent frames to fill timestamp gap
let frameCount = Math.floor(dtsCorrection / refSampleDuration);
Log.w(this.TAG, 'Large audio timestamp gap detected, may cause AV sync to drift. ' +
'Silent frames will be generated to avoid unsync.\n' +
`originalDts: ${originalDts} ms, curRefDts: ${curRefDts} ms, ` +
`dtsCorrection: ${Math.round(dtsCorrection)} ms, generate: ${frameCount} frames`);
dts = Math.floor(curRefDts);
sampleDuration = Math.floor(curRefDts + refSampleDuration) - dts;
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++) {
curRefDts = curRefDts + refSampleDuration;
let intDts = Math.floor(curRefDts); // change to integer
let intDuration = Math.floor(curRefDts + refSampleDuration) - intDts;
let frame = {
dts: intDts,
pts: intDts,
cts: 0,
unit: silentUnit,
size: silentUnit.byteLength,
duration: intDuration, // wait for next sample
originalDts: originalDts,
flags: {
isLeading: 0,
dependsOn: 1,
isDependedOn: 0,
hasRedundancy: 0
}
};
silentFrames.push(frame);
mdatBytes += unit.byteLength;
}
this._audioNextDts = curRefDts + refSampleDuration;
} else {
dts = Math.floor(curRefDts);
sampleDuration = Math.floor(curRefDts + refSampleDuration) - dts;
this._audioNextDts = curRefDts + refSampleDuration;
}
} else {
// keep the original dts calculate algorithm for mp3
dts = originalDts - dtsCorrection;
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);
}
}
this._audioNextDts = dts + sampleDuration;
}
if (firstDts === -1) {
firstDts = dts;
}
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);
}
}
if (mp4Samples.length === 0) {
//no samples need to remux
track.samples = [];
track.length = 0;
return;
}
// 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

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

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

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

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

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

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

@ -1,5 +1,5 @@
import { BasePlugin, Errors, Events } from 'xgplayer'
import Flv from './flv/flv'
import Flv from 'flv.js'
class FlvJsPlugin extends BasePlugin {
static get isSupported () {

View File

@ -27,7 +27,7 @@
"xgplayer-streaming-shared": "3.0.5-alpha.0"
},
"peerDependencies": {
"xgplayer": "3.0.5-alpha.0",
"xgplayer": ">=3.0.0",
"core-js": ">=3.12.1"
}
}

View File

@ -44,7 +44,7 @@
"xgplayer-transmuxer": "3.0.5-alpha.0"
},
"peerDependencies": {
"xgplayer": "3.0.5-alpha.0",
"xgplayer": ">=3.0.0",
"core-js": ">=3.12.1"
}
}

View File

@ -2816,7 +2816,7 @@ es6-iterator@^2.0.3:
es5-ext "^0.10.35"
es6-symbol "^3.1.1"
es6-promise@^4.2.4:
es6-promise@^4.2.4, es6-promise@^4.2.8:
version "4.2.8"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==
@ -3320,6 +3320,14 @@ flatted@^3.1.0:
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787"
integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==
flv.js@^1.6.2:
version "1.6.2"
resolved "https://registry.npmjs.org/flv.js/-/flv.js-1.6.2.tgz#fa3340fe3f7ee01d3977f7876aee66b8436e5922"
integrity sha512-xre4gUbX1MPtgQRKj2pxJENp/RnaHaxYvy3YToVVCrSmAWUu85b9mug6pTXF6zakUjNP2lFWZ1rkSX7gxhB/2A==
dependencies:
es6-promise "^4.2.8"
webworkify-webpack "^2.1.5"
for-each@^0.3.3:
version "0.3.3"
resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e"
@ -6202,6 +6210,11 @@ webidl-conversions@^7.0.0:
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a"
integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==
webworkify-webpack@^2.1.5:
version "2.1.5"
resolved "https://registry.npmjs.org/webworkify-webpack/-/webworkify-webpack-2.1.5.tgz#bf4336624c0626cbe85cf1ffde157f7aa90b1d1c"
integrity sha512-2akF8FIyUvbiBBdD+RoHpoTbHMQF2HwjcxfDvgztAX5YwbZNyrtfUMgvfgFVsgDhDPVTlkbb5vyasqDHfIDPQw==
webworkify@^1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/webworkify/-/webworkify-1.5.0.tgz#734ad87a774de6ebdd546e1d3e027da5b8f4a42c"