jmuxer 2.0.7 → 2.1.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/src/jmuxer.js CHANGED
@@ -1,8 +1,4 @@
1
1
  import * as debug from './util/debug';
2
- import { NALU } from './util/nalu.js';
3
- import { appendByteArray } from './util/utils.js';
4
- import { H264Parser } from './parsers/h264.js';
5
- import { AACParser } from './parsers/aac.js';
6
2
  import Event from './util/event';
7
3
  import RemuxController from './controller/remux.js';
8
4
  import BufferController from './controller/buffer.js';
@@ -19,6 +15,7 @@ export default class JMuxer extends Event {
19
15
  let defaults = {
20
16
  node: '',
21
17
  mode: 'both', // both, audio, video
18
+ videoCodec: 'H264', // H264, H265
22
19
  flushingTime: 500,
23
20
  maxDelay: 500,
24
21
  clearBuffer: true,
@@ -28,20 +25,24 @@ export default class JMuxer extends Event {
28
25
  onReady: function() {}, // function called when MSE is ready to accept frames
29
26
  onData: function() {}, // function called when data is ready to be sent
30
27
  onError: function() {}, // function called when jmuxer encounters any buffer related errors
28
+ onUnsupportedCodec: function() {}, // function called when a codec is not supported by the browser
31
29
  onMissingVideoFrames: function () {}, // function called when jmuxer encounters any missing video frames
32
30
  onMissingAudioFrames: function () {}, // function called when jmuxer encounters any missing audio frames
31
+ onKeyframePosition: function () {}, // function called when a keyframe is detected thus the provided time is seekable
32
+ onLoggerLog: console.log,
33
+ onLoggerErr: console.error,
33
34
  };
34
35
  this.options = Object.assign({}, defaults, options);
35
36
  this.env = typeof process === 'object' && typeof window === 'undefined' ? 'node' : 'browser';
36
37
  if (this.options.debug) {
37
- debug.setLogger();
38
+ debug.setLogger(this.options.onLoggerLog, this.options.onLoggerErr);
38
39
  }
39
40
 
40
41
  if (!this.options.fps) {
41
42
  this.options.fps = 30;
42
43
  }
43
44
  this.frameDuration = (1000 / this.options.fps) | 0;
44
- this.remuxController = new RemuxController(this.env);
45
+ this.remuxController = new RemuxController(this.env, options.live, this.options.videoCodec, this.frameDuration);
45
46
  this.remuxController.addTrack(this.options.mode);
46
47
 
47
48
  this.initData();
@@ -52,12 +53,34 @@ export default class JMuxer extends Event {
52
53
  this.remuxController.on('ready', this.createBuffer.bind(this));
53
54
  this.initBrowser();
54
55
  }
56
+
57
+ this.remuxController.on('missingVideoFrames', () => {
58
+ if (typeof this.options.onMissingVideoFrames === 'function') {
59
+ this.options.onMissingVideoFrames.call(null);
60
+ }
61
+ });
62
+ this.remuxController.on('missingAudioFrames', () => {
63
+ if (typeof this.options.onMissingAudioFrames === 'function') {
64
+ this.options.onMissingAudioFrames.call(null);
65
+ }
66
+ });
67
+ if (this.clearBuffer) {
68
+ // this is used to know when keyframes are,
69
+ // to essentially know which specific times are seekable
70
+ this.remuxController.on('keyframePosition', time => {
71
+ this.kfPosition.push(time);
72
+ });
73
+ }
74
+ if (typeof this.options.onKeyframePosition === 'function') {
75
+ this.remuxController.on('keyframePosition', time => {
76
+ this.options.onKeyframePosition.call(null, time);
77
+ });
78
+ }
55
79
  }
56
80
 
57
81
  initData() {
58
82
  this.lastCleaningTime = Date.now();
59
83
  this.kfPosition = [];
60
- this.kfCounter = 0;
61
84
  this.pendingUnits = {};
62
85
  this.remainingData = new Uint8Array();
63
86
  this.startInterval();
@@ -134,143 +157,11 @@ export default class JMuxer extends Event {
134
157
  }
135
158
 
136
159
  feed(data) {
137
- let remux = false,
138
- slices,
139
- left,
140
- duration,
141
- chunks = {
142
- video: [],
143
- audio: []
144
- };
145
-
146
160
  if (!data || !this.remuxController) return;
147
- duration = data.duration ? parseInt(data.duration) : 0;
161
+
162
+ data.duration = data.duration ? parseInt(data.duration) : 0;
148
163
 
149
- if (data.video) {
150
- data.video = appendByteArray(this.remainingData, data.video);
151
- [slices, left] = H264Parser.extractNALu(data.video);
152
- this.remainingData = left || new Uint8Array();
153
-
154
- if (slices.length > 0) {
155
- chunks.video = this.getVideoFrames(slices, duration, data.compositionTimeOffset);
156
- remux = true;
157
- } else {
158
- debug.error('Failed to extract any NAL units from video data:', left);
159
- if (typeof this.options.onMissingVideoFrames === 'function') {
160
- this.options.onMissingVideoFrames.call(null, data);
161
- }
162
- return;
163
- }
164
- }
165
- if (data.audio) {
166
- slices = AACParser.extractAAC(data.audio);
167
- if (slices.length > 0) {
168
- chunks.audio = this.getAudioFrames(slices, duration);
169
- remux = true;
170
- } else {
171
- debug.error('Failed to extract audio data from:', data.audio);
172
- if (typeof this.options.onMissingAudioFrames === 'function') {
173
- this.options.onMissingAudioFrames.call(null, data);
174
- }
175
- return;
176
- }
177
- }
178
- if (!remux) {
179
- debug.error('Input object must have video and/or audio property. Make sure it is a valid typed array');
180
- return;
181
- }
182
- this.remuxController.remux(chunks);
183
- }
184
-
185
- getVideoFrames(nalus, duration, compositionTimeOffset) {
186
- let units = [],
187
- frames = [],
188
- fd = 0,
189
- tt = 0,
190
- keyFrame = false,
191
- vcl = false;
192
- if (this.pendingUnits.units) {
193
- units = this.pendingUnits.units;
194
- vcl = this.pendingUnits.vcl;
195
- keyFrame = this.pendingUnits.keyFrame;
196
- this.pendingUnits = {};
197
- }
198
- for (let nalu of nalus) {
199
- let unit = new NALU(nalu);
200
- if (unit.type() === NALU.IDR || unit.type() === NALU.NDR) {
201
- H264Parser.parseHeader(unit);
202
- }
203
- if (units.length && vcl && (unit.isfmb || !unit.isvcl)) {
204
- frames.push({
205
- units,
206
- keyFrame
207
- });
208
- units = [];
209
- keyFrame = false;
210
- vcl = false;
211
- }
212
- units.push(unit);
213
- keyFrame = keyFrame || unit.isKeyframe();
214
- vcl = vcl || unit.isvcl;
215
- }
216
- if (units.length) {
217
- // lets keep indecisive nalus as pending in case of fixed fps
218
- if (!duration) {
219
- this.pendingUnits = {
220
- units,
221
- keyFrame,
222
- vcl
223
- };
224
- }
225
- else if (vcl) {
226
- frames.push({
227
- units,
228
- keyFrame
229
- });
230
- } else {
231
- let last = frames.length - 1;
232
- if (last >= 0) {
233
- frames[last].units = frames[last].units.concat(units);
234
- }
235
- }
236
- }
237
- fd = duration ? duration / frames.length | 0 : this.frameDuration;
238
- tt = duration ? (duration - (fd * frames.length)) : 0;
239
-
240
- frames.map((frame) => {
241
- frame.duration = fd;
242
- frame.compositionTimeOffset = compositionTimeOffset;
243
- if (tt > 0) {
244
- frame.duration++;
245
- tt--;
246
- }
247
- this.kfCounter++;
248
- if (frame.keyFrame && this.options.clearBuffer) {
249
- this.kfPosition.push((this.kfCounter * fd) / 1000);
250
- }
251
- });
252
- debug.log(`jmuxer: No. of frames of the last chunk: ${frames.length}`);
253
- return frames;
254
- }
255
-
256
- getAudioFrames(aacFrames, duration) {
257
- let frames = [],
258
- fd = 0,
259
- tt = 0;
260
-
261
- for (let units of aacFrames) {
262
- frames.push({ units });
263
- }
264
- fd = duration ? duration / frames.length | 0 : this.frameDuration;
265
- tt = duration ? (duration - (fd * frames.length)) : 0;
266
- frames.map((frame) => {
267
- frame.duration = fd;
268
- if (tt > 0) {
269
- frame.duration++;
270
- tt--;
271
- }
272
- });
273
- return frames;
164
+ this.remuxController.feed(data);
274
165
  }
275
166
 
276
167
  destroy() {
@@ -324,7 +215,10 @@ export default class JMuxer extends Event {
324
215
  for (let type in this.remuxController.tracks) {
325
216
  let track = this.remuxController.tracks[type];
326
217
  if (!JMuxer.isSupported(`${type}/mp4; codecs="${track.mp4track.codec}"`)) {
327
- debug.error('Browser does not support codec');
218
+ debug.error(`Browser does not support codec: ${type}/mp4; codecs="${track.mp4track.codec}"`);
219
+ if (typeof this.options.onUnsupportedCodec === 'function') {
220
+ this.options.onUnsupportedCodec.call(null, track.mp4track.codec);
221
+ }
328
222
  return false;
329
223
  }
330
224
  let sb = this.mediaSource.addSourceBuffer(`${type}/mp4; codecs="${track.mp4track.codec}"`);
@@ -353,7 +247,8 @@ export default class JMuxer extends Event {
353
247
  if (this.node.buffered && this.node.buffered.length > 0 && !this.node.seeking) {
354
248
  const end = this.node.buffered.end(0);
355
249
  if (end - this.node.currentTime > (this.options.maxDelay / 1000)) {
356
- console.log('delay');
250
+ debug.log('delay');
251
+ if (this.node.paused) this.node.play().catch(debug.error);
357
252
  this.node.currentTime = end - 0.001;
358
253
  }
359
254
  }
@@ -429,7 +324,7 @@ export default class JMuxer extends Event {
429
324
  URL.revokeObjectURL(this.url);
430
325
  // this.createBuffer();
431
326
  if (typeof this.options.onReady === 'function') {
432
- this.options.onReady.call(null, this.isReset);
327
+ this.options.onReady.call(null, this.isReset, this.mediaSource);
433
328
  }
434
329
  }
435
330
 
@@ -1,7 +1,6 @@
1
1
  import * as debug from '../util/debug';
2
2
 
3
3
  export class AACParser {
4
- static aacHeader;
5
4
 
6
5
  static get samplingRateMap() {
7
6
  return [96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350];
@@ -22,56 +21,30 @@ export class AACParser {
22
21
  static extractAAC(buffer) {
23
22
  let i = 0,
24
23
  length = buffer.byteLength,
25
- result = [],
24
+ slices = [],
26
25
  headerLength,
27
26
  frameLength;
28
27
 
29
28
  if (!AACParser.isAACPattern(buffer)) {
30
29
  debug.error('Invalid ADTS audio format');
31
- return result;
30
+ return {
31
+ valid: false,
32
+ };
32
33
  }
33
34
  headerLength = AACParser.getHeaderLength(buffer);
34
- if (!AACParser.aacHeader) {
35
- AACParser.aacHeader = buffer.subarray(0, headerLength);
36
- }
35
+ const header = buffer.subarray(0, headerLength);
37
36
 
38
37
  while (i < length) {
39
38
  frameLength = AACParser.getFrameLength(buffer);
40
- result.push(buffer.subarray(headerLength, frameLength));
39
+ slices.push(buffer.subarray(headerLength, frameLength));
41
40
  buffer = buffer.slice(frameLength);
42
41
  i += frameLength;
43
42
  }
44
- return result;
45
- }
46
-
47
- constructor(remuxer) {
48
- this.remuxer = remuxer;
49
- this.track = remuxer.mp4track;
43
+ return {
44
+ valid: true,
45
+ header,
46
+ slices,
47
+ };
50
48
  }
51
49
 
52
- setAACConfig() {
53
- let objectType,
54
- sampleIndex,
55
- channelCount,
56
- config = new Uint8Array(2),
57
- headerData = AACParser.aacHeader;
58
-
59
- if (!headerData) return;
60
-
61
- objectType = ((headerData[2] & 0xC0) >>> 6) + 1;
62
- sampleIndex = ((headerData[2] & 0x3C) >>> 2);
63
- channelCount = ((headerData[2] & 0x01) << 2);
64
- channelCount |= ((headerData[3] & 0xC0) >>> 6);
65
-
66
- /* refer to http://wiki.multimedia.cx/index.php?title=MPEG-4_Audio#Audio_Specific_Config */
67
- config[0] = objectType << 3;
68
- config[0] |= (sampleIndex & 0x0E) >> 1;
69
- config[1] |= (sampleIndex & 0x01) << 7;
70
- config[1] |= channelCount << 3;
71
-
72
- this.track.codec = 'mp4a.40.' + objectType;
73
- this.track.channelCount = channelCount;
74
- this.track.config = config;
75
- this.remuxer.readyToDecode = true;
76
- }
77
50
  }
@@ -1,56 +1,42 @@
1
1
  import { ExpGolomb } from '../util/exp-golomb.js';
2
- import { NALU } from '../util/nalu.js';
3
2
  import * as debug from '../util/debug';
4
3
 
4
+ // spec https://www.itu.int/rec/T-REC-H.264/
5
+
5
6
  export class H264Parser {
6
7
 
7
8
  static extractNALu(buffer) {
8
9
  let i = 0,
9
10
  length = buffer.byteLength,
10
- value,
11
- state = 0,
12
11
  result = [],
13
- left,
14
- lastIndex = 0;
12
+ lastIndex = 0,
13
+ zeroCount = 0;
15
14
 
16
15
  while (i < length) {
17
- value = buffer[i++];
18
- // finding 3 or 4-byte start codes (00 00 01 OR 00 00 00 01)
19
- switch (state) {
20
- case 0:
21
- if (value === 0) {
22
- state = 1;
23
- }
24
- break;
25
- case 1:
26
- if (value === 0) {
27
- state = 2;
28
- } else {
29
- state = 0;
30
- }
31
- break;
32
- case 2:
33
- case 3:
34
- if (value === 0) {
35
- state = 3;
36
- } else if (value === 1 && i < length) {
37
- if (lastIndex != i - state -1) {
38
- result.push(buffer.subarray(lastIndex, i - state -1));
39
- }
40
- lastIndex = i;
41
- state = 0;
42
- } else {
43
- state = 0;
44
- }
45
- break;
46
- default:
47
- break;
16
+ let value = buffer[i++];
17
+
18
+ if (value === 0) {
19
+ zeroCount++;
20
+ } else if (value === 1 && zeroCount >= 2) {
21
+ let startCodeLength = zeroCount + 1;
22
+
23
+ if (lastIndex !== i - startCodeLength) {
24
+ result.push(buffer.subarray(lastIndex, i - startCodeLength));
25
+ }
26
+
27
+ lastIndex = i;
28
+ zeroCount = 0;
29
+ } else {
30
+ zeroCount = 0;
48
31
  }
49
32
  }
50
33
 
34
+ // Remaining data after last start code
35
+ let left = null;
51
36
  if (lastIndex < length) {
52
37
  left = buffer.subarray(lastIndex, length);
53
38
  }
39
+
54
40
  return [result, left];
55
41
  }
56
42
 
@@ -235,9 +221,11 @@ export class H264Parser {
235
221
  let fixedFrameRate = decoder.readBoolean();
236
222
  let frameDuration = timeScale / (2 * unitsInTick);
237
223
 
238
- if (fixedFrameRate) {
239
- fps = frameDuration;
240
- }
224
+ // if (fixedFrameRate) {
225
+ // fps = frameDuration;
226
+ // }
227
+ // Return the fps value even if fixedFrameRate is not set
228
+ fps = frameDuration;
241
229
  }
242
230
  }
243
231
  return {
@@ -246,76 +234,95 @@ export class H264Parser {
246
234
  height: ((2 - frameMbsOnlyFlag) * (picHeightInMapUnitsMinus1 + 1) * 16) - ((frameMbsOnlyFlag ? 2 : 4) * (frameCropTopOffset + frameCropBottomOffset)),
247
235
  };
248
236
  }
249
- static parseHeader(unit) {
250
- let decoder = new ExpGolomb(unit.getPayload());
251
- // skip NALu type
252
- decoder.readUByte();
253
- unit.isfmb = decoder.readUEG() === 0;
254
- unit.stype = decoder.readUEG();
237
+
238
+ }
239
+
240
+ export class NALU264 {
241
+ static get NDR() { return 1; }
242
+ static get IDR() { return 5; }
243
+ static get SEI() { return 6; }
244
+ static get SPS() { return 7; }
245
+ static get PPS() { return 8; }
246
+ static get AUD() { return 9; }
247
+
248
+ static get TYPES() {
249
+ return {
250
+ [NALU264.IDR]: 'IDR',
251
+ [NALU264.SEI]: 'SEI',
252
+ [NALU264.SPS]: 'SPS',
253
+ [NALU264.PPS]: 'PPS',
254
+ [NALU264.NDR]: 'NDR',
255
+ [NALU264.AUD]: 'AUD',
256
+ };
257
+ }
258
+
259
+ constructor(data) {
260
+ this.payload = data;
261
+ this.nri = (this.payload[0] & 0x60) >> 5; // nal_ref_idc
262
+ this.nalUnitType = this.payload[0] & 0x1f;
263
+ this._sliceType = null;
264
+ this._isFirstSlice = false;
265
+ }
266
+
267
+ toString() {
268
+ return `${NALU264.TYPES[this.type()] || 'UNKNOWN'}: NRI: ${this.getNri()}`;
255
269
  }
256
- constructor(remuxer) {
257
- this.remuxer = remuxer;
258
- this.track = remuxer.mp4track;
270
+
271
+ getNri() {
272
+ return this.nri;
259
273
  }
260
274
 
261
- parseSPS(sps) {
262
- var config = H264Parser.readSPS(new Uint8Array(sps));
275
+ type() {
276
+ return this.nalUnitType;
277
+ }
263
278
 
264
- this.track.fps = config.fps;
265
- this.track.width = config.width;
266
- this.track.height = config.height;
267
- this.track.sps = [new Uint8Array(sps)];
268
- this.track.codec = 'avc1.';
279
+ get isKeyframe() {
280
+ return this.nalUnitType === NALU264.IDR;
281
+ }
269
282
 
270
- let codecarray = new DataView(sps.buffer, sps.byteOffset + 1, 4);
271
- for (let i = 0; i < 3; ++i) {
272
- var h = codecarray.getUint8(i).toString(16);
273
- if (h.length < 2) {
274
- h = '0' + h;
275
- }
276
- this.track.codec += h;
277
- }
283
+ get isVCL() {
284
+ return this.nalUnitType == NALU264.IDR || this.nalUnitType == NALU264.NDR;
278
285
  }
279
286
 
280
- parsePPS(pps) {
281
- this.track.pps = [new Uint8Array(pps)];
287
+ parseHeader() {
288
+ let decoder = new ExpGolomb(this.getPayload());
289
+ // skip NALu type
290
+ decoder.readUByte();
291
+ this._isFirstSlice = decoder.readUEG() === 0;
292
+ this._sliceType = decoder.readUEG();
282
293
  }
283
294
 
284
- parseNAL(unit) {
285
- if (!unit) return false;
295
+ get isFirstSlice() {
296
+ if (!this._isFirstSlice) {
297
+ this.parseHeader();
298
+ }
299
+ return this._isFirstSlice;
300
+ }
286
301
 
287
- let push = false;
288
- switch (unit.type()) {
289
- case NALU.IDR:
290
- case NALU.NDR:
291
- push = true;
292
- break;
293
- case NALU.PPS:
294
- if (!this.track.pps) {
295
- this.parsePPS(unit.getPayload());
296
- if (!this.remuxer.readyToDecode && this.track.pps && this.track.sps) {
297
- this.remuxer.readyToDecode = true;
298
- }
299
- }
300
- push = true;
301
- break;
302
- case NALU.SPS:
303
- if (!this.track.sps) {
304
- this.parseSPS(unit.getPayload());
305
- if (!this.remuxer.readyToDecode && this.track.pps && this.track.sps) {
306
- this.remuxer.readyToDecode = true;
307
- }
308
- }
309
- push = true;
310
- break;
311
- case NALU.AUD:
312
- debug.log('AUD - ignoing');
313
- break;
314
- case NALU.SEI:
315
- debug.log('SEI - ignoing');
316
- break;
317
- default:
302
+ get sliceType() {
303
+ if (!this._sliceType) {
304
+ this.parseHeader();
318
305
  }
319
- return push;
306
+ return this._sliceType;
307
+ }
308
+
309
+ getPayload() {
310
+ return this.payload;
311
+ }
312
+
313
+ getPayloadSize() {
314
+ return this.payload.byteLength;
315
+ }
316
+
317
+ getSize() {
318
+ return 4 + this.getPayloadSize();
319
+ }
320
+
321
+ getData() {
322
+ const result = new Uint8Array(this.getSize());
323
+ const view = new DataView(result.buffer);
324
+ view.setUint32(0, this.getSize() - 4);
325
+ result.set(this.getPayload(), 4);
326
+ return result;
320
327
  }
321
328
  }