@lumen5/beamcoder 0.0.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/.circleci/config.yml +41 -0
- package/.circleci/images/testbeam10-4.1/Dockerfile +12 -0
- package/.circleci/test_image/Dockerfile +14 -0
- package/.circleci/test_image/build.md +13 -0
- package/.eslintrc.js +27 -0
- package/.github/workflows/publish-npm.yml +33 -0
- package/LICENSE +674 -0
- package/README.md +1221 -0
- package/beamstreams.js +692 -0
- package/binding.gyp +103 -0
- package/examples/encode_h264.js +92 -0
- package/examples/jpeg_app.js +55 -0
- package/examples/jpeg_filter_app.js +101 -0
- package/examples/make_mp4.js +123 -0
- package/images/beamcoder_small.jpg +0 -0
- package/index.d.ts +83 -0
- package/index.js +44 -0
- package/install_ffmpeg.js +240 -0
- package/package.json +45 -0
- package/scratch/decode_aac.js +38 -0
- package/scratch/decode_avci.js +50 -0
- package/scratch/decode_hevc.js +38 -0
- package/scratch/decode_pcm.js +39 -0
- package/scratch/make_a_mux.js +68 -0
- package/scratch/muxer.js +74 -0
- package/scratch/read_wav.js +35 -0
- package/scratch/simple_mux.js +39 -0
- package/scratch/stream_avci.js +127 -0
- package/scratch/stream_mp4.js +78 -0
- package/scratch/stream_mux.js +47 -0
- package/scratch/stream_pcm.js +82 -0
- package/scratch/stream_wav.js +62 -0
- package/scripts/install_beamcoder_dependencies.sh +25 -0
- package/src/adaptor.h +202 -0
- package/src/beamcoder.cc +937 -0
- package/src/beamcoder_util.cc +1129 -0
- package/src/beamcoder_util.h +206 -0
- package/src/codec.cc +7386 -0
- package/src/codec.h +44 -0
- package/src/codec_par.cc +1818 -0
- package/src/codec_par.h +40 -0
- package/src/decode.cc +569 -0
- package/src/decode.h +75 -0
- package/src/demux.cc +584 -0
- package/src/demux.h +88 -0
- package/src/encode.cc +496 -0
- package/src/encode.h +72 -0
- package/src/filter.cc +1888 -0
- package/src/filter.h +30 -0
- package/src/format.cc +5287 -0
- package/src/format.h +77 -0
- package/src/frame.cc +2681 -0
- package/src/frame.h +52 -0
- package/src/governor.cc +286 -0
- package/src/governor.h +30 -0
- package/src/hwcontext.cc +378 -0
- package/src/hwcontext.h +35 -0
- package/src/log.cc +186 -0
- package/src/log.h +20 -0
- package/src/mux.cc +834 -0
- package/src/mux.h +106 -0
- package/src/packet.cc +762 -0
- package/src/packet.h +49 -0
- package/test/codecParamsSpec.js +148 -0
- package/test/decoderSpec.js +56 -0
- package/test/demuxerSpec.js +41 -0
- package/test/encoderSpec.js +69 -0
- package/test/filtererSpec.js +47 -0
- package/test/formatSpec.js +343 -0
- package/test/frameSpec.js +145 -0
- package/test/introspectionSpec.js +73 -0
- package/test/muxerSpec.js +34 -0
- package/test/packetSpec.js +122 -0
- package/types/Beamstreams.d.ts +98 -0
- package/types/Codec.d.ts +123 -0
- package/types/CodecContext.d.ts +555 -0
- package/types/CodecPar.d.ts +108 -0
- package/types/Decoder.d.ts +137 -0
- package/types/Demuxer.d.ts +113 -0
- package/types/Encoder.d.ts +94 -0
- package/types/Filter.d.ts +324 -0
- package/types/FormatContext.d.ts +380 -0
- package/types/Frame.d.ts +295 -0
- package/types/HWContext.d.ts +62 -0
- package/types/Muxer.d.ts +121 -0
- package/types/Packet.d.ts +82 -0
- package/types/PrivClass.d.ts +25 -0
- package/types/Stream.d.ts +165 -0
package/beamstreams.js
ADDED
|
@@ -0,0 +1,692 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Aerostat Beam Coder - Node.js native bindings to FFmpeg
|
|
3
|
+
Copyright (C) 2019 Streampunk Media Ltd.
|
|
4
|
+
|
|
5
|
+
This program is free software: you can redistribute it and/or modify
|
|
6
|
+
it under the terms of the GNU General Public License as published by
|
|
7
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
(at your option) any later version.
|
|
9
|
+
|
|
10
|
+
This program is distributed in the hope that it will be useful,
|
|
11
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
GNU General Public License for more details.
|
|
14
|
+
|
|
15
|
+
You should have received a copy of the GNU General Public License
|
|
16
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
|
|
18
|
+
https://www.streampunk.media/ mailto:furnace@streampunk.media
|
|
19
|
+
14 Ormiscaig, Aultbea, Achnasheen, IV22 2JJ U.K.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const beamcoder = require('bindings')('beamcoder');
|
|
23
|
+
const { Writable, Readable, Transform } = require('stream');
|
|
24
|
+
|
|
25
|
+
const doTimings = false;
|
|
26
|
+
const timings = [];
|
|
27
|
+
|
|
28
|
+
function frameDicer(encoder, isAudio) {
|
|
29
|
+
let sampleBytes = 4; // Assume floating point 4 byte samples for now...
|
|
30
|
+
const numChannels = encoder.channels;
|
|
31
|
+
const dstNumSamples = encoder.frame_size;
|
|
32
|
+
let dstFrmBytes = dstNumSamples * sampleBytes;
|
|
33
|
+
const doDice = false === beamcoder.encoders()[encoder.name].capabilities.VARIABLE_FRAME_SIZE;
|
|
34
|
+
|
|
35
|
+
let lastFrm = null;
|
|
36
|
+
let lastBuf = [];
|
|
37
|
+
const nullBuf = [];
|
|
38
|
+
for (let b = 0; b < numChannels; ++b)
|
|
39
|
+
nullBuf.push(Buffer.alloc(0));
|
|
40
|
+
|
|
41
|
+
const addFrame = srcFrm => {
|
|
42
|
+
let result = [];
|
|
43
|
+
let dstFrm;
|
|
44
|
+
let curStart = 0;
|
|
45
|
+
if (!lastFrm) {
|
|
46
|
+
lastFrm = beamcoder.frame(srcFrm.toJSON());
|
|
47
|
+
lastBuf = nullBuf;
|
|
48
|
+
dstFrmBytes = dstNumSamples * sampleBytes;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (lastBuf[0].length > 0)
|
|
52
|
+
dstFrm = beamcoder.frame(lastFrm.toJSON());
|
|
53
|
+
else
|
|
54
|
+
dstFrm = beamcoder.frame(srcFrm.toJSON());
|
|
55
|
+
dstFrm.nb_samples = dstNumSamples;
|
|
56
|
+
dstFrm.pkt_duration = dstNumSamples;
|
|
57
|
+
|
|
58
|
+
while (curStart + dstFrmBytes - lastBuf[0].length <= srcFrm.nb_samples * sampleBytes) {
|
|
59
|
+
const resFrm = beamcoder.frame(dstFrm.toJSON());
|
|
60
|
+
resFrm.data = lastBuf.map((d, i) =>
|
|
61
|
+
Buffer.concat([
|
|
62
|
+
d, srcFrm.data[i].slice(curStart, curStart + dstFrmBytes - d.length)],
|
|
63
|
+
dstFrmBytes));
|
|
64
|
+
result.push(resFrm);
|
|
65
|
+
|
|
66
|
+
dstFrm.pts += dstNumSamples;
|
|
67
|
+
dstFrm.pkt_dts += dstNumSamples;
|
|
68
|
+
curStart += dstFrmBytes - lastBuf[0].length;
|
|
69
|
+
lastFrm.pts = 0;
|
|
70
|
+
lastFrm.pkt_dts = 0;
|
|
71
|
+
lastBuf = nullBuf;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
lastFrm.pts = dstFrm.pts;
|
|
75
|
+
lastFrm.pkt_dts = dstFrm.pkt_dts;
|
|
76
|
+
lastBuf = srcFrm.data.map(d => d.slice(curStart, srcFrm.nb_samples * sampleBytes));
|
|
77
|
+
|
|
78
|
+
return result;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const getLast = () => {
|
|
82
|
+
let result = [];
|
|
83
|
+
if (lastBuf[0].length > 0) {
|
|
84
|
+
const resFrm = beamcoder.frame(lastFrm.toJSON());
|
|
85
|
+
resFrm.data = lastBuf.map(d => d.slice(0));
|
|
86
|
+
resFrm.nb_samples = lastBuf[0].length / sampleBytes;
|
|
87
|
+
resFrm.pkt_duration = resFrm.nb_samples;
|
|
88
|
+
lastFrm.pts = 0;
|
|
89
|
+
lastBuf = nullBuf;
|
|
90
|
+
result.push(resFrm);
|
|
91
|
+
}
|
|
92
|
+
return result;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
this.dice = (frames, flush = false) => {
|
|
96
|
+
if (isAudio && doDice) {
|
|
97
|
+
let result = frames.reduce((muxFrms, frm) => {
|
|
98
|
+
addFrame(frm).forEach(f => muxFrms.push(f));
|
|
99
|
+
return muxFrms;
|
|
100
|
+
}, []);
|
|
101
|
+
|
|
102
|
+
if (flush)
|
|
103
|
+
getLast().forEach(f => result.push(f));
|
|
104
|
+
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return frames;
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function serialBalancer(numStreams) {
|
|
113
|
+
let pending = [];
|
|
114
|
+
// initialise with negative ts and no pkt
|
|
115
|
+
// - there should be no output until each stream has sent its first packet
|
|
116
|
+
for (let s = 0; s < numStreams; ++s)
|
|
117
|
+
pending.push({ ts: -Number.MAX_VALUE, streamIndex: s });
|
|
118
|
+
|
|
119
|
+
const adjustTS = (pkt, srcTB, dstTB) => {
|
|
120
|
+
const adj = (srcTB[0] * dstTB[1]) / (srcTB[1] * dstTB[0]);
|
|
121
|
+
pkt.pts = Math.round(pkt.pts * adj);
|
|
122
|
+
pkt.dts = Math.round(pkt.dts * adj);
|
|
123
|
+
pkt.duration > 0 ? Math.round(pkt.duration * adj) : Math.round(adj);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const pullPkts = (pkt, streamIndex, ts) => {
|
|
127
|
+
return new Promise(resolve => {
|
|
128
|
+
Object.assign(pending[streamIndex], { pkt: pkt, ts: ts, resolve: resolve });
|
|
129
|
+
const minTS = pending.reduce((acc, pend) => Math.min(acc, pend.ts), Number.MAX_VALUE);
|
|
130
|
+
// console.log(streamIndex, pending.map(p => p.ts), minTS);
|
|
131
|
+
const nextPend = pending.find(pend => pend.pkt && (pend.ts === minTS));
|
|
132
|
+
if (nextPend) nextPend.resolve(nextPend.pkt);
|
|
133
|
+
if (!pkt) resolve();
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
this.writePkts = (packets, srcStream, dstStream, writeFn, final = false) => {
|
|
138
|
+
if (packets && packets.packets.length) {
|
|
139
|
+
return packets.packets.reduce(async (promise, pkt) => {
|
|
140
|
+
await promise;
|
|
141
|
+
pkt.stream_index = dstStream.index;
|
|
142
|
+
adjustTS(pkt, srcStream.time_base, dstStream.time_base);
|
|
143
|
+
const pktTS = pkt.pts * dstStream.time_base[0] / dstStream.time_base[1];
|
|
144
|
+
return writeFn(await pullPkts(pkt, dstStream.index, pktTS));
|
|
145
|
+
}, Promise.resolve());
|
|
146
|
+
} else if (final)
|
|
147
|
+
return pullPkts(null, dstStream.index, Number.MAX_VALUE);
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function parallelBalancer(params, streamType, numStreams) {
|
|
152
|
+
let resolveGet = null;
|
|
153
|
+
const tag = 'video' === streamType ? 'v' : 'a';
|
|
154
|
+
const pending = [];
|
|
155
|
+
// initialise with negative ts and no pkt
|
|
156
|
+
// - there should be no output until each stream has sent its first packet
|
|
157
|
+
for (let s = 0; s < numStreams; ++s)
|
|
158
|
+
pending.push({ ts: -Number.MAX_VALUE, streamIndex: s });
|
|
159
|
+
|
|
160
|
+
const makeSet = resolve => {
|
|
161
|
+
if (resolve) {
|
|
162
|
+
// console.log('makeSet', pending.map(p => p.ts));
|
|
163
|
+
const nextPends = pending.every(pend => pend.pkt) ? pending : null;
|
|
164
|
+
const final = pending.filter(pend => true === pend.final);
|
|
165
|
+
if (nextPends) {
|
|
166
|
+
nextPends.forEach(pend => pend.resolve());
|
|
167
|
+
resolve({
|
|
168
|
+
value: nextPends.map(pend => {
|
|
169
|
+
return { name: `in${pend.streamIndex}:${tag}`, frames: [ pend.pkt ] }; }),
|
|
170
|
+
done: false });
|
|
171
|
+
resolveGet = null;
|
|
172
|
+
pending.forEach(pend => Object.assign(pend, { pkt: null, ts: Number.MAX_VALUE }));
|
|
173
|
+
} else if (final.length > 0) {
|
|
174
|
+
final.forEach(f => f.resolve());
|
|
175
|
+
resolve({ done: true });
|
|
176
|
+
} else {
|
|
177
|
+
resolveGet = resolve;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const pushPkt = async (pkt, streamIndex, ts) =>
|
|
183
|
+
new Promise(resolve => {
|
|
184
|
+
Object.assign(pending[streamIndex], { pkt: pkt, ts: ts, final: pkt ? false : true, resolve: resolve });
|
|
185
|
+
makeSet(resolveGet);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const pullSet = async () => new Promise(resolve => makeSet(resolve));
|
|
189
|
+
|
|
190
|
+
const readStream = new Readable({
|
|
191
|
+
objectMode: true,
|
|
192
|
+
highWaterMark: params.highWaterMark ? params.highWaterMark || 4 : 4,
|
|
193
|
+
read() {
|
|
194
|
+
(async () => {
|
|
195
|
+
const start = process.hrtime();
|
|
196
|
+
const reqTime = start[0] * 1e3 + start[1] / 1e6;
|
|
197
|
+
const result = await pullSet();
|
|
198
|
+
if (result.done)
|
|
199
|
+
this.push(null);
|
|
200
|
+
else {
|
|
201
|
+
result.value.timings = result.value[0].frames[0].timings;
|
|
202
|
+
result.value.timings[params.name] = { reqTime: reqTime, elapsed: process.hrtime(start)[1] / 1000000 };
|
|
203
|
+
this.push(result.value);
|
|
204
|
+
}
|
|
205
|
+
})();
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
readStream.pushPkts = (packets, stream, streamIndex, final = false) => {
|
|
210
|
+
if (packets && packets.frames.length) {
|
|
211
|
+
return packets.frames.reduce(async (promise, pkt) => {
|
|
212
|
+
await promise;
|
|
213
|
+
const ts = pkt.pts * stream.time_base[0] / stream.time_base[1];
|
|
214
|
+
pkt.timings = packets.timings;
|
|
215
|
+
return pushPkt(pkt, streamIndex, ts);
|
|
216
|
+
}, Promise.resolve());
|
|
217
|
+
} else if (final) {
|
|
218
|
+
return pushPkt(null, streamIndex, Number.MAX_VALUE);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
return readStream;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function teeBalancer(params, numStreams) {
|
|
226
|
+
let resolvePush = null;
|
|
227
|
+
const pending = [];
|
|
228
|
+
for (let s = 0; s < numStreams; ++s)
|
|
229
|
+
pending.push({ frames: null, resolve: null, final: false });
|
|
230
|
+
|
|
231
|
+
const pullFrame = async index => {
|
|
232
|
+
return new Promise(resolve => {
|
|
233
|
+
if (pending[index].frames) {
|
|
234
|
+
resolve({ value: pending[index].frames, done: false });
|
|
235
|
+
Object.assign(pending[index], { frames: null, resolve: null });
|
|
236
|
+
} else if (pending[index].final)
|
|
237
|
+
resolve({ done: true });
|
|
238
|
+
else
|
|
239
|
+
pending[index].resolve = resolve;
|
|
240
|
+
|
|
241
|
+
if (resolvePush && pending.every(p => null === p.frames)) {
|
|
242
|
+
resolvePush();
|
|
243
|
+
resolvePush = null;
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const readStreams = [];
|
|
249
|
+
for (let s = 0; s < numStreams; ++s)
|
|
250
|
+
readStreams.push(new Readable({
|
|
251
|
+
objectMode: true,
|
|
252
|
+
highWaterMark: params.highWaterMark ? params.highWaterMark || 4 : 4,
|
|
253
|
+
read() {
|
|
254
|
+
(async () => {
|
|
255
|
+
const start = process.hrtime();
|
|
256
|
+
const reqTime = start[0] * 1e3 + start[1] / 1e6;
|
|
257
|
+
const result = await pullFrame(s);
|
|
258
|
+
if (result.done)
|
|
259
|
+
this.push(null);
|
|
260
|
+
else {
|
|
261
|
+
result.value.timings[params.name] = { reqTime: reqTime, elapsed: process.hrtime(start)[1] / 1000000 };
|
|
262
|
+
this.push(result.value);
|
|
263
|
+
}
|
|
264
|
+
})();
|
|
265
|
+
},
|
|
266
|
+
}));
|
|
267
|
+
|
|
268
|
+
readStreams.pushFrames = frames => {
|
|
269
|
+
return new Promise(resolve => {
|
|
270
|
+
pending.forEach((p, index) => {
|
|
271
|
+
if (frames.length)
|
|
272
|
+
p.frames = frames[index].frames;
|
|
273
|
+
else
|
|
274
|
+
p.final = true;
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
pending.forEach(p => {
|
|
278
|
+
if (p.resolve) {
|
|
279
|
+
if (p.frames) {
|
|
280
|
+
p.frames.timings = frames.timings;
|
|
281
|
+
p.resolve({ value: p.frames, done: false });
|
|
282
|
+
} else if (p.final)
|
|
283
|
+
p.resolve({ done: true });
|
|
284
|
+
}
|
|
285
|
+
Object.assign(p, { frames: null, resolve: null });
|
|
286
|
+
});
|
|
287
|
+
resolvePush = resolve;
|
|
288
|
+
});
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
return readStreams;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function transformStream(params, processFn, flushFn, reject) {
|
|
295
|
+
return new Transform({
|
|
296
|
+
objectMode: true,
|
|
297
|
+
highWaterMark: params.highWaterMark ? params.highWaterMark || 4 : 4,
|
|
298
|
+
transform(val, encoding, cb) {
|
|
299
|
+
(async () => {
|
|
300
|
+
const start = process.hrtime();
|
|
301
|
+
const reqTime = start[0] * 1e3 + start[1] / 1e6;
|
|
302
|
+
const result = await processFn(val);
|
|
303
|
+
result.timings = val.timings;
|
|
304
|
+
if (result.timings)
|
|
305
|
+
result.timings[params.name] = { reqTime: reqTime, elapsed: process.hrtime(start)[1] / 1000000 };
|
|
306
|
+
cb(null, result);
|
|
307
|
+
})().catch(cb);
|
|
308
|
+
},
|
|
309
|
+
flush(cb) {
|
|
310
|
+
(async () => {
|
|
311
|
+
const result = flushFn ? await flushFn() : null;
|
|
312
|
+
if (result) result.timings = {};
|
|
313
|
+
cb(null, result);
|
|
314
|
+
})().catch(cb);
|
|
315
|
+
}
|
|
316
|
+
}).on('error', err => reject(err));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const calcStats = (arr, elem, prop) => {
|
|
320
|
+
const mean = arr.reduce((acc, cur) => cur[elem] ? acc + cur[elem][prop] : acc, 0) / arr.length;
|
|
321
|
+
const stdDev = Math.pow(arr.reduce((acc, cur) => cur[elem] ? acc + Math.pow(cur[elem][prop] - mean, 2) : acc, 0) / arr.length, 0.5);
|
|
322
|
+
const max = arr.reduce((acc, cur) => cur[elem] ? Math.max(cur[elem][prop], acc) : acc, 0);
|
|
323
|
+
const min = arr.reduce((acc, cur) => cur[elem] ? Math.min(cur[elem][prop], acc) : acc, Number.MAX_VALUE);
|
|
324
|
+
return { mean: mean, stdDev: stdDev, max: max, min: min };
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
function writeStream(params, processFn, finalFn, reject) {
|
|
328
|
+
return new Writable({
|
|
329
|
+
objectMode: true,
|
|
330
|
+
highWaterMark: params.highWaterMark ? params.highWaterMark || 4 : 4,
|
|
331
|
+
write(val, encoding, cb) {
|
|
332
|
+
(async () => {
|
|
333
|
+
const start = process.hrtime();
|
|
334
|
+
const reqTime = start[0] * 1e3 + start[1] / 1e6;
|
|
335
|
+
const result = await processFn(val);
|
|
336
|
+
if ('mux' === params.name) {
|
|
337
|
+
const pktTimings = val.timings;
|
|
338
|
+
pktTimings[params.name] = { reqTime: reqTime, elapsed: process.hrtime(start)[1] / 1000000 };
|
|
339
|
+
if (doTimings)
|
|
340
|
+
timings.push(pktTimings);
|
|
341
|
+
}
|
|
342
|
+
cb(null, result);
|
|
343
|
+
})().catch(cb);
|
|
344
|
+
},
|
|
345
|
+
final(cb) {
|
|
346
|
+
(async () => {
|
|
347
|
+
const result = finalFn ? await finalFn() : null;
|
|
348
|
+
if (doTimings && ('mux' === params.name)) {
|
|
349
|
+
const elapsedStats = {};
|
|
350
|
+
Object.keys(timings[0]).forEach(k => elapsedStats[k] = calcStats(timings.slice(10, -10), k, 'elapsed'));
|
|
351
|
+
console.log('elapsed:');
|
|
352
|
+
console.table(elapsedStats);
|
|
353
|
+
|
|
354
|
+
const absArr = timings.map(t => {
|
|
355
|
+
const absDelays = {};
|
|
356
|
+
const keys = Object.keys(t);
|
|
357
|
+
keys.forEach((k, i) => absDelays[k] = { reqDelta: i > 0 ? t[k].reqTime - t[keys[i-1]].reqTime : 0 });
|
|
358
|
+
return absDelays;
|
|
359
|
+
});
|
|
360
|
+
const absStats = {};
|
|
361
|
+
Object.keys(absArr[0]).forEach(k => absStats[k] = calcStats(absArr.slice(10, -10), k, 'reqDelta'));
|
|
362
|
+
console.log('request time delta:');
|
|
363
|
+
console.table(absStats);
|
|
364
|
+
|
|
365
|
+
const totalsArr = timings.map(t => {
|
|
366
|
+
const total = (t.mux && t.read) ? t.mux.reqTime - t.read.reqTime + t.mux.elapsed : 0;
|
|
367
|
+
return { total: { total: total }};
|
|
368
|
+
});
|
|
369
|
+
console.log('total time:');
|
|
370
|
+
console.table(calcStats(totalsArr.slice(10, -10), 'total', 'total'));
|
|
371
|
+
}
|
|
372
|
+
cb(null, result);
|
|
373
|
+
})().catch(cb);
|
|
374
|
+
}
|
|
375
|
+
}).on('error', err => reject(err));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function readStream(params, demuxer, ms, index) {
|
|
379
|
+
const time_base = demuxer.streams[index].time_base;
|
|
380
|
+
const end_pts = ms ? ms.end * time_base[1] / time_base[0] : Number.MAX_SAFE_INTEGER;
|
|
381
|
+
async function getPacket() {
|
|
382
|
+
let packet = null;
|
|
383
|
+
do { packet = await demuxer.read(); }
|
|
384
|
+
while (packet && packet.stream_index !== index);
|
|
385
|
+
return packet;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return new Readable({
|
|
389
|
+
objectMode: true,
|
|
390
|
+
highWaterMark: params.highWaterMark ? params.highWaterMark || 4 : 4,
|
|
391
|
+
read() {
|
|
392
|
+
(async () => {
|
|
393
|
+
const start = process.hrtime();
|
|
394
|
+
const reqTime = start[0] * 1e3 + start[1] / 1e6;
|
|
395
|
+
const packet = await getPacket();
|
|
396
|
+
if (packet && (packet.pts < end_pts)) {
|
|
397
|
+
packet.timings = {};
|
|
398
|
+
packet.timings.read = { reqTime: reqTime, elapsed: process.hrtime(start)[1] / 1000000 };
|
|
399
|
+
this.push(packet);
|
|
400
|
+
} else
|
|
401
|
+
this.push(null);
|
|
402
|
+
})();
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function createBeamWritableStream(params, governor) {
|
|
408
|
+
const beamStream = new Writable({
|
|
409
|
+
highWaterMark: params.highwaterMark || 16384,
|
|
410
|
+
write: (chunk, encoding, cb) => {
|
|
411
|
+
(async () => {
|
|
412
|
+
await governor.write(chunk);
|
|
413
|
+
cb();
|
|
414
|
+
})();
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
return beamStream;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function demuxerStream(params) {
|
|
421
|
+
const governor = new beamcoder.governor({});
|
|
422
|
+
const stream = createBeamWritableStream(params, governor);
|
|
423
|
+
stream.on('finish', () => governor.finish());
|
|
424
|
+
stream.on('error', console.error);
|
|
425
|
+
stream.demuxer = options => {
|
|
426
|
+
options.governor = governor;
|
|
427
|
+
// delay initialisation of demuxer until stream has been written to - avoids lock-up
|
|
428
|
+
return new Promise(resolve => setTimeout(async () => resolve(await beamcoder.demuxer(options)), 20));
|
|
429
|
+
};
|
|
430
|
+
return stream;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function createBeamReadableStream(params, governor) {
|
|
434
|
+
const beamStream = new Readable({
|
|
435
|
+
highWaterMark: params.highwaterMark || 16384,
|
|
436
|
+
read: size => {
|
|
437
|
+
(async () => {
|
|
438
|
+
const chunk = await governor.read(size);
|
|
439
|
+
if (0 === chunk.length)
|
|
440
|
+
beamStream.push(null);
|
|
441
|
+
else
|
|
442
|
+
beamStream.push(chunk);
|
|
443
|
+
})();
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
return beamStream;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function muxerStream(params) {
|
|
450
|
+
const governor = new beamcoder.governor({ highWaterMark: 1 });
|
|
451
|
+
const stream = createBeamReadableStream(params, governor);
|
|
452
|
+
stream.on('end', () => governor.finish());
|
|
453
|
+
stream.on('error', console.error);
|
|
454
|
+
stream.muxer = options => {
|
|
455
|
+
options.governor = governor;
|
|
456
|
+
return beamcoder.muxer(options);
|
|
457
|
+
};
|
|
458
|
+
return stream;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
async function makeSources(params) {
|
|
462
|
+
if (!params.video) params.video = [];
|
|
463
|
+
if (!params.audio) params.audio = [];
|
|
464
|
+
|
|
465
|
+
params.video.forEach(p => p.sources.forEach(src => {
|
|
466
|
+
if (src.input_stream) {
|
|
467
|
+
const demuxerStream = beamcoder.demuxerStream({ highwaterMark: 1024 });
|
|
468
|
+
src.input_stream.pipe(demuxerStream);
|
|
469
|
+
src.format = demuxerStream.demuxer({ iformat: src.iformat, options: src.options });
|
|
470
|
+
} else
|
|
471
|
+
src.format = beamcoder.demuxer({ url: src.url, iformat: src.iformat, options: src.options });
|
|
472
|
+
}));
|
|
473
|
+
params.audio.forEach(p => p.sources.forEach(src => {
|
|
474
|
+
if (src.input_stream) {
|
|
475
|
+
const demuxerStream = beamcoder.demuxerStream({ highwaterMark: 1024 });
|
|
476
|
+
src.input_stream.pipe(demuxerStream);
|
|
477
|
+
src.format = demuxerStream.demuxer({ iformat: src.iformat, options: src.options });
|
|
478
|
+
} else
|
|
479
|
+
src.format = beamcoder.demuxer({ url: src.url, iformat: src.iformat, options: src.options });
|
|
480
|
+
}));
|
|
481
|
+
|
|
482
|
+
await params.video.reduce(async (promise, p) => {
|
|
483
|
+
await promise;
|
|
484
|
+
return p.sources.reduce(async (promise, src) => {
|
|
485
|
+
await promise;
|
|
486
|
+
src.format = await src.format;
|
|
487
|
+
if (src.ms && !src.input_stream)
|
|
488
|
+
src.format.seek({ time: src.ms.start });
|
|
489
|
+
return src.format;
|
|
490
|
+
}, Promise.resolve());
|
|
491
|
+
}, Promise.resolve());
|
|
492
|
+
await params.audio.reduce(async (promise, p) => {
|
|
493
|
+
await promise;
|
|
494
|
+
return p.sources.reduce(async (promise, src) => {
|
|
495
|
+
await promise;
|
|
496
|
+
src.format = await src.format;
|
|
497
|
+
if (src.ms && !src.input_stream)
|
|
498
|
+
src.format.seek({ time: src.ms.start });
|
|
499
|
+
return src.format;
|
|
500
|
+
}, Promise.resolve());
|
|
501
|
+
}, Promise.resolve());
|
|
502
|
+
|
|
503
|
+
params.video.forEach(p => p.sources.forEach(src =>
|
|
504
|
+
src.stream = readStream({ highWaterMark : 1 }, src.format, src.ms, src.streamIndex)));
|
|
505
|
+
params.audio.forEach(p => p.sources.forEach(src =>
|
|
506
|
+
src.stream = readStream({ highWaterMark : 1 }, src.format, src.ms, src.streamIndex)));
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function runStreams(streamType, sources, filterer, streams, mux, muxBalancer) {
|
|
510
|
+
return new Promise((resolve, reject) => {
|
|
511
|
+
if (!sources.length)
|
|
512
|
+
return resolve();
|
|
513
|
+
|
|
514
|
+
const timeBaseStream = sources[0].format.streams[sources[0].streamIndex];
|
|
515
|
+
const filterBalancer = parallelBalancer({ name: 'filterBalance', highWaterMark : 1 }, streamType, sources.length);
|
|
516
|
+
|
|
517
|
+
sources.forEach((src, srcIndex) => {
|
|
518
|
+
const decStream = transformStream({ name: 'decode', highWaterMark : 1 },
|
|
519
|
+
pkts => src.decoder.decode(pkts), () => src.decoder.flush(), reject);
|
|
520
|
+
const filterSource = writeStream({ name: 'filterSource', highWaterMark : 1 },
|
|
521
|
+
pkts => filterBalancer.pushPkts(pkts, src.format.streams[src.streamIndex], srcIndex),
|
|
522
|
+
() => filterBalancer.pushPkts(null, src.format.streams[src.streamIndex], srcIndex, true), reject);
|
|
523
|
+
|
|
524
|
+
src.stream.pipe(decStream).pipe(filterSource);
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
const streamTee = teeBalancer({ name: 'streamTee', highWaterMark : 1 }, streams.length);
|
|
528
|
+
const filtStream = transformStream({ name: 'filter', highWaterMark : 1 }, frms => {
|
|
529
|
+
if (filterer.cb) filterer.cb(frms[0].frames[0].pts);
|
|
530
|
+
return filterer.filter(frms);
|
|
531
|
+
}, () => {}, reject);
|
|
532
|
+
const streamSource = writeStream({ name: 'streamSource', highWaterMark : 1 },
|
|
533
|
+
frms => streamTee.pushFrames(frms), () => streamTee.pushFrames([], true), reject);
|
|
534
|
+
|
|
535
|
+
filterBalancer.pipe(filtStream).pipe(streamSource);
|
|
536
|
+
|
|
537
|
+
streams.forEach((str, i) => {
|
|
538
|
+
const dicer = new frameDicer(str.encoder, 'audio' === streamType);
|
|
539
|
+
const diceStream = transformStream({ name: 'dice', highWaterMark : 1 },
|
|
540
|
+
frms => dicer.dice(frms), () => dicer.dice([], true), reject);
|
|
541
|
+
const encStream = transformStream({ name: 'encode', highWaterMark : 1 },
|
|
542
|
+
frms => str.encoder.encode(frms), () => str.encoder.flush(), reject);
|
|
543
|
+
const muxStream = writeStream({ name: 'mux', highWaterMark : 1 },
|
|
544
|
+
pkts => muxBalancer.writePkts(pkts, timeBaseStream, str.stream, pkts => mux.writeFrame(pkts)),
|
|
545
|
+
() => muxBalancer.writePkts(null, timeBaseStream, str.stream, pkts => mux.writeFrame(pkts), true), reject);
|
|
546
|
+
muxStream.on('finish', resolve);
|
|
547
|
+
|
|
548
|
+
streamTee[i].pipe(diceStream).pipe(encStream).pipe(muxStream);
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
async function makeStreams(params) {
|
|
554
|
+
params.video.forEach(p => {
|
|
555
|
+
p.sources.forEach(src =>
|
|
556
|
+
src.decoder = beamcoder.decoder({ demuxer: src.format, stream_index: src.streamIndex }));
|
|
557
|
+
});
|
|
558
|
+
params.audio.forEach(p => {
|
|
559
|
+
p.sources.forEach(src =>
|
|
560
|
+
src.decoder = beamcoder.decoder({ demuxer: src.format, stream_index: src.streamIndex }));
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
params.video.forEach(p => {
|
|
564
|
+
p.filter = beamcoder.filterer({
|
|
565
|
+
filterType: 'video',
|
|
566
|
+
inputParams: p.sources.map((src, i) => {
|
|
567
|
+
const stream = src.format.streams[src.streamIndex];
|
|
568
|
+
return {
|
|
569
|
+
name: `in${i}:v`,
|
|
570
|
+
width: stream.codecpar.width,
|
|
571
|
+
height: stream.codecpar.height,
|
|
572
|
+
pixelFormat: stream.codecpar.format,
|
|
573
|
+
timeBase: stream.time_base,
|
|
574
|
+
pixelAspect: stream.sample_aspect_ratio };
|
|
575
|
+
}),
|
|
576
|
+
outputParams: p.streams.map((str, i) => { return { name: `out${i}:v`, pixelFormat: str.codecpar.format }; }),
|
|
577
|
+
filterSpec: p.filterSpec });
|
|
578
|
+
});
|
|
579
|
+
const vidFilts = await Promise.all(params.video.map(p => p.filter));
|
|
580
|
+
params.video.forEach((p, i) => p.filter = vidFilts[i]);
|
|
581
|
+
// params.video.forEach(p => console.log(p.filter.graph.dump()));
|
|
582
|
+
|
|
583
|
+
params.audio.forEach(p => {
|
|
584
|
+
p.filter = beamcoder.filterer({
|
|
585
|
+
filterType: 'audio',
|
|
586
|
+
inputParams: p.sources.map((src, i) => {
|
|
587
|
+
const stream = src.format.streams[src.streamIndex];
|
|
588
|
+
return {
|
|
589
|
+
name: `in${i}:a`,
|
|
590
|
+
sampleRate: src.decoder.sample_rate,
|
|
591
|
+
sampleFormat: src.decoder.sample_fmt,
|
|
592
|
+
channelLayout: src.decoder.channel_layout,
|
|
593
|
+
timeBase: stream.time_base };
|
|
594
|
+
}),
|
|
595
|
+
outputParams: p.streams.map((str, i) => {
|
|
596
|
+
return {
|
|
597
|
+
name: `out${i}:a`,
|
|
598
|
+
sampleRate: str.codecpar.sample_rate,
|
|
599
|
+
sampleFormat: str.codecpar.format,
|
|
600
|
+
channelLayout: str.codecpar.channel_layout }; }),
|
|
601
|
+
filterSpec: p.filterSpec });
|
|
602
|
+
});
|
|
603
|
+
const audFilts = await Promise.all(params.audio.map(p => p.filter));
|
|
604
|
+
params.audio.forEach((p, i) => p.filter = audFilts[i]);
|
|
605
|
+
// params.audio.forEach(p => console.log(p.filter.graph.dump()));
|
|
606
|
+
|
|
607
|
+
let mux;
|
|
608
|
+
if (params.out.output_stream) {
|
|
609
|
+
let muxerStream = beamcoder.muxerStream({ highwaterMark: 1024 });
|
|
610
|
+
muxerStream.pipe(params.out.output_stream);
|
|
611
|
+
mux = muxerStream.muxer({ format_name: params.out.formatName });
|
|
612
|
+
} else
|
|
613
|
+
mux = beamcoder.muxer({ format_name: params.out.formatName });
|
|
614
|
+
|
|
615
|
+
params.video.forEach(p => {
|
|
616
|
+
p.streams.forEach((str, i) => {
|
|
617
|
+
const encParams = p.filter.graph.filters.find(f => f.name === `out${i}:v`).inputs[0];
|
|
618
|
+
str.encoder = beamcoder.encoder({
|
|
619
|
+
name: str.name,
|
|
620
|
+
width: encParams.w,
|
|
621
|
+
height: encParams.h,
|
|
622
|
+
pix_fmt: encParams.format,
|
|
623
|
+
sample_aspect_ratio: encParams.sample_aspect_ratio,
|
|
624
|
+
time_base: encParams.time_base,
|
|
625
|
+
// framerate: [encParams.time_base[1], encParams.time_base[0]],
|
|
626
|
+
// bit_rate: 2000000,
|
|
627
|
+
// gop_size: 10,
|
|
628
|
+
// max_b_frames: 1,
|
|
629
|
+
// priv_data: { preset: 'slow' }
|
|
630
|
+
priv_data: { crf: 23 } }); // ... more required ...
|
|
631
|
+
});
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
params.audio.forEach(p => {
|
|
635
|
+
p.streams.forEach((str, i) => {
|
|
636
|
+
const encParams = p.filter.graph.filters.find(f => f.name === `out${i}:a`).inputs[0];
|
|
637
|
+
str.encoder = beamcoder.encoder({
|
|
638
|
+
name: str.name,
|
|
639
|
+
sample_fmt: encParams.format,
|
|
640
|
+
sample_rate: encParams.sample_rate,
|
|
641
|
+
channel_layout: encParams.channel_layout,
|
|
642
|
+
flags: { GLOBAL_HEADER: mux.oformat.flags.GLOBALHEADER } });
|
|
643
|
+
|
|
644
|
+
str.codecpar.frame_size = str.encoder.frame_size;
|
|
645
|
+
});
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
params.video.forEach(p => {
|
|
649
|
+
p.streams.forEach(str => {
|
|
650
|
+
str.stream = mux.newStream({
|
|
651
|
+
name: str.name,
|
|
652
|
+
time_base: str.time_base,
|
|
653
|
+
interleaved: true }); // Set to false for manual interleaving, true for automatic
|
|
654
|
+
Object.assign(str.stream.codecpar, str.codecpar);
|
|
655
|
+
});
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
params.audio.forEach(p => {
|
|
659
|
+
p.streams.forEach(str => {
|
|
660
|
+
str.stream = mux.newStream({
|
|
661
|
+
name: str.name,
|
|
662
|
+
time_base: str.time_base,
|
|
663
|
+
interleaved: true }); // Set to false for manual interleaving, true for automatic
|
|
664
|
+
Object.assign(str.stream.codecpar, str.codecpar);
|
|
665
|
+
});
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
return {
|
|
669
|
+
run: async () => {
|
|
670
|
+
await mux.openIO({
|
|
671
|
+
url: params.out.url ? params.out.url : '',
|
|
672
|
+
flags: params.out.flags ? params.out.flags : {}
|
|
673
|
+
});
|
|
674
|
+
await mux.writeHeader({ options: params.out.options ? params.out.options : {} });
|
|
675
|
+
|
|
676
|
+
const muxBalancer = new serialBalancer(mux.streams.length);
|
|
677
|
+
const muxStreamPromises = [];
|
|
678
|
+
params.video.forEach(p => muxStreamPromises.push(runStreams('video', p.sources, p.filter, p.streams, mux, muxBalancer)));
|
|
679
|
+
params.audio.forEach(p => muxStreamPromises.push(runStreams('audio', p.sources, p.filter, p.streams, mux, muxBalancer)));
|
|
680
|
+
await Promise.all(muxStreamPromises);
|
|
681
|
+
|
|
682
|
+
await mux.writeTrailer();
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
module.exports = {
|
|
688
|
+
demuxerStream,
|
|
689
|
+
muxerStream,
|
|
690
|
+
makeSources,
|
|
691
|
+
makeStreams
|
|
692
|
+
};
|