RTSPtoWeb: serving an IP camera to a browser without FFmpeg
RTSP Stream to WebBrowser
At a glance
- What is it?
- RTSPtoWeb is a Go server that converts RTSP streams into MSE, WebRTC and HLS output with no FFmpeg or GStreamer in the path. It costs little CPU per stream and drops audio entirely, which decides who can use it.
- Who is it for?
- Adopt RTSPtoWeb when your cameras speak RTSP with H264, your viewers are on a browser, and you would rather not keep FFmpeg or GStreamer installed, since the stated per stream cost is roughly 0.2 to 1 percent of one core. Do not adopt it for audio, for H265 or any non H264 source, or for a fleet where you cannot accept a single process holding every stream.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 2 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The claim is a Go binary with no FFmpeg in the data path
The usual way to get an RTSP camera into a browser is to run it through FFmpeg or GStreamer, which means installing and maintaining a media stack on the machine doing the conversion. RTSPtoWeb removes that step. The README states it is fully native Golang without the use of FFmpeg or GStreamer.
What it produces instead is a choice of browser-consumable formats: Media Source Extensions, WebRTC, or HLS. That choice is the interesting design decision in the project, because the three have completely different network and browser requirements. MSE needs an HTTP stream the page can append to. WebRTC needs a peer connection, which means ICE, STUN or TURN, and a UDP port range. HLS needs segments and a playlist.
The repository layout shows the same split made explicit in code. Alongside RTSPtoWeb.go there are apiHTTPMSE.go, apiHTTPWebRTC.go, apiHTTPHLS.go with hlsSegment.go and hlsFragment.go, plus apiHTTPHLSLL.go for the low latency variant, apiHTTPStream.go, apiHTTPSaveMP4.go and storageStream.go. The RTSP server side sits in serverRTSP.go and the streaming core in streamCore.go, with the upstream client work coming from the deepch/vdk module, which go.mod pins at v0.0.27.
The dependency list is Go-native and web-shaped: gin and gin-gonic/autotls for HTTP and automatic TLS, gobwas/ws for websockets, logrus for logging, and the pion/webrtc family for the WebRTC path, which is an indirect dependency at this version of go.mod.
Running from source in three commands
The installation path from source is deliberately short. Clone the repository, change into the directory, and run it:
$ git clone https://github.com/deepch/RTSPtoWeb$ cd RTSPtoWeb/$ GO111MODULE=on go run *.goThen open http://127.0.0.1:8083 in Chrome, Safari or Firefox. The default port 8083 comes from the config.json that ships in the repository root, so if nothing appears, the first thing to check is whether that file is being found.
There is also a Makefile, which wraps the same operations as build, run, serve and server targets, plus test and lint. The server target passes -config config.json, matching the flag default. Lint is go vet, and test runs the two shell scripts test.curl and test_multi.curl that sit in the repository root.
The only command-line flags are two. Running the built binary with --help prints the usage:
./RTSPtoWeb --helpwhich lists -config, described as the config patch with a default of config.json, and -debug, which sets debug mode and defaults to true. That debug default is worth knowing, since it means a first run is verbose whether or not you asked for it.
The Docker image bakes in a config path and a release-mode server
The container route is one command:
$ docker run --name rtsp-to-web --network host ghcr.io/deepch/rtsptoweb:latestThe image is published to the GitHub container registry under ghcr.io/deepch/rtsptoweb. Host networking is what you want here, because the RTSP server and the WebRTC UDP ports need to be reachable without a port mapping layer in between.
Overriding configuration means mounting your own config.json over the one in the image:
$ docker run --name rtsp-to-web \
-v /PATH_TO_CONFIG/config.json:/config/config.json \
--network host \
ghcr.io/deepch/rtsptoweb:latestThe Dockerfile explains why the path is /config/config.json. The build stage uses golang:1.27-alpine, sets CGO_ENABLED to 0 and cross compiles with GOOS, GOARCH and GOARM from build arguments, producing a static binary. The runtime stage is alpine:3.24, which copies in the binary, the web directory and the default config, and creates the config directory before copying the file into it. The entry point is a shell-style command running ./rtsp-to-web with --config=/config/config.json, and GIN_MODE is set to release.
go.mod asks for Go 1.26.0 with toolchain go1.27.2, so your local toolchain needs to be at least that new to build from source.
config.json decides how many channels run at once
The configuration is one JSON document with a server block, a streams map, a channel_defaults block and, optionally, a token block for play authorization. The example in the README is the best reference for the shape.
The server block holds debug, log_level, http_demo, http_debug, http_auth with http_login and http_password, http_port as :8083, http_dir, the ICE settings for WebRTC, the webrtc_port_min and webrtc_port_max UDP range, the https block with auto TLS and certificate settings, and rtsp_port as :5541.
Streams are keyed by name, each holding a name and a channels map. Channels hold a name, a url such as rtsp://admin:admin@YOU_CAMERA_IP/uri, on_demand, debug, audio and status. The ICE fields matter specifically for WebRTC, since ice_servers, ice_username, ice_credential and ice_candidates decide whether a browser behind a restrictive NAT can connect at all. The example uses a public STUN server.
The single most consequential channel setting is on_demand. With on_demand set to true the server pulls video from the source only when there is a viewer. With it false, the source is pulled constantly. On a camera that limits concurrent clients, a static channel for every camera is how you run out.
Audio is a per channel switch, defaulting to true in the example, but read the limitations section before relying on it.
H264 only, and no audio
The limitations section is three lines long and both of them matter:
Video codecs supported: H264 all profiles.
Audio codecs supported: no.
So a camera that streams H265 will not work, and no amount of configuration changes that, because the restriction is in the codec handling rather than in a flag. Many recent cameras offer H265 as their default and expect a client to negotiate down, which is a realistic way to hit this on first contact.
The audio line is the sharper edge. A channel can have audio set to true and still produce no audio, so the setting in the example config should not be read as a promise.
The README also gives a performance figure, stated as approximately 0.2 to 1 percent of one thread core CPU on an Intel Core i7 per stream. That is the number the no-FFmpeg design exists to deliver: the work is remuxing H264 into the container the browser wants, not decoding and re-encoding frames.
Sibling projects by the same author cover adjacent formats, RTSPtoWebRTC, RTSPtoWSMP4f, RTSPtoImage, RTSPtoHLS and RTSPtoHLSLL, which is useful context for which format you actually need.
Play authorization delegates the decision to your own endpoint
There is an authorization hook for handing out stream URLs. It is enabled with a token object in the configuration:
"token": {
"enable": true,
"backend": "http://127.0.0.1/file.php"
}The backend is an HTTP endpoint you provide. When a viewer requests a stream with a token, RTSPtoWeb asks your endpoint, which must reply with JSON carrying status set to "1" or "0". The example request URL carries the token as a query parameter:
rtsp://127.0.0.1:5541/demo/0?token=you_keyTwo things follow. The scheme in that URL is rtsp rather than http, which tells you the RTSP server port is where the token check happens. And the authorization decision is entirely yours, so the example backend being a PHP file is a placeholder rather than a recommendation.
For the demo interface itself there is a simpler HTTP Basic Auth in the server block, with http_auth, http_login and http_password, defaulting to the demo credentials in the example config. That is a convenient way to keep the built-in page private on a home network and a poor way to secure anything exposed to the internet.
TLS is handled in the same block, either through explicit https_cert and https_key paths or through automatic TLS, which matters for any browser that will not treat plain HTTP camera streams as a secure context.
Development signals split between a busy trunk and a stale release list
The repository is not archived and the last push was on 2026-10-15, two days before 2026-10-17, so work is landing on master. The tagged releases tell a different story: the most recent are v2.4.3 dated 2023-03-29, v2.4.2 and v2.4.1 both dated 2023-03-11. If you pin a version, you are pinning code from 2023 while the trunk moves, and the Docker image tagged latest follows the trunk, not the newest tag.
The trunk itself is not dated in the way a small utility usually is. go.mod requires Go 1.26.0 with toolchain go1.27.2, the Dockerfile builds on golang:1.27-alpine and runs on alpine:3.24, and gin is at v1.12.0. renovate.json is in the repository, so dependency updates are automated rather than manual.
The licence is MIT, and the README calls out two third party components with their own MIT terms, pion/webrtc and joy4. This is not legal advice, and the actual terms are in LICENSE.md.
The named authors are Andrey Semochkin for the initial video work and Dmitriy Vladykin for the web UI, which matches the file split between the media side and apiHTTPServer.go plus the web directory. There is a SECURITY.md, so if you are running this exposed, that file is where the project expects disclosure to start.
Editorial conclusion
Adopt RTSPtoWeb when your cameras speak RTSP with H264, your viewers are on a browser, and you would rather not keep FFmpeg or GStreamer installed, since the stated per stream cost is roughly 0.2 to 1 percent of one core. Do not adopt it for audio, for H265 or any non H264 source, or for a fleet where you cannot accept a single process holding every stream. Before deploying, set on_demand to true per channel so cameras are not pulled with nobody watching, replace the demo credentials in config.json, and decide between WebRTC and HLS knowing that the first needs ice_servers and open UDP ports while the second does not.
Frequently asked questions
How do I run RTSPtoWeb from source?
Clone the repository, cd into RTSPtoWeb/, then run GO111MODULE=on go run *.go and open http://127.0.0.1:8083 in a browser. go.mod requires Go 1.26.0 with toolchain go1.27.2.
Does RTSPtoWeb need FFmpeg installed?
No. The README states it is fully native Golang without the use of FFmpeg or GStreamer, converting RTSP into MSE, WebRTC or HLS output directly.
Which video and audio codecs are supported?
Video is limited to H264 in all profiles. Audio codecs supported: none, so the per channel audio setting will not produce sound.
What does the on_demand channel setting do?
With on_demand set to true the server pulls video from the camera only when a viewer is present, and with it false it pulls constantly. channel_defaults can set the default for all channels.
How do I restrict who can watch a stream?
Enable the token object with a backend URL of your own, and have that endpoint return JSON with status set to "1" or "0". Viewers then request the stream with a token query parameter, for example rtsp://127.0.0.1:5541/demo/0?token=you_key.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/deepch-rtsptoweb)