Skip to content

Commit 26dc084

Browse files
perf: stream is now a cold observable (#36)
BREAKING CHANGE: change to public API and configuration interface.
1 parent 78c7d8f commit 26dc084

21 files changed

+168
-390
lines changed

.node-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
12.11.0
1+
12.11.1

.nvmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
12.11.0
1+
12.11.1

README.md

Lines changed: 13 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -37,29 +37,30 @@ This package is designed to be run in Node. For the best developer experience us
3737
$ npm i camera-probe
3838
```
3939

40-
## CLI
40+
## CLI Usage
4141
For CLI usage its easier to install globally like so:
4242
```sh
4343
$ npm i -g camera-probe
4444

4545
// starting listening
4646
$ camera-probe
47-
```
48-
49-
## Usage
50-
Starts probing the network using the default configuration.
51-
```ts
52-
import { devices$ } from 'camera-probe'
5347

54-
devices$().subscribe(console.info)
48+
// This table will update as cameras come online and offline.
49+
┌─────────┬───────────┬─────────────┬─────────────────┬──────────────────────────────────────────┬────────────────────────────────────────────────┐
50+
│ (index) │ Name │ Model │ IP │ URN │ Endpoint │
51+
├─────────┼───────────┼─────────────┼─────────────────┼──────────────────────────────────────────┼────────────────────────────────────────────────┤
52+
│ 0 │ 'Amcrest''IP2M-841B''192.168.1.1''38b4eeff-f5bd-46b9-92e4-30e6acffee73''http://192.168.1.1/onvif/device_service'
53+
│ 1 │ 'IPCAM''631GA''192.168.1.2''4f5dcb4f-eea6-4cda-b290-f2b2b7d2f14f''http://192.168.1.2:80/onvif/device_service'
54+
└─────────┴───────────┴─────────────┴─────────────────┴──────────────────────────────────────────┴────────────────────────────────────────────────┘
5555
```
5656

57+
## Programmatic Usage
5758
```js
58-
// example probe results
59-
// two cameras discovered on the network with ONVIF WS-Discovery via UDP
60-
// This will be the last emitted value in the observable until a new camera comes online
61-
// or a camera is disconnected or otherwise fails to respond to a ping.
59+
import { onvifDevices$ } from 'camera-probe'
60+
61+
onvifDevices$.subscribe(console.log)
6262

63+
// results
6364
[ { name: 'Amcrest',
6465
hardware: 'IP2M-8200',
6566
location: 'china',
@@ -92,33 +93,3 @@ devices$().subscribe(console.info)
9293
profiles: [ 'Streaming' ],
9394
xaddrs: [ 'http://192.168.5.13:80/onvif/device_service' ] } ]
9495
```
95-
96-
If you'd like to tweak default settings feel free to override in the `.run()` method.
97-
98-
```ts
99-
import { probe } from 'camera-probe'
100-
101-
probe()
102-
.run({
103-
PORTS: [3702],
104-
PROBE_NETWORK_TIMEOUT_MS: 20000
105-
})
106-
.subscribe(console.log)
107-
```
108-
109-
## Default Configuration
110-
```ts
111-
export const DEFAULT_CONFIG: IProbeConfig = {
112-
DOM_PARSER: new DOMParser(),
113-
PORTS: {
114-
UPNP: [1900],
115-
WS_DISCOVERY: [3702]
116-
},
117-
MULTICAST_ADDRESS: '239.255.255.250',
118-
PROBE_SAMPLE_TIME_MS: 2000,
119-
PROBE_NETWORK_TIMEOUT_MS: 2000 * 1.5,
120-
PROBE_SAMPLE_START_DELAY_TIME_MS: 0,
121-
ONVIF_DEVICES: ['NetworkVideoTransmitter', 'Device', 'NetworkVideoDisplay'],
122-
NOT_FOUND_STRING: 'unknown'
123-
}
124-
```

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
"@types/node": "^12.7.11",
5353
"@types/xmldom": "^0.1.29",
5454
"jest": "^24.9.0",
55-
"rollup": "^1.23.0",
55+
"rollup": "^1.23.1",
5656
"rollup-plugin-commonjs": "^10.1.0",
5757
"rollup-plugin-typescript2": "^0.24.3",
5858
"semantic-release": "^15.13.24",

src/config/config.default.ts

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,3 @@
1-
import { IProbeConfig } from './config.interface'
21
import { DOMParser } from 'xmldom'
3-
4-
export const DEFAULT_CONFIG: IProbeConfig = {
5-
DOM_PARSER: new DOMParser(),
6-
MULTICAST_ADDRESS: '239.255.255.250',
7-
PROBE_SAMPLE_TIME_MS: 2000,
8-
PROBE_NETWORK_TIMEOUT_MS: 12000,
9-
FALLOUT_MS: 4000,
10-
ONVIF_DEVICES: ['NetworkVideoTransmitter', 'Device', 'NetworkVideoDisplay'],
11-
PORTS: {
12-
UPNP: [1900],
13-
WS_DISCOVERY: [3702]
14-
}
15-
}
2+
const dom = new DOMParser()
3+
export const XML_PARSER_FN = (str: string) => dom.parseFromString(str, 'application/xml')

src/config/config.interface.ts

Lines changed: 0 additions & 55 deletions
This file was deleted.

src/core/interfaces.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,32 @@
1+
import { SocketType } from 'dgram'
2+
13
export type Strings = readonly string[]
24
export type Numbers = readonly number[]
35

46
export interface TimestampedMessage { readonly msg: string, readonly ts: number }
57
export type TimestampMessages = readonly TimestampedMessage[]
68
export type StringDictionary = { readonly [key: string]: string }
9+
10+
export interface IProbeConfig {
11+
readonly SOCKET_PROTOCOL: SocketType
12+
readonly PORTS: Numbers
13+
readonly MULTICAST_ADDRESS: string
14+
readonly PROBE_REQUEST_SAMPLE_RATE_MS: number
15+
readonly PROBE_RESPONSE_TIMEOUT_MS: number
16+
readonly PROBE_RESPONSE_FALLOUT_MS: number
17+
readonly RESULT_DEDUPE_FN: (msg: TimestampMessages) => StringDictionary
18+
}
19+
20+
export const DEFAULT_PROBE_CONFIG: IProbeConfig = {
21+
PORTS: [],
22+
SOCKET_PROTOCOL: 'udp4',
23+
MULTICAST_ADDRESS: '239.255.255.250',
24+
PROBE_REQUEST_SAMPLE_RATE_MS: 3000,
25+
PROBE_RESPONSE_FALLOUT_MS: 4000,
26+
PROBE_RESPONSE_TIMEOUT_MS: 6000,
27+
RESULT_DEDUPE_FN: (msg: TimestampMessages) => {
28+
return msg.reduce((acc, curr) => {
29+
return { ...acc, curr }
30+
}, {})
31+
}
32+
}

src/core/probe.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { flattenBuffersWithInfo } from '../core/probe'
1+
import { flattenBuffersWithInfo } from './probe'
22

33
describe('Probe', () => {
44
it('should flatten buffers to ports', done => {

src/core/probe.ts

Lines changed: 32 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,25 @@
1-
import { map, filter, scan, distinctUntilChanged, takeUntil, mapTo, tap } from 'rxjs/operators'
2-
import { ISocketStream, socketStream } from '../core/socket-stream'
3-
import { reader, IResult } from 'typescript-monads'
4-
import { Observable, timer } from 'rxjs'
5-
import { IProbeConfig } from '../config/config.interface'
6-
import { Strings, Numbers } from '../core/interfaces'
1+
import { createSocket, RemoteInfo } from 'dgram'
2+
import { Strings, Numbers, IProbeConfig, DEFAULT_PROBE_CONFIG } from './interfaces'
3+
import { Observable, Observer, fromEvent, timer } from 'rxjs'
4+
import { first, shareReplay, map, distinctUntilChanged, mapTo, takeWhile, takeUntil, scan } from 'rxjs/operators'
75

6+
type IMessage = readonly [Buffer, RemoteInfo]
87
type TimestampMessages = readonly TimestampedMessage[]
98
type StringDictionary = { readonly [key: string]: string }
109
interface TimestampedMessage { readonly msg: string, readonly ts: number }
1110
interface BufferPort { readonly buffer: Buffer, readonly port: number, readonly address: string }
1211

13-
const flattenXml = (str: string) => str.replace(/>\s*/g, '>').replace(/\s*</g, '<')
1412
const mapStringToBuffer = (str: string) => Buffer.from(str, 'utf8')
13+
const flattenXml = (str: string) => str.replace(/>\s*/g, '>').replace(/\s*</g, '<')
1514
const toArrayOfValues = <T extends StringDictionary>(source: Observable<T>) => source.pipe(map(a => Object.keys(a).map(b => a[b])))
1615
const flattenDocumentStrings = (source: Observable<Strings>) => source.pipe(map(a => a.map(flattenXml)))
17-
const filterOkResults = <TOk, TFail>(source: Observable<IResult<TOk, TFail>>) => source.pipe(filter(a => a.isOk()))
18-
const timestamp = <TFail>(source: Observable<IResult<Buffer, TFail>>) => source.pipe(map<IResult<Buffer, TFail>, TimestampedMessage>(a => ({ msg: a.unwrap().toString(), ts: Date.now() })))
16+
const timestamp = (source: Observable<Buffer>) => source.pipe(map<Buffer, TimestampedMessage>(a => ({ msg: a.toString(), ts: Date.now() })))
1917
const distinctUntilObjectChanged = <T>(source: Observable<T>) => source.pipe(distinctUntilChanged((a, b) => {
2018
const keys1 = Object.keys(a)
2119
const keys2 = Object.keys(b)
22-
23-
return keys1.length === keys2.length && keys1.reduce((acc: boolean, curr) => {
24-
return acc === false ? false : keys2.includes(curr) as boolean
25-
}, true)
20+
21+
return keys1.length === keys2.length &&
22+
keys1.reduce((acc: boolean, curr) => acc === false ? false : keys2.includes(curr) as boolean, true)
2623
}))
2724

2825
const accumulateFreshMessages =
@@ -42,27 +39,27 @@ export const flattenBuffersWithInfo =
4239
ports.reduce((acc, port) =>
4340
[...acc, ...buffers.map(buffer => ({ buffer, port, address }))], [] as readonly BufferPort[])
4441

45-
export const initSocketStream = reader<IProbeConfig, ISocketStream>(c => socketStream('udp4', c.PROBE_NETWORK_TIMEOUT_MS, c.distinctFilterFn))
46-
4742
export const probe =
48-
(socket: ISocketStream) =>
49-
(ports: Numbers) =>
50-
(address: string) =>
51-
(messagesToSend: Strings = []) =>
52-
(mapFn: (msg: readonly TimestampedMessage[]) => StringDictionary) =>
53-
reader<IProbeConfig, Observable<Strings>>(cfg => {
54-
timer(0, cfg.PROBE_SAMPLE_TIME_MS).pipe(
55-
mapTo(flattenBuffersWithInfo(ports)(address)(messagesToSend.map(mapStringToBuffer))),
56-
takeUntil(socket.close$))
57-
.subscribe(bfrPorts => bfrPorts.forEach(mdl => socket.socket.send(mdl.buffer, 0, mdl.buffer.length, mdl.port, mdl.address)))
43+
(config?: Partial<IProbeConfig>) =>
44+
(messages: Strings): Observable<Strings> =>
45+
Observable.create((obs: Observer<Strings>) => {
46+
const cfg = { ...DEFAULT_PROBE_CONFIG, ...(config || {}) }
47+
const socket = createSocket({ type: 'udp4' })
48+
const socketClosed$ = fromEvent<void>(socket, 'close').pipe(first(), shareReplay(1))
49+
const socketMessages$ = fromEvent<IMessage>(socket, 'message').pipe(map(a => a[0]), shareReplay(1))
50+
51+
timer(0, cfg.PROBE_REQUEST_SAMPLE_RATE_MS).pipe(
52+
mapTo(flattenBuffersWithInfo(cfg.PORTS)(cfg.MULTICAST_ADDRESS)(messages.map(mapStringToBuffer))),
53+
takeWhile(() => !obs.closed),
54+
takeUntil(socketClosed$))
55+
.subscribe(bfrPorts => bfrPorts.forEach(mdl => socket.send(mdl.buffer, 0, mdl.buffer.length, mdl.port, mdl.address)))
5856

59-
return socket.messages$.pipe(
60-
filterOkResults,
61-
timestamp,
62-
accumulateFreshMessages(cfg.FALLOUT_MS),
63-
mapStrToDictionary(mapFn),
64-
distinctUntilObjectChanged,
65-
toArrayOfValues,
66-
flattenDocumentStrings
67-
)
68-
})
57+
socketMessages$.pipe(
58+
timestamp,
59+
accumulateFreshMessages(cfg.PROBE_RESPONSE_FALLOUT_MS),
60+
mapStrToDictionary(cfg.RESULT_DEDUPE_FN),
61+
distinctUntilObjectChanged,
62+
toArrayOfValues,
63+
flattenDocumentStrings
64+
).subscribe(msg => obs.next(msg))
65+
})

0 commit comments

Comments
 (0)