@alessmicrosystems/mpegts.js 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +158 -0
- package/README_ja.md +153 -0
- package/README_zh.md +157 -0
- package/d.ts/mpegts.d.ts +524 -0
- package/d.ts/src/core/mse-events.d.ts +9 -0
- package/d.ts/src/core/transmuxing-events.d.ts +24 -0
- package/d.ts/src/demux/aac.d.ts +44 -0
- package/d.ts/src/demux/ac3.d.ts +70 -0
- package/d.ts/src/demux/av1-parser.d.ts +77 -0
- package/d.ts/src/demux/av1.d.ts +11 -0
- package/d.ts/src/demux/base-demuxer.d.ts +55 -0
- package/d.ts/src/demux/h264.d.ts +40 -0
- package/d.ts/src/demux/h265.d.ts +65 -0
- package/d.ts/src/demux/klv.d.ts +17 -0
- package/d.ts/src/demux/mp3.d.ts +6 -0
- package/d.ts/src/demux/mpeg4-audio.d.ts +28 -0
- package/d.ts/src/demux/pat-pmt-pes.d.ts +106 -0
- package/d.ts/src/demux/patpmt.d.ts +40 -0
- package/d.ts/src/demux/pes-private-data.d.ts +14 -0
- package/d.ts/src/demux/pgs-data.d.ts +9 -0
- package/d.ts/src/demux/scte35.d.ts +250 -0
- package/d.ts/src/demux/sei.d.ts +8 -0
- package/d.ts/src/demux/smpte2038.d.ts +22 -0
- package/d.ts/src/demux/ts-demuxer.d.ts +124 -0
- package/d.ts/src/player/live-latency-chaser.d.ts +10 -0
- package/d.ts/src/player/live-latency-synchronizer.d.ts +10 -0
- package/d.ts/src/player/loading-controller.d.ts +19 -0
- package/d.ts/src/player/mse-player.d.ts +30 -0
- package/d.ts/src/player/player-engine-dedicated-thread-worker.d.ts +2 -0
- package/d.ts/src/player/player-engine-dedicated-thread.d.ts +48 -0
- package/d.ts/src/player/player-engine-main-thread.d.ts +50 -0
- package/d.ts/src/player/player-engine-worker-cmd-def.d.ts +25 -0
- package/d.ts/src/player/player-engine-worker-msg-def.d.ts +54 -0
- package/d.ts/src/player/player-engine-worker.d.ts +2 -0
- package/d.ts/src/player/player-engine.d.ts +16 -0
- package/d.ts/src/player/player-events.d.ts +21 -0
- package/d.ts/src/player/seeking-handler.d.ts +22 -0
- package/d.ts/src/player/startup-stall-jumper.d.ts +14 -0
- package/d.ts/src/utils/typedarray-equality.d.ts +2 -0
- package/dist/mpegts.js +3 -0
- package/dist/mpegts.js.LICENSE.txt +7 -0
- package/dist/mpegts.js.map +1 -0
- package/package.json +53 -0
- package/src/config.js +67 -0
- package/src/core/features.js +88 -0
- package/src/core/media-info.js +127 -0
- package/src/core/media-segment-info.js +230 -0
- package/src/core/mse-controller.js +599 -0
- package/src/core/mse-events.ts +28 -0
- package/src/core/transmuxer.js +346 -0
- package/src/core/transmuxing-controller.js +628 -0
- package/src/core/transmuxing-events.ts +43 -0
- package/src/core/transmuxing-worker.js +286 -0
- package/src/demux/aac.ts +397 -0
- package/src/demux/ac3.ts +335 -0
- package/src/demux/amf-parser.js +243 -0
- package/src/demux/av1-parser.ts +629 -0
- package/src/demux/av1.ts +103 -0
- package/src/demux/base-demuxer.ts +69 -0
- package/src/demux/demux-errors.js +26 -0
- package/src/demux/exp-golomb.js +116 -0
- package/src/demux/flv-demuxer.js +1854 -0
- package/src/demux/h264.ts +187 -0
- package/src/demux/h265-parser.js +501 -0
- package/src/demux/h265.ts +214 -0
- package/src/demux/klv.ts +40 -0
- package/src/demux/mp3.ts +7 -0
- package/src/demux/mpeg4-audio.ts +45 -0
- package/src/demux/pat-pmt-pes.ts +132 -0
- package/src/demux/pes-private-data.ts +16 -0
- package/src/demux/pgs-data.ts +11 -0
- package/src/demux/scte35.ts +723 -0
- package/src/demux/sei.ts +99 -0
- package/src/demux/smpte2038.ts +89 -0
- package/src/demux/sps-parser.js +298 -0
- package/src/demux/ts-demuxer.ts +2405 -0
- package/src/index.js +4 -0
- package/src/io/fetch-stream-loader.js +266 -0
- package/src/io/io-controller.js +647 -0
- package/src/io/loader.js +134 -0
- package/src/io/param-seek-handler.js +85 -0
- package/src/io/range-seek-handler.js +52 -0
- package/src/io/speed-sampler.js +93 -0
- package/src/io/websocket-loader.js +151 -0
- package/src/io/xhr-moz-chunked-loader.js +211 -0
- package/src/io/xhr-msstream-loader.js +307 -0
- package/src/io/xhr-range-loader.js +366 -0
- package/src/mpegts.js +95 -0
- package/src/player/live-latency-chaser.ts +66 -0
- package/src/player/live-latency-synchronizer.ts +79 -0
- package/src/player/loading-controller.ts +142 -0
- package/src/player/mse-player.ts +150 -0
- package/src/player/native-player.js +262 -0
- package/src/player/player-engine-dedicated-thread.ts +479 -0
- package/src/player/player-engine-main-thread.ts +463 -0
- package/src/player/player-engine-worker-cmd-def.ts +62 -0
- package/src/player/player-engine-worker-msg-def.ts +102 -0
- package/src/player/player-engine-worker.ts +370 -0
- package/src/player/player-engine.ts +35 -0
- package/src/player/player-errors.js +39 -0
- package/src/player/player-events.ts +40 -0
- package/src/player/seeking-handler.ts +205 -0
- package/src/player/startup-stall-jumper.ts +86 -0
- package/src/remux/aac-silent.js +56 -0
- package/src/remux/mp4-generator.js +866 -0
- package/src/remux/mp4-remuxer.js +778 -0
- package/src/utils/browser.js +128 -0
- package/src/utils/exception.js +73 -0
- package/src/utils/logger.js +140 -0
- package/src/utils/logging-control.js +165 -0
- package/src/utils/polyfill.js +68 -0
- package/src/utils/typedarray-equality.ts +69 -0
- package/src/utils/utf8-conv.js +84 -0
- package/src/utils/webworkify-webpack.js +202 -0
- package/tsconfig.json +16 -0
- package/tslint.json +1 -0
- package/types/index.d.ts +3 -0
- package/types/test-flv.ts +8 -0
- package/types/tsconfig.json +24 -0
- package/webpack.config.js +55 -0
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2016 Bilibili. All Rights Reserved.
|
|
3
|
+
*
|
|
4
|
+
* @author zheng qian <xqq@xqq.im>
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import EventEmitter from 'events';
|
|
20
|
+
import Log from '../utils/logger.js';
|
|
21
|
+
import Browser from '../utils/browser.js';
|
|
22
|
+
import MSEEvents from './mse-events';
|
|
23
|
+
import {IllegalStateException} from '../utils/exception.js';
|
|
24
|
+
|
|
25
|
+
// Media Source Extensions controller
|
|
26
|
+
class MSEController {
|
|
27
|
+
|
|
28
|
+
constructor(config) {
|
|
29
|
+
this.TAG = 'MSEController';
|
|
30
|
+
|
|
31
|
+
this._config = config;
|
|
32
|
+
this._emitter = new EventEmitter();
|
|
33
|
+
|
|
34
|
+
if (this._config.isLive && this._config.autoCleanupSourceBuffer == undefined) {
|
|
35
|
+
// For live stream, do auto cleanup by default
|
|
36
|
+
this._config.autoCleanupSourceBuffer = true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
this.e = {
|
|
40
|
+
onSourceOpen: this._onSourceOpen.bind(this),
|
|
41
|
+
onSourceEnded: this._onSourceEnded.bind(this),
|
|
42
|
+
onSourceClose: this._onSourceClose.bind(this),
|
|
43
|
+
onStartStreaming: this._onStartStreaming.bind(this),
|
|
44
|
+
onEndStreaming: this._onEndStreaming.bind(this),
|
|
45
|
+
onQualityChange: this._onQualityChange.bind(this),
|
|
46
|
+
onSourceBufferError: this._onSourceBufferError.bind(this),
|
|
47
|
+
onSourceBufferUpdateEnd: this._onSourceBufferUpdateEnd.bind(this)
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Use ManagedMediaSource only if w3c MediaSource is not available (e.g. iOS Safari)
|
|
51
|
+
this._useManagedMediaSource = ('ManagedMediaSource' in self) && !('MediaSource' in self);
|
|
52
|
+
|
|
53
|
+
this._mediaSource = null;
|
|
54
|
+
this._mediaSourceObjectURL = null;
|
|
55
|
+
|
|
56
|
+
this._mediaElementProxy = null;
|
|
57
|
+
|
|
58
|
+
this._isBufferFull = false;
|
|
59
|
+
this._hasPendingEos = false;
|
|
60
|
+
|
|
61
|
+
this._requireSetMediaDuration = false;
|
|
62
|
+
this._pendingMediaDuration = 0;
|
|
63
|
+
|
|
64
|
+
this._pendingSourceBufferInit = [];
|
|
65
|
+
this._mimeTypes = {
|
|
66
|
+
video: null,
|
|
67
|
+
audio: null
|
|
68
|
+
};
|
|
69
|
+
this._sourceBuffers = {
|
|
70
|
+
video: null,
|
|
71
|
+
audio: null
|
|
72
|
+
};
|
|
73
|
+
this._lastInitSegments = {
|
|
74
|
+
video: null,
|
|
75
|
+
audio: null
|
|
76
|
+
};
|
|
77
|
+
this._pendingSegments = {
|
|
78
|
+
video: [],
|
|
79
|
+
audio: []
|
|
80
|
+
};
|
|
81
|
+
this._pendingRemoveRanges = {
|
|
82
|
+
video: [],
|
|
83
|
+
audio: []
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
destroy() {
|
|
88
|
+
if (this._mediaSource) {
|
|
89
|
+
this.shutdown();
|
|
90
|
+
}
|
|
91
|
+
if (this._mediaSourceObjectURL) {
|
|
92
|
+
this.revokeObjectURL();
|
|
93
|
+
}
|
|
94
|
+
this.e = null;
|
|
95
|
+
this._emitter.removeAllListeners();
|
|
96
|
+
this._emitter = null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
on(event, listener) {
|
|
100
|
+
this._emitter.addListener(event, listener);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
off(event, listener) {
|
|
104
|
+
this._emitter.removeListener(event, listener);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
initialize(mediaElementProxy) {
|
|
108
|
+
if (this._mediaSource) {
|
|
109
|
+
throw new IllegalStateException('MediaSource has been attached to an HTMLMediaElement!');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (this._useManagedMediaSource) {
|
|
113
|
+
Log.v(this.TAG, 'Using ManagedMediaSource');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let ms = this._mediaSource = this._useManagedMediaSource ? new self.ManagedMediaSource() : new self.MediaSource();
|
|
117
|
+
ms.addEventListener('sourceopen', this.e.onSourceOpen);
|
|
118
|
+
ms.addEventListener('sourceended', this.e.onSourceEnded);
|
|
119
|
+
ms.addEventListener('sourceclose', this.e.onSourceClose);
|
|
120
|
+
|
|
121
|
+
if (this._useManagedMediaSource) {
|
|
122
|
+
ms.addEventListener('startstreaming', this.e.onStartStreaming);
|
|
123
|
+
ms.addEventListener('endstreaming', this.e.onEndStreaming);
|
|
124
|
+
ms.addEventListener('qualitychange', this.e.onQualityChange);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
this._mediaElementProxy = mediaElementProxy;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
shutdown() {
|
|
131
|
+
if (this._mediaSource) {
|
|
132
|
+
let ms = this._mediaSource;
|
|
133
|
+
for (let type in this._sourceBuffers) {
|
|
134
|
+
// pending segments should be discard
|
|
135
|
+
let ps = this._pendingSegments[type];
|
|
136
|
+
ps.splice(0, ps.length);
|
|
137
|
+
this._pendingSegments[type] = null;
|
|
138
|
+
this._pendingRemoveRanges[type] = null;
|
|
139
|
+
this._lastInitSegments[type] = null;
|
|
140
|
+
|
|
141
|
+
// remove all sourcebuffers
|
|
142
|
+
let sb = this._sourceBuffers[type];
|
|
143
|
+
if (sb) {
|
|
144
|
+
if (ms.readyState !== 'closed') {
|
|
145
|
+
// ms edge can throw an error: Unexpected call to method or property access
|
|
146
|
+
try {
|
|
147
|
+
ms.removeSourceBuffer(sb);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
Log.e(this.TAG, error.message);
|
|
150
|
+
}
|
|
151
|
+
sb.removeEventListener('error', this.e.onSourceBufferError);
|
|
152
|
+
sb.removeEventListener('updateend', this.e.onSourceBufferUpdateEnd);
|
|
153
|
+
}
|
|
154
|
+
this._mimeTypes[type] = null;
|
|
155
|
+
this._sourceBuffers[type] = null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (ms.readyState === 'open') {
|
|
159
|
+
try {
|
|
160
|
+
ms.endOfStream();
|
|
161
|
+
} catch (error) {
|
|
162
|
+
Log.e(this.TAG, error.message);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
this._mediaElementProxy = null;
|
|
166
|
+
ms.removeEventListener('sourceopen', this.e.onSourceOpen);
|
|
167
|
+
ms.removeEventListener('sourceended', this.e.onSourceEnded);
|
|
168
|
+
ms.removeEventListener('sourceclose', this.e.onSourceClose);
|
|
169
|
+
if (this._useManagedMediaSource) {
|
|
170
|
+
ms.removeEventListener('startstreaming', this.e.onStartStreaming);
|
|
171
|
+
ms.removeEventListener('endstreaming', this.e.onEndStreaming);
|
|
172
|
+
ms.removeEventListener('qualitychange', this.e.onQualityChange);
|
|
173
|
+
}
|
|
174
|
+
this._pendingSourceBufferInit = [];
|
|
175
|
+
this._isBufferFull = false;
|
|
176
|
+
this._mediaSource = null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
isManagedMediaSource() {
|
|
181
|
+
return this._useManagedMediaSource;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
getObject() {
|
|
185
|
+
if (!this._mediaSource) {
|
|
186
|
+
throw new IllegalStateException('MediaSource has not been initialized yet!');
|
|
187
|
+
}
|
|
188
|
+
return this._mediaSource;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
getHandle() {
|
|
192
|
+
if (!this._mediaSource) {
|
|
193
|
+
throw new IllegalStateException('MediaSource has not been initialized yet!');
|
|
194
|
+
}
|
|
195
|
+
return this._mediaSource.handle;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
getObjectURL() {
|
|
199
|
+
if (!this._mediaSource) {
|
|
200
|
+
throw new IllegalStateException('MediaSource has not been initialized yet!');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (this._mediaSourceObjectURL == null) {
|
|
204
|
+
this._mediaSourceObjectURL = URL.createObjectURL(this._mediaSource);
|
|
205
|
+
}
|
|
206
|
+
return this._mediaSourceObjectURL;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
revokeObjectURL() {
|
|
210
|
+
if (this._mediaSourceObjectURL) {
|
|
211
|
+
URL.revokeObjectURL(this._mediaSourceObjectURL);
|
|
212
|
+
this._mediaSourceObjectURL = null;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
appendInitSegment(initSegment, deferred = undefined) {
|
|
217
|
+
if (!this._mediaSource || this._mediaSource.readyState !== 'open' || this._mediaSource.streaming === false) {
|
|
218
|
+
// sourcebuffer creation requires mediaSource.readyState === 'open'
|
|
219
|
+
// so we defer the sourcebuffer creation, until sourceopen event triggered
|
|
220
|
+
this._pendingSourceBufferInit.push(initSegment);
|
|
221
|
+
// make sure that this InitSegment is in the front of pending segments queue
|
|
222
|
+
this._pendingSegments[initSegment.type].push(initSegment);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
let is = initSegment;
|
|
227
|
+
let mimeType = `${is.container}`;
|
|
228
|
+
if (is.codec && is.codec.length > 0) {
|
|
229
|
+
if (is.codec === 'opus' && Browser.safari) {
|
|
230
|
+
is.codec = 'Opus';
|
|
231
|
+
}
|
|
232
|
+
mimeType += `;codecs=${is.codec}`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
let firstInitSegment = false;
|
|
236
|
+
|
|
237
|
+
Log.v(this.TAG, 'Received Initialization Segment, mimeType: ' + mimeType);
|
|
238
|
+
this._lastInitSegments[is.type] = is;
|
|
239
|
+
|
|
240
|
+
if (mimeType !== this._mimeTypes[is.type]) {
|
|
241
|
+
if (!this._mimeTypes[is.type]) { // empty, first chance create sourcebuffer
|
|
242
|
+
firstInitSegment = true;
|
|
243
|
+
try {
|
|
244
|
+
let sb = this._sourceBuffers[is.type] = this._mediaSource.addSourceBuffer(mimeType);
|
|
245
|
+
sb.addEventListener('error', this.e.onSourceBufferError);
|
|
246
|
+
sb.addEventListener('updateend', this.e.onSourceBufferUpdateEnd);
|
|
247
|
+
} catch (error) {
|
|
248
|
+
Log.e(this.TAG, error.message);
|
|
249
|
+
this._emitter.emit(MSEEvents.ERROR, {code: error.code, msg: error.message});
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
} else {
|
|
253
|
+
Log.v(this.TAG, `Notice: ${is.type} mimeType changed, origin: ${this._mimeTypes[is.type]}, target: ${mimeType}`);
|
|
254
|
+
}
|
|
255
|
+
this._mimeTypes[is.type] = mimeType;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (!deferred) {
|
|
259
|
+
// deferred means this InitSegment has been pushed to pendingSegments queue
|
|
260
|
+
this._pendingSegments[is.type].push(is);
|
|
261
|
+
}
|
|
262
|
+
if (!firstInitSegment) { // append immediately only if init segment in subsequence
|
|
263
|
+
if (this._sourceBuffers[is.type] && !this._sourceBuffers[is.type].updating) {
|
|
264
|
+
this._doAppendSegments();
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (Browser.safari && is.container === 'audio/mpeg' && is.mediaDuration > 0) {
|
|
268
|
+
// 'audio/mpeg' track under Safari may cause MediaElement's duration to be NaN
|
|
269
|
+
// Manually correct MediaSource.duration to make progress bar seekable, and report right duration
|
|
270
|
+
this._requireSetMediaDuration = true;
|
|
271
|
+
this._pendingMediaDuration = is.mediaDuration / 1000; // in seconds
|
|
272
|
+
this._updateMediaSourceDuration();
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
appendMediaSegment(mediaSegment) {
|
|
277
|
+
let ms = mediaSegment;
|
|
278
|
+
this._pendingSegments[ms.type].push(ms);
|
|
279
|
+
|
|
280
|
+
if (this._config.autoCleanupSourceBuffer && this._needCleanupSourceBuffer()) {
|
|
281
|
+
this._doCleanupSourceBuffer();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
let sb = this._sourceBuffers[ms.type];
|
|
285
|
+
if (sb && !sb.updating && !this._hasPendingRemoveRanges()) {
|
|
286
|
+
this._doAppendSegments();
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
flush() {
|
|
291
|
+
// remove all appended buffers
|
|
292
|
+
for (let type in this._sourceBuffers) {
|
|
293
|
+
if (!this._sourceBuffers[type]) {
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// abort current buffer append algorithm
|
|
298
|
+
let sb = this._sourceBuffers[type];
|
|
299
|
+
if (this._mediaSource.readyState === 'open') {
|
|
300
|
+
try {
|
|
301
|
+
// If range removal algorithm is running, InvalidStateError will be throwed
|
|
302
|
+
// Ignore it.
|
|
303
|
+
sb.abort();
|
|
304
|
+
} catch (error) {
|
|
305
|
+
Log.e(this.TAG, error.message);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// pending segments should be discard
|
|
310
|
+
let ps = this._pendingSegments[type];
|
|
311
|
+
ps.splice(0, ps.length);
|
|
312
|
+
|
|
313
|
+
if (this._mediaSource.readyState === 'closed') {
|
|
314
|
+
// Parent MediaSource object has been detached from HTMLMediaElement
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// record ranges to be remove from SourceBuffer
|
|
319
|
+
for (let i = 0; i < sb.buffered.length; i++) {
|
|
320
|
+
let start = sb.buffered.start(i);
|
|
321
|
+
let end = sb.buffered.end(i);
|
|
322
|
+
this._pendingRemoveRanges[type].push({start, end});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// if sb is not updating, let's remove ranges now!
|
|
326
|
+
if (!sb.updating) {
|
|
327
|
+
this._doRemoveRanges();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Safari 10 may get InvalidStateError in the later appendBuffer() after SourceBuffer.remove() call
|
|
331
|
+
// Internal parser's state may be invalid at this time. Re-append last InitSegment to workaround.
|
|
332
|
+
// Related issue: https://bugs.webkit.org/show_bug.cgi?id=159230
|
|
333
|
+
if (Browser.safari) {
|
|
334
|
+
let lastInitSegment = this._lastInitSegments[type];
|
|
335
|
+
if (lastInitSegment) {
|
|
336
|
+
this._pendingSegments[type].push(lastInitSegment);
|
|
337
|
+
if (!sb.updating) {
|
|
338
|
+
this._doAppendSegments();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
endOfStream() {
|
|
346
|
+
let ms = this._mediaSource;
|
|
347
|
+
let sb = this._sourceBuffers;
|
|
348
|
+
if (!ms || ms.readyState !== 'open') {
|
|
349
|
+
if (ms && ms.readyState === 'closed' && this._hasPendingSegments()) {
|
|
350
|
+
// If MediaSource hasn't turned into open state, and there're pending segments
|
|
351
|
+
// Mark pending endOfStream, defer call until all pending segments appended complete
|
|
352
|
+
this._hasPendingEos = true;
|
|
353
|
+
}
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
if (sb.video && sb.video.updating || sb.audio && sb.audio.updating) {
|
|
357
|
+
// If any sourcebuffer is updating, defer endOfStream operation
|
|
358
|
+
// See _onSourceBufferUpdateEnd()
|
|
359
|
+
this._hasPendingEos = true;
|
|
360
|
+
} else {
|
|
361
|
+
this._hasPendingEos = false;
|
|
362
|
+
// Notify media data loading complete
|
|
363
|
+
// This is helpful for correcting total duration to match last media segment
|
|
364
|
+
// Otherwise MediaElement's ended event may not be triggered
|
|
365
|
+
ms.endOfStream();
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
_needCleanupSourceBuffer() {
|
|
370
|
+
if (!this._config.autoCleanupSourceBuffer) {
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
let currentTime = this._mediaElementProxy.getCurrentTime();
|
|
375
|
+
|
|
376
|
+
for (let type in this._sourceBuffers) {
|
|
377
|
+
let sb = this._sourceBuffers[type];
|
|
378
|
+
if (sb) {
|
|
379
|
+
let buffered = sb.buffered;
|
|
380
|
+
if (buffered.length >= 1) {
|
|
381
|
+
if (currentTime - buffered.start(0) >= this._config.autoCleanupMaxBackwardDuration) {
|
|
382
|
+
return true;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
_doCleanupSourceBuffer() {
|
|
392
|
+
let currentTime = this._mediaElementProxy.getCurrentTime();
|
|
393
|
+
|
|
394
|
+
for (let type in this._sourceBuffers) {
|
|
395
|
+
let sb = this._sourceBuffers[type];
|
|
396
|
+
if (sb) {
|
|
397
|
+
let buffered = sb.buffered;
|
|
398
|
+
let doRemove = false;
|
|
399
|
+
|
|
400
|
+
for (let i = 0; i < buffered.length; i++) {
|
|
401
|
+
let start = buffered.start(i);
|
|
402
|
+
let end = buffered.end(i);
|
|
403
|
+
|
|
404
|
+
if (start <= currentTime && currentTime < end + 3) { // padding 3 seconds
|
|
405
|
+
if (currentTime - start >= this._config.autoCleanupMaxBackwardDuration) {
|
|
406
|
+
doRemove = true;
|
|
407
|
+
let removeEnd = currentTime - this._config.autoCleanupMinBackwardDuration;
|
|
408
|
+
this._pendingRemoveRanges[type].push({start: start, end: removeEnd});
|
|
409
|
+
}
|
|
410
|
+
} else if (end < currentTime) {
|
|
411
|
+
doRemove = true;
|
|
412
|
+
this._pendingRemoveRanges[type].push({start: start, end: end});
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (doRemove && !sb.updating) {
|
|
417
|
+
this._doRemoveRanges();
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
_updateMediaSourceDuration() {
|
|
424
|
+
let sb = this._sourceBuffers;
|
|
425
|
+
if (this._mediaElementProxy.getReadyState() === 0 || this._mediaSource.readyState !== 'open') {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if ((sb.video && sb.video.updating) || (sb.audio && sb.audio.updating)) {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
let current = this._mediaSource.duration;
|
|
433
|
+
let target = this._pendingMediaDuration;
|
|
434
|
+
|
|
435
|
+
if (target > 0 && (isNaN(current) || target > current)) {
|
|
436
|
+
Log.v(this.TAG, `Update MediaSource duration from ${current} to ${target}`);
|
|
437
|
+
this._mediaSource.duration = target;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
this._requireSetMediaDuration = false;
|
|
441
|
+
this._pendingMediaDuration = 0;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
_doRemoveRanges() {
|
|
445
|
+
for (let type in this._pendingRemoveRanges) {
|
|
446
|
+
if (!this._sourceBuffers[type] || this._sourceBuffers[type].updating) {
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
let sb = this._sourceBuffers[type];
|
|
450
|
+
let ranges = this._pendingRemoveRanges[type];
|
|
451
|
+
while (ranges.length && !sb.updating) {
|
|
452
|
+
let range = ranges.shift();
|
|
453
|
+
sb.remove(range.start, range.end);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
_doAppendSegments() {
|
|
459
|
+
let pendingSegments = this._pendingSegments;
|
|
460
|
+
|
|
461
|
+
for (let type in pendingSegments) {
|
|
462
|
+
if (!this._sourceBuffers[type] || this._sourceBuffers[type].updating || this._mediaSource.streaming === false) {
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (pendingSegments[type].length > 0) {
|
|
467
|
+
let segment = pendingSegments[type].shift();
|
|
468
|
+
|
|
469
|
+
if (typeof segment.timestampOffset === 'number' && isFinite(segment.timestampOffset)) {
|
|
470
|
+
// For MPEG audio stream in MSE, if unbuffered-seeking occurred
|
|
471
|
+
// We need explicitly set timestampOffset to the desired point in timeline for mpeg SourceBuffer.
|
|
472
|
+
let currentOffset = this._sourceBuffers[type].timestampOffset;
|
|
473
|
+
let targetOffset = segment.timestampOffset / 1000; // in seconds
|
|
474
|
+
|
|
475
|
+
let delta = Math.abs(currentOffset - targetOffset);
|
|
476
|
+
if (delta > 0.1) { // If time delta > 100ms
|
|
477
|
+
Log.v(this.TAG, `Update MPEG audio timestampOffset from ${currentOffset} to ${targetOffset}`);
|
|
478
|
+
this._sourceBuffers[type].timestampOffset = targetOffset;
|
|
479
|
+
}
|
|
480
|
+
delete segment.timestampOffset;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (!segment.data || segment.data.byteLength === 0) {
|
|
484
|
+
// Ignore empty buffer
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
try {
|
|
489
|
+
this._sourceBuffers[type].appendBuffer(segment.data);
|
|
490
|
+
this._isBufferFull = false;
|
|
491
|
+
} catch (error) {
|
|
492
|
+
this._pendingSegments[type].unshift(segment);
|
|
493
|
+
if (error.code === 22) { // QuotaExceededError
|
|
494
|
+
/* Notice that FireFox may not throw QuotaExceededError if SourceBuffer is full
|
|
495
|
+
* Currently we can only do lazy-load to avoid SourceBuffer become scattered.
|
|
496
|
+
* SourceBuffer eviction policy may be changed in future version of FireFox.
|
|
497
|
+
*
|
|
498
|
+
* Related issues:
|
|
499
|
+
* https://bugzilla.mozilla.org/show_bug.cgi?id=1279885
|
|
500
|
+
* https://bugzilla.mozilla.org/show_bug.cgi?id=1280023
|
|
501
|
+
*/
|
|
502
|
+
|
|
503
|
+
// report buffer full, abort network IO
|
|
504
|
+
if (!this._isBufferFull) {
|
|
505
|
+
this._emitter.emit(MSEEvents.BUFFER_FULL);
|
|
506
|
+
}
|
|
507
|
+
this._isBufferFull = true;
|
|
508
|
+
} else {
|
|
509
|
+
Log.e(this.TAG, error.message);
|
|
510
|
+
this._emitter.emit(MSEEvents.ERROR, {code: error.code, msg: error.message});
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
_onSourceOpen() {
|
|
518
|
+
Log.v(this.TAG, 'MediaSource onSourceOpen');
|
|
519
|
+
this._mediaSource.removeEventListener('sourceopen', this.e.onSourceOpen);
|
|
520
|
+
// deferred sourcebuffer creation / initialization
|
|
521
|
+
if (this._pendingSourceBufferInit.length > 0) {
|
|
522
|
+
let pendings = this._pendingSourceBufferInit;
|
|
523
|
+
while (pendings.length) {
|
|
524
|
+
let segment = pendings.shift();
|
|
525
|
+
this.appendInitSegment(segment, true);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
// there may be some pending media segments, append them
|
|
529
|
+
if (this._hasPendingSegments()) {
|
|
530
|
+
this._doAppendSegments();
|
|
531
|
+
}
|
|
532
|
+
this._emitter.emit(MSEEvents.SOURCE_OPEN);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
_onStartStreaming() {
|
|
536
|
+
Log.v(this.TAG, 'ManagedMediaSource onStartStreaming');
|
|
537
|
+
this._emitter.emit(MSEEvents.START_STREAMING);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
_onEndStreaming() {
|
|
541
|
+
Log.v(this.TAG, 'ManagedMediaSource onEndStreaming');
|
|
542
|
+
this._emitter.emit(MSEEvents.END_STREAMING);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
_onQualityChange() {
|
|
546
|
+
Log.v(this.TAG, 'ManagedMediaSource onQualityChange');
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
_onSourceEnded() {
|
|
550
|
+
// fired on endOfStream
|
|
551
|
+
Log.v(this.TAG, 'MediaSource onSourceEnded');
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
_onSourceClose() {
|
|
555
|
+
// fired on detaching from media element
|
|
556
|
+
Log.v(this.TAG, 'MediaSource onSourceClose');
|
|
557
|
+
if (this._mediaSource && this.e != null) {
|
|
558
|
+
this._mediaSource.removeEventListener('sourceopen', this.e.onSourceOpen);
|
|
559
|
+
this._mediaSource.removeEventListener('sourceended', this.e.onSourceEnded);
|
|
560
|
+
this._mediaSource.removeEventListener('sourceclose', this.e.onSourceClose);
|
|
561
|
+
if (this._useManagedMediaSource) {
|
|
562
|
+
this._mediaSource.removeEventListener('startstreaming', this.e.onStartStreaming);
|
|
563
|
+
this._mediaSource.removeEventListener('endstreaming', this.e.onEndStreaming);
|
|
564
|
+
this._mediaSource.removeEventListener('qualitychange', this.e.onQualityChange);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
_hasPendingSegments() {
|
|
570
|
+
let ps = this._pendingSegments;
|
|
571
|
+
return ps.video.length > 0 || ps.audio.length > 0;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
_hasPendingRemoveRanges() {
|
|
575
|
+
let prr = this._pendingRemoveRanges;
|
|
576
|
+
return prr.video.length > 0 || prr.audio.length > 0;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
_onSourceBufferUpdateEnd() {
|
|
580
|
+
if (this._requireSetMediaDuration) {
|
|
581
|
+
this._updateMediaSourceDuration();
|
|
582
|
+
} else if (this._hasPendingRemoveRanges()) {
|
|
583
|
+
this._doRemoveRanges();
|
|
584
|
+
} else if (this._hasPendingSegments()) {
|
|
585
|
+
this._doAppendSegments();
|
|
586
|
+
} else if (this._hasPendingEos) {
|
|
587
|
+
this.endOfStream();
|
|
588
|
+
}
|
|
589
|
+
this._emitter.emit(MSEEvents.UPDATE_END);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
_onSourceBufferError(e) {
|
|
593
|
+
Log.e(this.TAG, `SourceBuffer Error: ${e}`);
|
|
594
|
+
// this error might not always be fatal, just ignore it
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
export default MSEController;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2016 Bilibili. All Rights Reserved.
|
|
3
|
+
*
|
|
4
|
+
* @author zheng qian <xqq@xqq.im>
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
enum MSEEvents {
|
|
20
|
+
ERROR = 'error',
|
|
21
|
+
SOURCE_OPEN = 'source_open',
|
|
22
|
+
UPDATE_END = 'update_end',
|
|
23
|
+
BUFFER_FULL = 'buffer_full',
|
|
24
|
+
START_STREAMING = 'start_streaming',
|
|
25
|
+
END_STREAMING = 'end_streaming',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export default MSEEvents;
|