1
1
import { ChildProcessWithoutNullStreams , spawn } from 'child_process' ;
2
2
import { EventEmitter } from 'events' ;
3
3
import * as stream from 'stream' ;
4
+ import * as si from 'systeminformation' ;
4
5
import { Flip , Rotation } from '..' ;
5
6
6
7
export enum Codec {
@@ -36,20 +37,6 @@ declare interface StreamCamera {
36
37
}
37
38
38
39
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
-
53
40
private readonly options : StreamOptions ;
54
41
private childProcess ?: ChildProcessWithoutNullStreams ;
55
42
private streams : Array < stream . Readable > = [ ] ;
@@ -68,166 +55,185 @@ class StreamCamera extends EventEmitter {
68
55
} ;
69
56
}
70
57
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
+
71
72
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
+ ) ,
185
191
) ,
186
- ) ,
187
- ) ;
192
+ ) ;
188
193
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 ( ) ) ;
191
196
192
- let stdoutBuffer = Buffer . alloc ( 0 ) ;
197
+ const jpegSignature = await StreamCamera . getJpegSignature ( ) ;
198
+ let stdoutBuffer = Buffer . alloc ( 0 ) ;
193
199
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 ) ) ;
197
203
198
- if ( this . options . codec !== Codec . MJPEG ) return ;
204
+ if ( this . options . codec !== Codec . MJPEG ) return ;
199
205
200
- stdoutBuffer = Buffer . concat ( [ stdoutBuffer , data ] ) ;
206
+ stdoutBuffer = Buffer . concat ( [ stdoutBuffer , data ] ) ;
201
207
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 ) ;
205
211
206
- if ( signatureIndex === - 1 ) break ;
212
+ if ( signatureIndex === - 1 ) break ;
207
213
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 ) ;
210
216
211
- const nextSignatureIndex = stdoutBuffer . indexOf (
212
- StreamCamera . jpegSignature ,
213
- StreamCamera . jpegSignature . length ,
214
- ) ;
217
+ const nextSignatureIndex = stdoutBuffer . indexOf ( jpegSignature , jpegSignature . length ) ;
215
218
216
- if ( nextSignatureIndex === - 1 ) break ;
219
+ if ( nextSignatureIndex === - 1 ) break ;
217
220
218
- this . emit ( 'frame' , stdoutBuffer . slice ( 0 , nextSignatureIndex ) ) ;
221
+ this . emit ( 'frame' , stdoutBuffer . slice ( 0 , nextSignatureIndex ) ) ;
219
222
220
- stdoutBuffer = stdoutBuffer . slice ( nextSignatureIndex ) ;
221
- }
222
- } ) ;
223
+ stdoutBuffer = stdoutBuffer . slice ( nextSignatureIndex ) ;
224
+ }
225
+ } ) ;
223
226
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 ) ) ;
228
231
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
+ }
231
237
} ) ;
232
238
}
233
239
0 commit comments