Skip to content

Commit

Permalink
feat(streams)!: Node.js Buffer+Stream -> JS Uint8Array/Web Streams API (
Browse files Browse the repository at this point in the history
#1069)

Replaces use of `Buffer` with `Uint8Array`. The latter is now widely
supported and available in Node.js and Browsers.
Notable differences:
- `Buffer.slice(...)` has been replaced with `Uint8Array.subarray(...)`
  as that is the actual behaviour of the original method
  (`Uint8Array.slice(...)` makes a copy).
- Converting to and from strings is handled by `TextEncoder`/`TextDecoder`
- Converting to and from differently sized big-endian integers is
  handled by using `DataView`

Replaces use of Node.js Stream module with Web Streams API.
Since the latter is substantially different in some important details
regarding stream pipelining, the way pipelines are built are redefined.
The component concept as module blocks is removed and instead the
components themselves expose streams that can be combined together.
The pipelines are then simple stream compositions.

Because this is a major (breaking) change, it's done in concert with
other planned improvements that are also breaking changes (see section
below for details).

Improvements:
- replacing the Node.js stream module with Web Streams API removes
  the dependency on the (legacy) stream-browserify package and
  results in a much smaller library (bundle) size, so there is no longer
  a need for a separate "light" version
- `debug` package replaced by custom internal logging utilities
  (allowing proper ES module support) Fixes #990, Closes #992
- added audio test signal to the H.264 test

Refactoring:
- RTSP session and parser are combined in a single component and the
  session controller has been rewritten as a request-response flow.
  An async `start` method starts the streams and returns SDP + range.
- RTP depay is combined into a single component that detects
  the proper format based on payloadType, and allows registering
  a "peeker" that can inspect messages (instead of having to insert
  an extra transform stream)
- Extended use of TypeScript in areas where this was lacking

BREAKING CHANGES:
- No support for CommonJS:
  - Node.js has support for ES modules
  - Browsers have support for ES modules, but you can also still
    use the IIFE global variable, or use a bundler (all of which
    support ES modules)
- No distinction between Node.js/Browser:
  - The library targets mainly Browser, so some things rely on `window`
    and expect it to be present, however most things work both platforms.
  - Node-only pipelines are removed, these are trivial to re-implement
    with Web Streams API if necessary. The CLI player has its own TCP
    source for that reason (replacing the CliXyz pipelines).
- The generic "component" and "pipeline" classes were removed:
  - Components extend Web Streams API instead
  - Pipelines rely on `pipeTo`/`pipeThrough` composition and a `start`
    method to initiate flow of data.
- Some public methods on pipelines have been removed (refer to their
  type for details) in cases where a simple alternative is available, or
  check the examples to see how to modify usage. There are less pipelines
  but they are more versatile, with accessible readonly components.
  In general, promises/async methods are preferred over callbacks.

Co-authored-by: Rikard Tegnander <[email protected]>
Co-authored-by: Victor Ingvarsson <[email protected]>
  • Loading branch information
3 people authored Jan 10, 2025
1 parent d5941de commit 8114f37
Show file tree
Hide file tree
Showing 214 changed files with 6,918 additions and 10,847 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
*.log

# Coverage directory (tests with coverage)
build/
coverage/

# Bundles
Expand Down
65 changes: 0 additions & 65 deletions example-streams-node/mjpeg-player.js

This file was deleted.

82 changes: 82 additions & 0 deletions example-streams-node/pipeline.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { createConnection } from 'node:net'
import { Mp4Muxer, RtpDepay, RtspSession } from 'media-stream-library'

class TcpSource {
constructor(socket) {
if (socket === undefined) {
throw new Error('socket argument missing')
}

this.readable = new ReadableStream({
start: (controller) => {
socket.on('data', (chunk) => {
controller.enqueue(new Uint8Array(chunk))
})
socket.on('end', () => {
console.error('server closed connection')
controller.close()
})
},
cancel: () => {
console.error('canceling TCP client')
socket.close(CLOSE_ABORTED, 'client canceled')
},
})

this.writable = new WritableStream({
start: (controller) => {
socket.on('end', () => {
controller.error('socket closed')
})
socket.on('error', () => {
controller.error('socket errored')
})
},
write: (chunk) => {
try {
socket.write(chunk)
} catch (err) {
console.error('chunk lost during send:', err)
}
},
close: () => {
console.error('closing TCP client')
socket.destroy('normal closure')
},
abort: (reason) => {
console.error('aborting TCP client:', reason && reason.message)
socket.destroy('abort')
},
})
}
}

export async function start(rtspUri) {
const url = new URL(rtspUri)
const socket = createConnection(url.port, url.hostname)
await new Promise((resolve) => {
socket.once('connect', resolve)
})

const tcpSource = new TcpSource(socket)
const rtspSession = new RtspSession({ uri: rtspUri })
const rtpDepay = new RtpDepay()
const mp4Muxer = new Mp4Muxer()

const stdout = new WritableStream({
write: (msg, controller) => {
process.stdout.write(msg.data)
},
})

rtspSession.play()

return Promise.all([
tcpSource.readable
.pipeThrough(rtspSession.demuxer)
.pipeThrough(rtpDepay)
.pipeThrough(mp4Muxer)
.pipeTo(stdout),
rtspSession.commands.pipeTo(tcpSource.writable),
])
}
63 changes: 0 additions & 63 deletions example-streams-node/player.cjs

This file was deleted.

48 changes: 48 additions & 0 deletions example-streams-node/player.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { start } from './pipeline.mjs'

function help() {
console.log(`
Stream live from camera (to be used from Node CLI).
Command line tool to open a websocket/rtsp connection to a camera.
Example usage:
node player.mjs rtsp://192.168.0.2/axis-media/media.amp?audio=1&resolution=800x600 | vlc -
Some VAPIX options:
- videocodec=[h264,mpeg4,jpeg] (Select a specific video codec)
- streamprofile=<name> (Use a specific stream profile)
- recordingid=<name> (Play a specific recording)
- resolution=<wxh> (The required resolution, e.g. 800x600)
- audio=[0,1] (Enable=1 or disable=0 audio)
- camera=[1,2,...,quad] (Select a video source)
- compression=[0..100] (Vary between no=0 and full=100 compression)
- colorlevel=[0..100] (Vary between grey=0 and color=100)
- color=[0,1] (Enable=0 or disable=0 color)
- clock=[0,1] (Show=1 or hide=0 the clock)
- date=[0,1] (Show=1 or hide=0 the date)
- text=[0,1] (Show=1 or hide=0 the text overlay)
- textstring=<message>
- textcolor=[black,white]
- textbackgroundcolor=[black,white,transparent,semitransparent]
- textpos=[0,1] (Show text at top=0 or bottom=0)
- rotation=[0,90,180,270] (How may degrees to rotate the strea,)
- duration=<number> (How many seconds of video you want, unlimited=0)
- nbrofframes=<number> (How many frames of video you want, unlimited=0)
- fps=<number> (How many frames per second, unlimited=0)
`)
}

const [uri] = process.argv.slice(2)
if (!uri) {
console.error('You must specify either a host or full RTSP uri')
help()
process.exit(1)
}

// Setup a new pipeline
// const pipeline = new pipelines.CliMp4Pipeline(config)
// pipeline.rtsp.play()
start(uri).catch((err) => {
console.error('failed:', err)
})
19 changes: 19 additions & 0 deletions example-streams-web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,24 @@ the camera example directly, browse directly to

### Camera example

**Note on CORS** when accessing the camera using HTTP (e.g. streaming HTTP MP4) it's
likely you will get a CORS error. To resolve this, you can add a custom header
to the camera to allow any origin.

The simplest way is to navigate to the IP of the camera, open developer tools
on that page and run the following in the console:

```
const rsp = await fetch("/axis-cgi/customhttpheader.cgi", {
method: "POST",
body: JSON.stringify({
"apiVersion":"1.1",
"method":"set",
"params": {"Access-Control-Allow-Origin": "*"}
})
})
```

After serving the examples with and browsing to a file under
`http://localhost:8080/camera`, you will need to enter the camera (device) IP
address and choose an encoding. After that, just click the `play` button and
Expand Down Expand Up @@ -93,3 +111,4 @@ After you verified everything seems to be running fine, you can browse to
file like e.g. `http://localhost:8080/test/mjpeg.html` for the motion JPEG
example. Note that if you specified your own launch command, make sure it uses
the correct encoding to match the example.

10 changes: 4 additions & 6 deletions example-streams-web/camera/simple-metadata-player.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { pipelines } = window.mediaStreamLibrary
const { MetadataPipeline } = window.mediaStreamLibrary

// force auth
const authorize = async (host) => {
Expand All @@ -21,12 +21,10 @@ const authorize = async (host) => {
const play = (host) => {
const initialTime = window.performance.now()
// Setup a new pipeline
const pipeline = new pipelines.MetadataPipeline({
const pipeline = new MetadataPipeline({
ws: {
uri: `ws://${host}/rtsp-over-websocket`,
tokenUri: `http://${host}/axis-cgi/rtspwssession.cgi`,
protocol: 'binary',
timeout: 10000,
},
rtsp: {
uri: `rtsp://${host}/axis-media/media.amp?event=on&video=0&audio=0`,
Expand All @@ -43,8 +41,8 @@ const play = (host) => {
document.querySelector('#placeholder').prepend(title, content)
},
})
pipeline.ready.then(() => {
pipeline.rtsp.play()
pipeline.start().catch((err) => {
console.error('metadata pipeline failed:', err)
})

return pipeline
Expand Down
2 changes: 1 addition & 1 deletion example-streams-web/camera/simple-metadata.html
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
<button id="play">Play</button>
</div>
<div class="metadata-container" id="placeholder" />
<script src="/node_modules/media-stream-library/dist/media-stream-library.min.js"></script>
<script src="/media-stream-library.min.js"></script>
<script src="simple-metadata-player.js"></script>
</body>
</html>
15 changes: 8 additions & 7 deletions example-streams-web/camera/simple-mp4-player.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { pipelines } = window.mediaStreamLibrary
const { HttpMp4Pipeline } = window.mediaStreamLibrary

// force auth
const authorize = async (host) => {
Expand All @@ -24,21 +24,22 @@ const play = (host) => {
const mediaElement = document.querySelector('video')

// Setup a new pipeline
pipeline = new pipelines.HttpMsePipeline({
http: {
uri: `http://${host}/axis-cgi/media.cgi?videocodec=h264&container=mp4`,
},
pipeline = new HttpMp4Pipeline({
uri: `http://${host}/axis-cgi/media.cgi?videocodec=h264&container=mp4`,
mediaElement,
})
pipeline.http.play()
pipeline.start().catch((err) => {
console.error(err)
})
}

// Each time a device ip is entered, authorize and then play
const playButton = document.querySelector('#play')
playButton.addEventListener('click', async () => {
pipeline && pipeline.close()

const host = window.location.host
const device = document.querySelector('#device')
const host = device.value || device.placeholder

await authorize(host)

Expand Down
Loading

0 comments on commit 8114f37

Please sign in to comment.