Skip to content

Commit 9dae0bb

Browse files
committed
fix: stream camera jpeg signatures
1 parent b1593be commit 9dae0bb

File tree

4 files changed

+173
-159
lines changed

4 files changed

+173
-159
lines changed

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
"test": "jest --runInBand",
2323
"prepublishOnly": "yarn clean && yarn build"
2424
},
25+
"dependencies": {
26+
"systeminformation": "4"
27+
},
2528
"devDependencies": {
2629
"@lcdev/eslint-config": "0.2",
2730
"@lcdev/jest": "0.2",

src/lib/stream-camera.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ test('Method takeImage() grabs JPEG from MJPEG stream', async () => {
1111

1212
await streamCamera.stopCapture();
1313

14-
expect(jpegImage.indexOf(StreamCamera.jpegSignature)).toBe(0);
14+
expect(jpegImage.indexOf(await StreamCamera.getJpegSignature())).toBe(0);
1515
});
1616

1717
test('Method createStream() returns a stream of video data', async () => {

src/lib/stream-camera.ts

Lines changed: 164 additions & 158 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ChildProcessWithoutNullStreams, spawn } from 'child_process';
22
import { EventEmitter } from 'events';
33
import * as stream from 'stream';
4+
import * as si from 'systeminformation';
45
import { Flip, Rotation } from '..';
56

67
export enum Codec {
@@ -36,20 +37,6 @@ declare interface StreamCamera {
3637
}
3738

3839
class StreamCamera extends EventEmitter {
39-
static readonly jpegSignature = Buffer.from([
40-
0xff,
41-
0xd8,
42-
0xff,
43-
0xe0,
44-
0x00,
45-
0x10,
46-
0x4a,
47-
0x46,
48-
0x49,
49-
0x46,
50-
0x00,
51-
]);
52-
5340
private readonly options: StreamOptions;
5441
private childProcess?: ChildProcessWithoutNullStreams;
5542
private streams: Array<stream.Readable> = [];
@@ -68,166 +55,185 @@ class StreamCamera extends EventEmitter {
6855
};
6956
}
7057

58+
static async getJpegSignature() {
59+
const systemInfo = await si.system();
60+
switch (systemInfo.model) {
61+
case 'BCM2835 - Pi 3 Model B':
62+
case 'BCM2835 - Pi 3 Model B+':
63+
case 'BCM2835 - Pi 4 Model B':
64+
return Buffer.from([0xff, 0xd8, 0xff, 0xdb, 0x00, 0x84, 0x00]);
65+
default:
66+
throw new Error(
67+
`Could not determine JPEG signature. Unknown system model '${systemInfo.model}'`,
68+
);
69+
}
70+
}
71+
7172
startCapture(): Promise<void> {
72-
return new Promise((resolve, reject) => {
73-
const args: Array<string> = [
74-
/**
75-
* Width
76-
*/
77-
...(this.options.width ? ['--width', this.options.width.toString()] : []),
78-
79-
/**
80-
* Height
81-
*/
82-
...(this.options.height ? ['--height', this.options.height.toString()] : []),
83-
84-
/**
85-
* Rotation
86-
*/
87-
...(this.options.rotation ? ['--rotation', this.options.rotation.toString()] : []),
88-
89-
/**
90-
* Horizontal flip
91-
*/
92-
...(this.options.flip &&
93-
(this.options.flip === Flip.Horizontal || this.options.flip === Flip.Both)
94-
? ['--hflip']
95-
: []),
96-
97-
/**
98-
* Vertical flip
99-
*/
100-
...(this.options.flip &&
101-
(this.options.flip === Flip.Vertical || this.options.flip === Flip.Both)
102-
? ['--vflip']
103-
: []),
104-
105-
/**
106-
* Bit rate
107-
*/
108-
...(this.options.bitRate ? ['--bitrate', this.options.bitRate.toString()] : []),
109-
110-
/**
111-
* Frame rate
112-
*/
113-
...(this.options.fps ? ['--framerate', this.options.fps.toString()] : []),
114-
115-
/**
116-
* Codec
117-
*
118-
* H264 or MJPEG
119-
*
120-
*/
121-
...(this.options.codec ? ['--codec', this.options.codec.toString()] : []),
122-
123-
/**
124-
* Sensor mode
125-
*
126-
* Camera version 1.x (OV5647):
127-
*
128-
* | Mode | Size | Aspect Ratio | Frame rates | FOV | Binning |
129-
* |------|---------------------|--------------|-------------|---------|---------------|
130-
* | 0 | automatic selection | | | | |
131-
* | 1 | 1920x1080 | 16:9 | 1-30fps | Partial | None |
132-
* | 2 | 2592x1944 | 4:3 | 1-15fps | Full | None |
133-
* | 3 | 2592x1944 | 4:3 | 0.1666-1fps | Full | None |
134-
* | 4 | 1296x972 | 4:3 | 1-42fps | Full | 2x2 |
135-
* | 5 | 1296x730 | 16:9 | 1-49fps | Full | 2x2 |
136-
* | 6 | 640x480 | 4:3 | 42.1-60fps | Full | 2x2 plus skip |
137-
* | 7 | 640x480 | 4:3 | 60.1-90fps | Full | 2x2 plus skip |
138-
*
139-
*
140-
* Camera version 2.x (IMX219):
141-
*
142-
* | Mode | Size | Aspect Ratio | Frame rates | FOV | Binning |
143-
* |------|---------------------|--------------|-------------|---------|---------|
144-
* | 0 | automatic selection | | | | |
145-
* | 1 | 1920x1080 | 16:9 | 0.1-30fps | Partial | None |
146-
* | 2 | 3280x2464 | 4:3 | 0.1-15fps | Full | None |
147-
* | 3 | 3280x2464 | 4:3 | 0.1-15fps | Full | None |
148-
* | 4 | 1640x1232 | 4:3 | 0.1-40fps | Full | 2x2 |
149-
* | 5 | 1640x922 | 16:9 | 0.1-40fps | Full | 2x2 |
150-
* | 6 | 1280x720 | 16:9 | 40-90fps | Partial | 2x2 |
151-
* | 7 | 640x480 | 4:3 | 40-90fps | Partial | 2x2 |
152-
*
153-
*/
154-
...(this.options.sensorMode ? ['--mode', this.options.sensorMode.toString()] : []),
155-
156-
/**
157-
* Capture time (ms)
158-
*
159-
* Zero = forever
160-
*
161-
*/
162-
'--timeout',
163-
(0).toString(),
164-
165-
/**
166-
* Do not display preview overlay on screen
167-
*/
168-
'--nopreview',
169-
170-
/**
171-
* Output to stdout
172-
*/
173-
'--output',
174-
'-',
175-
];
176-
177-
// Spawn child process
178-
this.childProcess = spawn('raspivid', args);
179-
180-
// Listen for error event to reject promise
181-
this.childProcess.once('error', () =>
182-
reject(
183-
new Error(
184-
"Could not start capture with StreamCamera. Are you running on a Raspberry Pi with 'raspivid' installed?",
73+
// eslint-disable-next-line no-async-promise-executor
74+
return new Promise(async (resolve, reject) => {
75+
// TODO: refactor promise logic to be more ergonomic
76+
// so that we don't need to try/catch here
77+
try {
78+
const args: Array<string> = [
79+
/**
80+
* Width
81+
*/
82+
...(this.options.width ? ['--width', this.options.width.toString()] : []),
83+
84+
/**
85+
* Height
86+
*/
87+
...(this.options.height ? ['--height', this.options.height.toString()] : []),
88+
89+
/**
90+
* Rotation
91+
*/
92+
...(this.options.rotation ? ['--rotation', this.options.rotation.toString()] : []),
93+
94+
/**
95+
* Horizontal flip
96+
*/
97+
...(this.options.flip &&
98+
(this.options.flip === Flip.Horizontal || this.options.flip === Flip.Both)
99+
? ['--hflip']
100+
: []),
101+
102+
/**
103+
* Vertical flip
104+
*/
105+
...(this.options.flip &&
106+
(this.options.flip === Flip.Vertical || this.options.flip === Flip.Both)
107+
? ['--vflip']
108+
: []),
109+
110+
/**
111+
* Bit rate
112+
*/
113+
...(this.options.bitRate ? ['--bitrate', this.options.bitRate.toString()] : []),
114+
115+
/**
116+
* Frame rate
117+
*/
118+
...(this.options.fps ? ['--framerate', this.options.fps.toString()] : []),
119+
120+
/**
121+
* Codec
122+
*
123+
* H264 or MJPEG
124+
*
125+
*/
126+
...(this.options.codec ? ['--codec', this.options.codec.toString()] : []),
127+
128+
/**
129+
* Sensor mode
130+
*
131+
* Camera version 1.x (OV5647):
132+
*
133+
* | Mode | Size | Aspect Ratio | Frame rates | FOV | Binning |
134+
* |------|---------------------|--------------|-------------|---------|---------------|
135+
* | 0 | automatic selection | | | | |
136+
* | 1 | 1920x1080 | 16:9 | 1-30fps | Partial | None |
137+
* | 2 | 2592x1944 | 4:3 | 1-15fps | Full | None |
138+
* | 3 | 2592x1944 | 4:3 | 0.1666-1fps | Full | None |
139+
* | 4 | 1296x972 | 4:3 | 1-42fps | Full | 2x2 |
140+
* | 5 | 1296x730 | 16:9 | 1-49fps | Full | 2x2 |
141+
* | 6 | 640x480 | 4:3 | 42.1-60fps | Full | 2x2 plus skip |
142+
* | 7 | 640x480 | 4:3 | 60.1-90fps | Full | 2x2 plus skip |
143+
*
144+
*
145+
* Camera version 2.x (IMX219):
146+
*
147+
* | Mode | Size | Aspect Ratio | Frame rates | FOV | Binning |
148+
* |------|---------------------|--------------|-------------|---------|---------|
149+
* | 0 | automatic selection | | | | |
150+
* | 1 | 1920x1080 | 16:9 | 0.1-30fps | Partial | None |
151+
* | 2 | 3280x2464 | 4:3 | 0.1-15fps | Full | None |
152+
* | 3 | 3280x2464 | 4:3 | 0.1-15fps | Full | None |
153+
* | 4 | 1640x1232 | 4:3 | 0.1-40fps | Full | 2x2 |
154+
* | 5 | 1640x922 | 16:9 | 0.1-40fps | Full | 2x2 |
155+
* | 6 | 1280x720 | 16:9 | 40-90fps | Partial | 2x2 |
156+
* | 7 | 640x480 | 4:3 | 40-90fps | Partial | 2x2 |
157+
*
158+
*/
159+
...(this.options.sensorMode ? ['--mode', this.options.sensorMode.toString()] : []),
160+
161+
/**
162+
* Capture time (ms)
163+
*
164+
* Zero = forever
165+
*
166+
*/
167+
'--timeout',
168+
(0).toString(),
169+
170+
/**
171+
* Do not display preview overlay on screen
172+
*/
173+
'--nopreview',
174+
175+
/**
176+
* Output to stdout
177+
*/
178+
'--output',
179+
'-',
180+
];
181+
182+
// Spawn child process
183+
this.childProcess = spawn('raspivid', args);
184+
185+
// Listen for error event to reject promise
186+
this.childProcess.once('error', () =>
187+
reject(
188+
new Error(
189+
"Could not start capture with StreamCamera. Are you running on a Raspberry Pi with 'raspivid' installed?",
190+
),
185191
),
186-
),
187-
);
192+
);
188193

189-
// Wait for first data event to resolve promise
190-
this.childProcess.stdout.once('data', () => resolve());
194+
// Wait for first data event to resolve promise
195+
this.childProcess.stdout.once('data', () => resolve());
191196

192-
let stdoutBuffer = Buffer.alloc(0);
197+
const jpegSignature = await StreamCamera.getJpegSignature();
198+
let stdoutBuffer = Buffer.alloc(0);
193199

194-
// Listen for image data events and parse MJPEG frames if codec is MJPEG
195-
this.childProcess.stdout.on('data', (data: Buffer) => {
196-
this.streams.forEach(stream => stream.push(data));
200+
// Listen for image data events and parse MJPEG frames if codec is MJPEG
201+
this.childProcess.stdout.on('data', (data: Buffer) => {
202+
this.streams.forEach(stream => stream.push(data));
197203

198-
if (this.options.codec !== Codec.MJPEG) return;
204+
if (this.options.codec !== Codec.MJPEG) return;
199205

200-
stdoutBuffer = Buffer.concat([stdoutBuffer, data]);
206+
stdoutBuffer = Buffer.concat([stdoutBuffer, data]);
201207

202-
// Extract all image frames from the current buffer
203-
while (true) {
204-
const signatureIndex = stdoutBuffer.indexOf(StreamCamera.jpegSignature, 0);
208+
// Extract all image frames from the current buffer
209+
while (true) {
210+
const signatureIndex = stdoutBuffer.indexOf(jpegSignature, 0);
205211

206-
if (signatureIndex === -1) break;
212+
if (signatureIndex === -1) break;
207213

208-
// Make sure the signature starts at the beginning of the buffer
209-
if (signatureIndex > 0) stdoutBuffer = stdoutBuffer.slice(signatureIndex);
214+
// Make sure the signature starts at the beginning of the buffer
215+
if (signatureIndex > 0) stdoutBuffer = stdoutBuffer.slice(signatureIndex);
210216

211-
const nextSignatureIndex = stdoutBuffer.indexOf(
212-
StreamCamera.jpegSignature,
213-
StreamCamera.jpegSignature.length,
214-
);
217+
const nextSignatureIndex = stdoutBuffer.indexOf(jpegSignature, jpegSignature.length);
215218

216-
if (nextSignatureIndex === -1) break;
219+
if (nextSignatureIndex === -1) break;
217220

218-
this.emit('frame', stdoutBuffer.slice(0, nextSignatureIndex));
221+
this.emit('frame', stdoutBuffer.slice(0, nextSignatureIndex));
219222

220-
stdoutBuffer = stdoutBuffer.slice(nextSignatureIndex);
221-
}
222-
});
223+
stdoutBuffer = stdoutBuffer.slice(nextSignatureIndex);
224+
}
225+
});
223226

224-
// Listen for error events
225-
this.childProcess.stdout.on('error', err => this.emit('error', err));
226-
this.childProcess.stderr.on('data', data => this.emit('error', new Error(data.toString())));
227-
this.childProcess.stderr.on('error', err => this.emit('error', err));
227+
// Listen for error events
228+
this.childProcess.stdout.on('error', err => this.emit('error', err));
229+
this.childProcess.stderr.on('data', data => this.emit('error', new Error(data.toString())));
230+
this.childProcess.stderr.on('error', err => this.emit('error', err));
228231

229-
// Listen for close events
230-
this.childProcess.stdout.on('close', () => this.emit('close'));
232+
// Listen for close events
233+
this.childProcess.stdout.on('close', () => this.emit('close'));
234+
} catch (err) {
235+
return reject(err);
236+
}
231237
});
232238
}
233239

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4395,6 +4395,11 @@ symbol-tree@^3.2.2:
43954395
version "3.2.2"
43964396
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6"
43974397

4398+
systeminformation@4:
4399+
version "4.23.0"
4400+
resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-4.23.0.tgz#9a38262d3f6c2db4b7b5abd4808a2035c8a162c2"
4401+
integrity sha512-Gx24ttVOuqKpaGJ5bMYOt/Y7VTntc/Du6h0oh1evKPjxXdSeAcwk1+xsTxEizN3+ZuuVyoHl5hV0jtlHDOGeYA==
4402+
43984403
table@^5.2.3:
43994404
version "5.4.6"
44004405
resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e"

0 commit comments

Comments
 (0)