jmuxer 2.0.7 → 2.1.0

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/package.json CHANGED
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "name": "jmuxer",
3
- "version": "2.0.7",
3
+ "version": "2.1.0",
4
4
  "description": "jMuxer - a simple javascript mp4 muxer for non-standard streaming communications protocol",
5
5
  "main": "dist/jmuxer.min.js",
6
6
  "scripts": {
7
7
  "build": "rollup -c",
8
- "dev": "rollup -c -w",
8
+ "watch": "rollup -c -w",
9
+ "server": "cd example && nodemon server.mjs",
10
+ "dev": "concurrently --names \"BUILD,SERVER\" -c \"bgBlue.bold,bgMagenta.bold\" \"npm run watch\" \"npm run server\"",
9
11
  "pro": "NODE_ENV=production rollup -c",
10
12
  "lint": "eslint -c .eslintrc.json ./src/",
11
13
  "test": "karma start karma.conf.js"
@@ -42,17 +44,20 @@
42
44
  "@rollup/pluginutils": "^4.0.0",
43
45
  "caniuse-lite": "^1.0.30001223",
44
46
  "chai": "^4.2.0",
47
+ "concurrently": "^9.2.1",
45
48
  "eslint": "^8.2.0",
49
+ "express": "^5.1.0",
50
+ "express-ws": "^5.0.2",
46
51
  "karma": "^6.3.2",
47
52
  "karma-chai": "^0.1.0",
48
53
  "karma-chrome-launcher": "^3.1.0",
49
54
  "karma-mocha": "^2.0.1",
50
55
  "karma-rollup-preprocessor": "^7.0.5",
51
- "mocha": "^9.1.3",
52
- "patch-package": "^6.4.7",
56
+ "mocha": "^11.7.2",
57
+ "msgpack-lite": "^0.1.26",
58
+ "nodemon": "^3.1.10",
53
59
  "path": "^0.12.7",
54
60
  "rollup": "^2.59.0",
55
- "rollup-plugin-terser": "^7.0.2",
56
- "ws": "^8.2.3"
61
+ "rollup-plugin-terser": "^7.0.2"
57
62
  }
58
63
  }
@@ -2,38 +2,49 @@ import * as debug from '../util/debug';
2
2
  import { MP4 } from '../util/mp4-generator.js';
3
3
  import { AACRemuxer } from '../remuxer/aac.js';
4
4
  import { H264Remuxer } from '../remuxer/h264.js';
5
+ import { H265Remuxer } from '../remuxer/h265.js';
5
6
  import { appendByteArray, secToTime } from '../util/utils.js';
6
7
  import Event from '../util/event';
7
8
 
8
9
  export default class RemuxController extends Event {
9
10
 
10
- constructor(env) {
11
+ constructor(env, live, videoCodec, frameDuration) {
11
12
  super('remuxer');
13
+ this.videoCodec = videoCodec;
14
+ this.frameDuration = frameDuration;
12
15
  this.initialized = false;
13
- this.trackTypes = [];
14
16
  this.tracks = {};
15
17
  this.seq = 1;
16
18
  this.env = env;
17
19
  this.timescale = 1000;
18
- this.mediaDuration = 0;
19
- this.aacParser = null;
20
+ this.mediaDuration = live ? 0xffffffff : 0;
20
21
  }
21
22
 
22
23
  addTrack(type) {
23
24
  if (type === 'video' || type === 'both') {
24
- this.tracks.video = new H264Remuxer(this.timescale);
25
- this.trackTypes.push('video');
25
+ if (this.videoCodec == 'H265') {
26
+ this.tracks.video = new H265Remuxer(this.timescale, this.mediaDuration, this.frameDuration);
27
+ } else {
28
+ this.tracks.video = new H264Remuxer(this.timescale, this.mediaDuration, this.frameDuration);
29
+ }
30
+ this.tracks.video.on('outOfData', () => {
31
+ this.dispatch('missingVideoFrames');
32
+ });
33
+ this.tracks.video.on('keyframePosition', (time) => {
34
+ this.dispatch('keyframePosition', time);
35
+ });
26
36
  }
27
37
  if (type === 'audio' || type === 'both') {
28
- const aacRemuxer = new AACRemuxer(this.timescale);
29
- this.aacParser = aacRemuxer.getAacParser();
38
+ const aacRemuxer = new AACRemuxer(this.timescale, this.mediaDuration, this.frameDuration);
30
39
  this.tracks.audio = aacRemuxer;
31
- this.trackTypes.push('audio');
40
+ this.tracks.video.on('outOfData', () => {
41
+ this.dispatch('missingAudioFrames');
42
+ });
32
43
  }
33
44
  }
34
45
 
35
46
  reset() {
36
- for (let type of this.trackTypes) {
47
+ for (const type in this.tracks) {
37
48
  this.tracks[type].resetTrack();
38
49
  }
39
50
  this.initialized = false;
@@ -46,41 +57,40 @@ export default class RemuxController extends Event {
46
57
 
47
58
  flush() {
48
59
  if (!this.initialized) {
49
- if (this.isReady()) {
50
- this.dispatch('ready');
51
- this.initSegment();
52
- this.initialized = true;
53
- this.flush();
54
- }
55
- } else {
56
- for (let type of this.trackTypes) {
57
- let track = this.tracks[type];
58
- let pay = track.getPayload();
59
- if (pay && pay.byteLength) {
60
- const moof = MP4.moof(this.seq, track.dts, track.mp4track);
61
- const mdat = MP4.mdat(pay);
62
- let payload = appendByteArray(moof, mdat);
63
- let data = {
64
- type: type,
65
- payload: payload,
66
- dts: track.dts
67
- };
68
- if (type === 'video') {
69
- data.fps = track.mp4track.fps;
70
- }
71
- this.dispatch('buffer', data);
72
- let duration = secToTime(track.dts / this.timescale);
73
- debug.log(`put segment (${type}): dts: ${track.dts} frames: ${track.mp4track.samples.length} second: ${duration}`);
74
- track.flush();
75
- this.seq++;
60
+ if (!this.isReady()) return;
61
+
62
+ this.dispatch('ready');
63
+ this.initSegment();
64
+ this.initialized = true;
65
+ }
66
+
67
+ for (const type in this.tracks) {
68
+ let track = this.tracks[type];
69
+ let pay = track.getPayload();
70
+ if (pay && pay.byteLength) {
71
+ const moof = MP4.moof(this.seq, track.dts, track.mp4track);
72
+ const mdat = MP4.mdat(pay);
73
+ let payload = appendByteArray(moof, mdat);
74
+ let data = {
75
+ type: type,
76
+ payload: payload,
77
+ dts: track.dts
78
+ };
79
+ if (type === 'video') {
80
+ data.fps = track.mp4track.fps;
76
81
  }
82
+ this.dispatch('buffer', data);
83
+ let duration = secToTime(track.dts / this.timescale);
84
+ debug.log(`put segment (${type}): dts: ${track.dts} frames: ${track.mp4track.samples.length} second: ${duration}`);
85
+ track.flush();
86
+ this.seq++;
77
87
  }
78
88
  }
79
89
  }
80
90
 
81
91
  initSegment() {
82
92
  let tracks = [];
83
- for (let type of this.trackTypes) {
93
+ for (const type in this.tracks) {
84
94
  let track = this.tracks[type];
85
95
  if (this.env == 'browser') {
86
96
  let data = {
@@ -103,20 +113,27 @@ export default class RemuxController extends Event {
103
113
  }
104
114
 
105
115
  isReady() {
106
- for (let type of this.trackTypes) {
116
+ for (const type in this.tracks) {
107
117
  if (!this.tracks[type].readyToDecode || !this.tracks[type].samples.length) return false;
108
118
  }
109
119
  return true;
110
120
  }
111
121
 
112
- remux(data) {
113
- for (let type of this.trackTypes) {
114
- let frames = data[type];
115
- if (type === 'audio' && this.tracks.video && !this.tracks.video.readyToDecode) continue; /* if video is present, don't add audio until video get ready */
116
- if (frames.length > 0) {
117
- this.tracks[type].remux(frames);
118
- }
122
+ feed(data) {
123
+ let remux = false;
124
+
125
+ if (data.video && this.tracks.video) {
126
+ remux |= this.tracks.video.feed(data.video, data.duration, data.compositionTimeOffset);
119
127
  }
128
+ if (data.audio && this.tracks.audio) {
129
+ remux |= this.tracks.audio.feed(data.audio, data.duration);
130
+ }
131
+
132
+ if (!remux) {
133
+ debug.error('Input object must have video and/or audio property. Make sure it is a valid typed array');
134
+ return;
135
+ }
136
+
120
137
  this.flush();
121
138
  }
122
139
  }
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
  }
@@ -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
  }