Skip to content

Commit 8921c67

Browse files
committed
Added createStream() method to StreamCamera
1 parent 9ac15ff commit 8921c67

File tree

4 files changed

+151
-34
lines changed

4 files changed

+151
-34
lines changed

README.md

Lines changed: 57 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ There are many NPM modules for connecting to the Raspberry Pi camera, why use th
1111

1212
- **Speed:** JPEG images can be captured in ~33ms using a built in MJPEG parser
1313
- **Efficient:** Pictures and video streams are piped directly into Node as a `Buffer`, keeping all data in memory and eliminating disk I/O
14-
- **Usable:** Video streams are available as `stream.Readable` objects that can be piped or listened to
14+
- **Usable:** Video streams are available as [`stream.Readable`](https://nodejs.org/api/stream.html#stream_class_stream_readable) objects that can be piped or listened to
1515
- **Tested:** Contains automated tests using Jest
1616
- **Modern:** Uses the latest ESNext features and up to date development practices
1717
- **Structure**: Ships with TypeScript definition files
@@ -60,13 +60,17 @@ const runApp = async () => {
6060
codec: Codec.H264
6161
});
6262

63-
const videoStream = await streamCamera.startCapture();
63+
const videoStream = streamCamera.createStream();
6464

6565
const writeStream = fs.createWriteStream("video-stream.h264");
6666

6767
videoStream.pipe(writeStream);
6868

69-
setTimeout(() => streamCamera.stopCapture(), 5000);
69+
await streamCamera.startCapture();
70+
71+
await new Promise(resolve => setTimeout(() => resolve(), 5000));
72+
73+
await streamCamera.stopCapture();
7074
};
7175

7276
runApp();
@@ -96,9 +100,11 @@ const streamCamera = new StreamCamera({
96100

97101
const writeStream = fs.createWriteStream("video-stream.h264");
98102

99-
streamCamera.startCapture().then(videoStream => {
100-
101-
videoStream.pipe(writeStream);
103+
const videoStream = streamCamera.createStream();
104+
105+
videoStream.pipe(writeStream);
106+
107+
streamCamera.startCapture().then(() => {
102108

103109
setTimeout(() => streamCamera.stopCapture(), 5000);
104110
});
@@ -159,7 +165,7 @@ Capturing a video stream is easy. There are currently 2 codecs supported: `H264`
159165
The GPU on the Raspberry Pi comes with a hardware-accelerated H264 encoder and JPEG encoder. To capture videos in real time, using these hardware encoders are required.
160166

161167
### Stream
162-
A standard NodeJS [readable stream](https://nodejs.org/api/stream.html#stream_class_stream_readable) is available after calling `startCapture()`. As with any readable stream, it can be piped or listened to.
168+
A standard NodeJS [readable stream](https://nodejs.org/api/stream.html#stream_class_stream_readable) is available after calling `createStream()`. As with any readable stream, it can be piped or listened to.
163169

164170
```javascript
165171
import { StreamCamera, Codec } from "pi-camera-connect";
@@ -171,19 +177,23 @@ const runApp = async () => {
171177
codec: Codec.H264
172178
});
173179
174-
const videoStream = await streamCamera.startCapture();
180+
const videoStream = streamCamera.createStream();
175181
176182
const writeStream = fs.createWriteStream("video-stream.h264");
177183
178184
// Pipe the video stream to our video file
179185
videoStream.pipe(writeStream);
180186
187+
await streamCamera.startCapture();
188+
181189
// We can also listen to data events as they arrive
182190
videoStream.on("data", data => console.log("New data", data));
183191
videoStream.on("end", data => console.log("Video stream has ended"));
184192
185-
// Stop the stream after 5 seconds
186-
setTimeout(() => streamCamera.stopCapture(), 5000);
193+
// Wait for 5 seconds
194+
await new Promise(resolve => setTimeout(() => resolve(), 5000));
195+
196+
await streamCamera.stopCapture();
187197
};
188198
189199
runApp();
@@ -197,12 +207,13 @@ Note that this example produces a raw H264 video. Wrapping it in a video contain
197207

198208
## API
199209
- [`StillCamera`](#stillcamera)
200-
- `constructor(options: StillOptions = {}): StillCamera`
210+
- `constructor(options?: StillOptions): StillCamera`
201211
- `takeImage(): Promise<Buffer>`
202212
- [`StreamCamera`](#streamcamera)
203-
- `constructor(options: StreamOptions = {}): StreamCamera`
204-
- `startCapture(): Promise<stream.Readable>`
213+
- `constructor(options?: StreamOptions): StreamCamera`
214+
- `startCapture(): Promise<void>`
205215
- `stopCapture(): Promise<void>`
216+
- `createStream(): stream.Readable`
206217
- `takeImage(): Promise<Buffer>`
207218
- [`Rotation`](#rotation)
208219
- [`Flip`](#flip)
@@ -212,7 +223,7 @@ Note that this example produces a raw H264 video. Wrapping it in a video contain
212223
## `StillCamera`
213224
A class for taking still images. Equivalent to running the `raspistill` command.
214225

215-
### `constructor (options: StillOptions = {}): StillCamera`
226+
### `constructor (options?: StillOptions): StillCamera`
216227

217228
Instantiates a new `StillCamera` class.
218229

@@ -241,7 +252,7 @@ const image = await stillCamera.takeImage();
241252
## `StreamCamera`
242253
A class for capturing a stream of camera data, either as `H264` or `MJPEG`.
243254

244-
### `constructor(options: StreamOptions = {}): StreamCamera`
255+
### `constructor(options?: StreamOptions): StreamCamera`
245256

246257
Instantiates a new `StreamCamera` class.
247258

@@ -261,15 +272,43 @@ const streamCamera = new StreamCamera({
261272
- [`codec: Codec`](#codec) - *Default: `Codec.H264`*
262273
- [`sensorMode: SensorMode`](#sensormode) - *Default: `SensorMode.AutoSelect`*
263274

264-
### `startCapture(): Promise<stream.Readable>`
265-
Begins the camera stream. Returns a `Promise` with a readable stream of the video data that is resolved when the capture has started.
275+
### `startCapture(): Promise<void>`
276+
Begins the camera stream. Returns a `Promise` that is resolved when the capture has started.
266277

267278
### `stopCapture(): Promise<void>`
268279
Ends the camera stream. Returns a `Promise` that is resolved when the capture has stopped.
269280

281+
### `createStream(): stream.Readable`
282+
Creates a [`readable stream`](https://nodejs.org/api/stream.html#stream_class_stream_readable) of video data. There is no limit to the number of streams you can create.
283+
284+
Be aware that, as with any readable stream, data will buffer in memory until it is read. If you create a video stream but do not read its data, your program will quickly run out of memory.
285+
286+
Ways to read data so that it does not remain buffered in memory include:
287+
- Switching the stream to 'flowing' mode by calling either `resume()`, `pipe()`, or attaching a listener to the `'data'` event
288+
- Calling `read()` when the stream is in 'paused' mode
289+
290+
See the [readable stream documentation](https://nodejs.org/api/stream.html#stream_two_modes) for more information on flowing/paused modes.
291+
292+
```javascript
293+
const streamCamera = new StreamCamera({
294+
codec: Codec.H264
295+
});
296+
297+
const videoStream = streamCamera.createStream();
298+
299+
await streamCamera.startCapture();
300+
301+
videoStream.on("data", data => console.log("New video data", data));
302+
303+
// Wait 5 seconds
304+
await new Promise(resolve => setTimeout(() => resolve(), 5000));
305+
306+
await streamCamera.stopCapture();
307+
```
308+
270309
### `takeImage(): Promise<Buffer>`
271310

272-
Takes a JPEG image from an MJPEG camera stream, resulting in very fast image captures. Returns a `Promise` with a `Buffer` containing the image bytes.
311+
Takes a JPEG image frame from an MJPEG camera stream, resulting in very fast image captures. Returns a `Promise` with a `Buffer` containing the image bytes.
273312

274313
*Note: `StreamOptions.codec` must be set to `Codec.MJPEG`, otherwise `takeImage()` with throw an error.*
275314
```javascript

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "pi-camera-connect",
3-
"version": "0.1.0",
3+
"version": "0.2.0",
44
"description": "Library to capture and stream Raspberry Pi camera data directly to NodeJS",
55
"main": "build/index.js",
66
"types": "build/index.d.ts",

src/lib/stream-camera.test.ts

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import StreamCamera, { Codec } from "./stream-camera";
22

3-
test("takeImage() grabs JPEG from MJPEG stream", async () => {
3+
test("Method takeImage() grabs JPEG from MJPEG stream", async () => {
44

55
const streamCamera = new StreamCamera({
66
codec: Codec.MJPEG
@@ -14,3 +14,77 @@ test("takeImage() grabs JPEG from MJPEG stream", async () => {
1414

1515
expect(jpegImage.indexOf(StreamCamera.jpegSignature)).toBe(0);
1616
});
17+
18+
test("Method createStream() returns a stream of video data", async () => {
19+
20+
const streamCamera = new StreamCamera({
21+
codec: Codec.MJPEG
22+
});
23+
24+
await streamCamera.startCapture();
25+
26+
const videoStream = streamCamera.createStream();
27+
28+
// Wait 300 ms for data to arrive
29+
await new Promise(resolve => setTimeout(() => resolve(), 300));
30+
31+
const data = videoStream.read();
32+
33+
await streamCamera.stopCapture();
34+
35+
expect(data).not.toBeNull();
36+
expect(data.length).toBeGreaterThan(0);
37+
});
38+
39+
test("StreamCamera can push to multiple streams", async () => {
40+
41+
const streamCamera = new StreamCamera({
42+
codec: Codec.MJPEG
43+
});
44+
45+
await streamCamera.startCapture();
46+
47+
const videoStream1 = streamCamera.createStream();
48+
const videoStream2 = streamCamera.createStream();
49+
50+
// Wait 300 ms for data to arrive
51+
await new Promise(resolve => setTimeout(() => resolve(), 300));
52+
53+
const data1 = videoStream1.read();
54+
const data2 = videoStream2.read();
55+
56+
await streamCamera.stopCapture();
57+
58+
expect(data1).not.toBeNull();
59+
expect(data1.length).toBeGreaterThan(0);
60+
61+
expect(data2).not.toBeNull();
62+
expect(data2.length).toBeGreaterThan(0);
63+
});
64+
65+
test("Method stopCapture() ends all streams", async () => {
66+
67+
const streamCamera = new StreamCamera({
68+
codec: Codec.MJPEG
69+
});
70+
71+
await streamCamera.startCapture();
72+
73+
const videoStream1 = streamCamera.createStream();
74+
const videoStream2 = streamCamera.createStream();
75+
76+
// Readable streams will only call "end" when all data has been read
77+
// Calling resume() turns on flowing mode, which auto-reads data as it arrives
78+
videoStream1.resume();
79+
videoStream2.resume();
80+
81+
const stream1EndPromise = new Promise(resolve => videoStream1.on("end", () => resolve()));
82+
const stream2EndPromise = new Promise(resolve => videoStream2.on("end", () => resolve()));
83+
84+
await streamCamera.stopCapture();
85+
86+
return Promise.all([
87+
expect(stream1EndPromise).resolves.toBeUndefined(),
88+
expect(stream2EndPromise).resolves.toBeUndefined()
89+
]);
90+
});

src/lib/stream-camera.ts

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ class StreamCamera extends EventEmitter {
4141

4242
private options: StreamOptions;
4343
private childProcess?: ChildProcess;
44-
private stream?: stream.Readable;
44+
private streams: Array<stream.Readable> = [];
4545

4646
constructor(options: StreamOptions = {}) {
4747

@@ -58,7 +58,7 @@ class StreamCamera extends EventEmitter {
5858
};
5959
}
6060

61-
startCapture(): Promise<stream.Readable> {
61+
startCapture(): Promise<void> {
6262

6363
return new Promise(async (resolve, reject) => {
6464

@@ -161,25 +161,21 @@ class StreamCamera extends EventEmitter {
161161
"--output", "-"
162162
];
163163

164-
this.stream = new stream.Readable({
165-
read: () => {}
166-
});
167-
168164
// Spawn child process
169165
this.childProcess = spawn("raspivid", args);
170166

171167
// Listen for error event to reject promise
172168
this.childProcess.once("error", err => reject(new Error("Could not start capture with StreamCamera. Are you running on a Raspberry Pi with 'raspivid' installed?")));
173169

174170
// Wait for first data event to resolve promise
175-
this.childProcess.stdout.once("data", () => resolve(this.stream));
171+
this.childProcess.stdout.once("data", () => resolve());
176172

177173
let stdoutBuffer = Buffer.alloc(0);
178174

179175
// Listen for image data events and parse MJPEG frames if codec is MJPEG
180176
this.childProcess.stdout.on("data", (data: Buffer) => {
181177

182-
this.stream && this.stream.push(data);
178+
this.streams.forEach(stream => stream.push(data));
183179

184180
if (this.options.codec !== Codec.MJPEG)
185181
return;
@@ -223,14 +219,22 @@ class StreamCamera extends EventEmitter {
223219

224220
this.childProcess && this.childProcess.kill();
225221

226-
// Push null to the data stream to indicate EOF
227-
if (this.stream) {
222+
// Push null to each stream to indicate EOF
223+
// tslint:disable-next-line no-null-keyword
224+
this.streams.forEach(stream => stream.push(null));
225+
226+
this.streams = [];
227+
}
228+
229+
createStream() {
230+
231+
const newStream = new stream.Readable({
232+
read: () => {}
233+
});
228234

229-
// tslint:disable-next-line no-null-keyword
230-
this.stream.push(null);
235+
this.streams.push(newStream);
231236

232-
this.stream = undefined;
233-
}
237+
return newStream;
234238
}
235239

236240
takeImage() {

0 commit comments

Comments
 (0)