jmuxer 2.0.1 → 2.0.4

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/src/jmuxer.js CHANGED
@@ -8,19 +8,22 @@ import RemuxController from './controller/remux.js';
8
8
  import BufferController from './controller/buffer.js';
9
9
  import { Duplex } from 'stream';
10
10
 
11
- export default class JMuxmer extends Event {
11
+ export default class JMuxer extends Event {
12
12
  static isSupported(codec) {
13
13
  return (window.MediaSource && window.MediaSource.isTypeSupported(codec));
14
14
  }
15
15
 
16
16
  constructor(options) {
17
17
  super('jmuxer');
18
+ this.isReset = false;
18
19
  let defaults = {
19
20
  node: '',
20
21
  mode: 'both', // both, audio, video
21
- flushingTime: 1500,
22
+ flushingTime: 500,
23
+ maxDelay: 500,
22
24
  clearBuffer: true,
23
25
  fps: 30,
26
+ readFpsFromTrack: false, // set true to fetch fps value from NALu
24
27
  debug: false,
25
28
  onReady: function() {}, // function called when MSE is ready to accept frames
26
29
  onError: function() {}, // function called when jmuxer encounters any buffer related error
@@ -37,11 +40,8 @@ export default class JMuxmer extends Event {
37
40
  this.frameDuration = (1000 / this.options.fps) | 0;
38
41
  this.remuxController = new RemuxController(this.env);
39
42
  this.remuxController.addTrack(this.options.mode);
40
- this.lastCleaningTime = Date.now();
41
- this.kfPosition = [];
42
- this.kfCounter = 0;
43
- this.pendingUnits = {};
44
- this.remainingData = new Uint8Array();
43
+
44
+ this.initData();
45
45
 
46
46
  /* events callback */
47
47
  this.remuxController.on('buffer', this.onBuffer.bind(this));
@@ -49,6 +49,14 @@ export default class JMuxmer extends Event {
49
49
  this.remuxController.on('ready', this.createBuffer.bind(this));
50
50
  this.initBrowser();
51
51
  }
52
+ }
53
+
54
+ initData() {
55
+ this.lastCleaningTime = Date.now();
56
+ this.kfPosition = [];
57
+ this.kfCounter = 0;
58
+ this.pendingUnits = {};
59
+ this.remainingData = new Uint8Array();
52
60
  this.startInterval();
53
61
  }
54
62
 
@@ -59,7 +67,6 @@ export default class JMuxmer extends Event {
59
67
  this.node = typeof this.options.node === 'string' ? document.getElementById(this.options.node) : this.options.node;
60
68
  this.mseReady = false;
61
69
  this.setupMSE();
62
- this.sourceBuffers = {};
63
70
  }
64
71
 
65
72
  createStream() {
@@ -88,13 +95,26 @@ export default class JMuxmer extends Event {
88
95
  }
89
96
  this.isMSESupported = !!window.MediaSource;
90
97
  this.mediaSource = new MediaSource();
91
- this.node.src = URL.createObjectURL(this.mediaSource);
98
+ this.url = URL.createObjectURL(this.mediaSource);
99
+ this.node.src = this.url;
100
+ this.mseEnded = false;
92
101
  this.mediaSource.addEventListener('sourceopen', this.onMSEOpen.bind(this));
93
102
  this.mediaSource.addEventListener('sourceclose', this.onMSEClose.bind(this));
94
103
  this.mediaSource.addEventListener('webkitsourceopen', this.onMSEOpen.bind(this));
95
104
  this.mediaSource.addEventListener('webkitsourceclose', this.onMSEClose.bind(this));
96
105
  }
97
106
 
107
+ endMSE() {
108
+ if (!this.mseEnded) {
109
+ try {
110
+ this.mseEnded = true;
111
+ this.mediaSource.endOfStream();
112
+ } catch (e) {
113
+ debug.error('mediasource is not available to end');
114
+ }
115
+ }
116
+ }
117
+
98
118
  feed(data) {
99
119
  let remux = false,
100
120
  slices,
@@ -107,20 +127,28 @@ export default class JMuxmer extends Event {
107
127
 
108
128
  if (!data || !this.remuxController) return;
109
129
  duration = data.duration ? parseInt(data.duration) : 0;
130
+
110
131
  if (data.video) {
111
132
  data.video = appendByteArray(this.remainingData, data.video);
112
133
  [slices, left] = H264Parser.extractNALu(data.video);
134
+ this.remainingData = left || new Uint8Array();
135
+
113
136
  if (slices.length > 0) {
114
137
  chunks.video = this.getVideoFrames(slices, duration);
115
138
  remux = true;
139
+ } else {
140
+ debug.error('Failed to extract any NAL units from video data:', left);
141
+ return;
116
142
  }
117
- this.remainingData = left || new Uint8Array();
118
143
  }
119
144
  if (data.audio) {
120
145
  slices = AACParser.extractAAC(data.audio);
121
146
  if (slices.length > 0) {
122
147
  chunks.audio = this.getAudioFrames(slices, duration);
123
148
  remux = true;
149
+ } else {
150
+ debug.error('Failed to extract audio data from:', data.audio);
151
+ return;
124
152
  }
125
153
  }
126
154
  if (!remux) {
@@ -138,9 +166,9 @@ export default class JMuxmer extends Event {
138
166
  keyFrame = false,
139
167
  vcl = false;
140
168
  if (this.pendingUnits.units) {
141
- units = this.pendingUnits.units;
142
- vcl = this.pendingUnits.vcl;
143
- keyFrame = this.pendingUnits.keyFrame;
169
+ units = this.pendingUnits.units;
170
+ vcl = this.pendingUnits.vcl;
171
+ keyFrame = this.pendingUnits.keyFrame;
144
172
  this.pendingUnits = {};
145
173
  }
146
174
  for (let nalu of nalus) {
@@ -177,12 +205,14 @@ export default class JMuxmer extends Event {
177
205
  });
178
206
  } else {
179
207
  let last = frames.length - 1;
180
- frames[last].units = frames[last].units.concat(units);
208
+ if (last >= 0) {
209
+ frames[last].units = frames[last].units.concat(units);
210
+ }
181
211
  }
182
212
  }
183
213
  fd = duration ? duration / frames.length | 0 : this.frameDuration;
184
214
  tt = duration ? (duration - (fd * frames.length)) : 0;
185
-
215
+
186
216
  frames.map((frame) => {
187
217
  frame.duration = fd;
188
218
  if (tt > 0) {
@@ -204,7 +234,7 @@ export default class JMuxmer extends Event {
204
234
  tt = 0;
205
235
 
206
236
  for (let units of aacFrames) {
207
- frames.push({units});
237
+ frames.push({ units });
208
238
  }
209
239
  fd = duration ? duration / frames.length | 0 : this.frameDuration;
210
240
  tt = duration ? (duration - (fd * frames.length)) : 0;
@@ -220,16 +250,6 @@ export default class JMuxmer extends Event {
220
250
 
221
251
  destroy() {
222
252
  this.stopInterval();
223
- if (this.mediaSource) {
224
- try {
225
- if (this.bufferControllers) {
226
- this.mediaSource.endOfStream();
227
- }
228
- } catch (e) {
229
- debug.error(`mediasource is not available to end ${e.message}`);
230
- }
231
- this.mediaSource = null;
232
- }
233
253
  if (this.stream) {
234
254
  this.remuxController.flush();
235
255
  this.stream.push(null);
@@ -244,10 +264,33 @@ export default class JMuxmer extends Event {
244
264
  this.bufferControllers[type].destroy();
245
265
  }
246
266
  this.bufferControllers = null;
267
+ this.endMSE();
247
268
  }
248
269
  this.node = false;
249
270
  this.mseReady = false;
250
271
  this.videoStarted = false;
272
+ this.mediaSource = null;
273
+ }
274
+
275
+ reset() {
276
+ this.stopInterval();
277
+ this.isReset = true;
278
+ this.node.pause();
279
+ if (this.remuxController) {
280
+ this.remuxController.reset();
281
+ }
282
+ if (this.bufferControllers) {
283
+ for (let type in this.bufferControllers) {
284
+ this.bufferControllers[type].destroy();
285
+ }
286
+ this.bufferControllers = null;
287
+ this.endMSE();
288
+ }
289
+ this.initData();
290
+ if (this.env == 'browser') {
291
+ this.initBrowser();
292
+ }
293
+ debug.log('JMuxer was reset');
251
294
  }
252
295
 
253
296
  createBuffer() {
@@ -255,24 +298,24 @@ export default class JMuxmer extends Event {
255
298
  this.bufferControllers = {};
256
299
  for (let type in this.remuxController.tracks) {
257
300
  let track = this.remuxController.tracks[type];
258
- if (!JMuxmer.isSupported(`${type}/mp4; codecs="${track.mp4track.codec}"`)) {
301
+ if (!JMuxer.isSupported(`${type}/mp4; codecs="${track.mp4track.codec}"`)) {
259
302
  debug.error('Browser does not support codec');
260
303
  return false;
261
304
  }
262
305
  let sb = this.mediaSource.addSourceBuffer(`${type}/mp4; codecs="${track.mp4track.codec}"`);
263
306
  this.bufferControllers[type] = new BufferController(sb, type);
264
- this.sourceBuffers[type] = sb;
265
307
  this.bufferControllers[type].on('error', this.onBufferError.bind(this));
266
308
  }
267
309
  }
268
310
 
269
311
  startInterval() {
270
- this.interval = setInterval(()=>{
271
- if (this.bufferControllers) {
272
- this.releaseBuffer();
273
- this.clearBuffer();
312
+ this.interval = setInterval(() => {
313
+ if (this.options.flushingTime) {
314
+ this.applyAndClearBuffer();
315
+ } else if (this.bufferControllers) {
316
+ this.cancelDelay();
274
317
  }
275
- }, this.options.flushingTime);
318
+ }, this.options.flushingTime || 1000);
276
319
  }
277
320
 
278
321
  stopInterval() {
@@ -281,12 +324,29 @@ export default class JMuxmer extends Event {
281
324
  }
282
325
  }
283
326
 
327
+ cancelDelay() {
328
+ if (this.node.buffered && this.node.buffered.length > 0 && !this.node.seeking) {
329
+ const end = this.node.buffered.end(0);
330
+ if (end - this.node.currentTime > (this.options.maxDelay / 1000)) {
331
+ console.log('delay');
332
+ this.node.currentTime = end - 0.001;
333
+ }
334
+ }
335
+ }
336
+
284
337
  releaseBuffer() {
285
338
  for (let type in this.bufferControllers) {
286
339
  this.bufferControllers[type].doAppend();
287
340
  }
288
341
  }
289
342
 
343
+ applyAndClearBuffer() {
344
+ if (this.bufferControllers) {
345
+ this.releaseBuffer();
346
+ this.clearBuffer();
347
+ }
348
+ }
349
+
290
350
  getSafeClearOffsetOfBuffer(offset) {
291
351
  let maxLimit = (this.options.mode === 'audio' && offset) || 0,
292
352
  adjacentOffset;
@@ -297,7 +357,7 @@ export default class JMuxmer extends Event {
297
357
  adjacentOffset = this.kfPosition[i];
298
358
  }
299
359
  if (adjacentOffset) {
300
- this.kfPosition = this.kfPosition.filter( kfDelimiter => {
360
+ this.kfPosition = this.kfPosition.filter(kfDelimiter => {
301
361
  if (kfDelimiter < adjacentOffset) {
302
362
  maxLimit = kfDelimiter;
303
363
  }
@@ -318,22 +378,31 @@ export default class JMuxmer extends Event {
318
378
  }
319
379
 
320
380
  onBuffer(data) {
381
+ if (this.options.readFpsFromTrack && typeof data.fps !== 'undefined' && this.options.fps != data.fps) {
382
+ this.options.fps = data.fps;
383
+ this.frameDuration = Math.ceil(1000 / data.fps);
384
+ debug.log(`JMuxer changed FPS to ${data.fps} from track data`);
385
+ }
321
386
  if (this.env == 'browser') {
322
387
  if (this.bufferControllers && this.bufferControllers[data.type]) {
323
388
  this.bufferControllers[data.type].feed(data.payload);
324
389
  }
325
- } else if(this.stream) {
390
+ } else if (this.stream) {
326
391
  this.stream.push(data.payload);
327
392
  }
393
+ if (this.options.flushingTime === 0) {
394
+ this.applyAndClearBuffer();
395
+ }
328
396
  }
329
397
 
330
398
  /* Events on MSE */
331
399
  onMSEOpen() {
332
400
  this.mseReady = true;
401
+ URL.revokeObjectURL(this.url);
402
+ // this.createBuffer();
333
403
  if (typeof this.options.onReady === 'function') {
334
- this.options.onReady.call(null);
404
+ this.options.onReady.call(null, this.isReset);
335
405
  }
336
- this.createBuffer();
337
406
  }
338
407
 
339
408
  onMSEClose() {
@@ -342,23 +411,20 @@ export default class JMuxmer extends Event {
342
411
  }
343
412
 
344
413
  onBufferError(data) {
345
- if (typeof this.options.onError === 'function') {
346
- this.options.onError.call(null, data);
347
- }
348
414
  if (data.name == 'QuotaExceeded') {
415
+ debug.log(`JMuxer cleaning ${data.type} buffer due to QuotaExceeded error`);
349
416
  this.bufferControllers[data.type].initCleanup(this.node.currentTime);
350
417
  return;
351
418
  }
352
-
353
- if (this.mediaSource.sourceBuffers.length > 0 && this.sourceBuffers[data.type]) {
354
- this.mediaSource.removeSourceBuffer(this.sourceBuffers[data.type]);
419
+ else if (data.name == 'InvalidStateError') {
420
+ debug.log('JMuxer is reseting due to InvalidStateError');
421
+ this.reset();
355
422
  }
356
- if (this.mediaSource.sourceBuffers.length == 0) {
357
- try {
358
- this.mediaSource.endOfStream();
359
- } catch (e) {
360
- debug.error('mediasource is not available to end');
361
- }
423
+ else {
424
+ this.endMSE();
425
+ }
426
+ if (typeof this.options.onError === 'function') {
427
+ this.options.onError.call(null, data);
362
428
  }
363
429
  }
364
- }
430
+ }
@@ -98,8 +98,30 @@ export class H264Parser {
98
98
  picWidthInMbsMinus1,
99
99
  picHeightInMapUnitsMinus1,
100
100
  frameMbsOnlyFlag,
101
- scalingListCount;
102
- decoder.readUByte();
101
+ scalingListCount,
102
+ fps = 0;
103
+ decoder.readUByte(); // skip NAL header
104
+
105
+ // rewrite NAL
106
+ let rbsp = [],
107
+ hdr_bytes = 1,
108
+ nal_bytes = data.byteLength;
109
+ for (let i = hdr_bytes; i < nal_bytes; i ++) {
110
+ if ((i + 2) < nal_bytes && decoder.readBits(24, false) === 0x000003) {
111
+ rbsp.push(decoder.readBits(8));
112
+ rbsp.push(decoder.readBits(8));
113
+ i += 2;
114
+
115
+ // emulation_prevention_three_byte
116
+ decoder.readBits(8);
117
+ }
118
+ else {
119
+ rbsp.push(decoder.readBits(8));
120
+ }
121
+ }
122
+ decoder.setData(new Uint8Array(rbsp));
123
+ // end of rewrite data
124
+
103
125
  profileIdc = decoder.readUByte(); // profile_idc
104
126
  profileCompat = decoder.readBits(5); // constraint_set[0-4]_flag, u(5)
105
127
  decoder.skipBits(3); // reserved_zero_3bits u(3),
@@ -191,7 +213,7 @@ export class H264Parser {
191
213
  break;
192
214
  }
193
215
  }
194
- if (sarRatio) {
216
+ if (sarRatio && sarRatio[0] > 0 && sarRatio[1] > 0) {
195
217
  sarScale = sarRatio[0] / sarRatio[1];
196
218
  }
197
219
  }
@@ -212,9 +234,14 @@ export class H264Parser {
212
234
  let timeScale = decoder.readUInt();
213
235
  let fixedFrameRate = decoder.readBoolean();
214
236
  let frameDuration = timeScale / (2 * unitsInTick);
237
+
238
+ if (fixedFrameRate) {
239
+ fps = frameDuration;
240
+ }
215
241
  }
216
242
  }
217
243
  return {
244
+ fps: fps > 0 ? fps : undefined,
218
245
  width: Math.ceil((((picWidthInMbsMinus1 + 1) * 16) - frameCropLeftOffset * 2 - frameCropRightOffset * 2) * sarScale),
219
246
  height: ((2 - frameMbsOnlyFlag) * (picHeightInMapUnitsMinus1 + 1) * 16) - ((frameMbsOnlyFlag ? 2 : 4) * (frameCropTopOffset + frameCropBottomOffset)),
220
247
  };
@@ -229,12 +256,12 @@ export class H264Parser {
229
256
  constructor(remuxer) {
230
257
  this.remuxer = remuxer;
231
258
  this.track = remuxer.mp4track;
232
- this.isSafari = remuxer.env = 'browser' && /Safari/.test(navigator.userAgent) && /Apple Computer/.test(navigator.vendor);
233
259
  }
234
260
 
235
261
  parseSPS(sps) {
236
262
  var config = H264Parser.readSPS(new Uint8Array(sps));
237
263
 
264
+ this.track.fps = config.fps;
238
265
  this.track.width = config.width;
239
266
  this.track.height = config.height;
240
267
  this.track.sps = [new Uint8Array(sps)];
@@ -273,15 +300,13 @@ export class H264Parser {
273
300
  push = true;
274
301
  break;
275
302
  case NALU.SPS:
276
- if (!this.track.sps || this.isSafari) {
303
+ if (!this.track.sps) {
277
304
  this.parseSPS(unit.getPayload());
278
305
  if (!this.remuxer.readyToDecode && this.track.pps && this.track.sps) {
279
306
  this.remuxer.readyToDecode = true;
280
307
  }
281
308
  }
282
- if (!this.isSafari) {
283
- push = true;
284
- }
309
+ push = true;
285
310
  break;
286
311
  case NALU.AUD:
287
312
  debug.log('AUD - ignoing');
@@ -4,12 +4,11 @@ import { BaseRemuxer } from './base.js';
4
4
 
5
5
  export class AACRemuxer extends BaseRemuxer {
6
6
 
7
- constructor(timescale, env) {
7
+ constructor(timescale) {
8
8
  super();
9
9
  this.readyToDecode = false;
10
10
  this.nextDts = 0;
11
11
  this.dts = 0;
12
- this.env = env;
13
12
  this.mp4track = {
14
13
  id: BaseRemuxer.getTrackID(),
15
14
  type: 'audio',
@@ -22,7 +21,6 @@ export class AACRemuxer extends BaseRemuxer {
22
21
  config: '',
23
22
  codec: '',
24
23
  };
25
-
26
24
  this.samples = [];
27
25
  this.aac = new AACParser(this);
28
26
  }
@@ -33,6 +31,8 @@ export class AACRemuxer extends BaseRemuxer {
33
31
  this.mp4track.channelCount = '';
34
32
  this.mp4track.config = '';
35
33
  this.mp4track.timescale = this.timescale;
34
+ this.nextDts = 0;
35
+ this.dts = 0;
36
36
  }
37
37
 
38
38
  remux(frames) {
@@ -4,12 +4,11 @@ import { BaseRemuxer } from './base.js';
4
4
 
5
5
  export class H264Remuxer extends BaseRemuxer {
6
6
 
7
- constructor(timescale, env) {
7
+ constructor(timescale) {
8
8
  super();
9
9
  this.readyToDecode = false;
10
10
  this.nextDts = 0;
11
11
  this.dts = 0;
12
- this.env = env;
13
12
  this.mp4track = {
14
13
  id: BaseRemuxer.getTrackID(),
15
14
  type: 'video',
@@ -17,6 +16,7 @@ export class H264Remuxer extends BaseRemuxer {
17
16
  fragmented: true,
18
17
  sps: '',
19
18
  pps: '',
19
+ fps: 30,
20
20
  width: 0,
21
21
  height: 0,
22
22
  timescale: timescale,
@@ -31,6 +31,8 @@ export class H264Remuxer extends BaseRemuxer {
31
31
  this.readyToDecode = false;
32
32
  this.mp4track.sps = '';
33
33
  this.mp4track.pps = '';
34
+ this.nextDts = 0;
35
+ this.dts = 0;
34
36
  }
35
37
 
36
38
  remux(frames) {
@@ -99,7 +101,7 @@ export class H264Remuxer extends BaseRemuxer {
99
101
  }
100
102
 
101
103
  if (!samples.length) return null;
102
-
104
+
103
105
  return new Uint8Array(payload.buffer, 0, this.mp4track.len);
104
106
  }
105
107
  }
@@ -10,6 +10,12 @@ export class ExpGolomb {
10
10
  this.bitLength = data.byteLength * 8;
11
11
  }
12
12
 
13
+ setData(data) {
14
+ this.data = data;
15
+ this.index = 0;
16
+ this.bitLength = data.byteLength * 8;
17
+ }
18
+
13
19
  get bitsAvailable() {
14
20
  return this.bitLength - this.index;
15
21
  }