Self-hosted service
pion/rtwatch avatar
pion/rtwatch

rtwatch: a WebRTC video watcher where the server holds the playhead

Watch videos with friends using WebRTC, let the server do the pausing and seeking.

548 stars53 forksGoMIT

At a glance

What is it?
rtwatch is a Go program that lets a group of people watch a video together over WebRTC, with the backend holding all the state so that pausing or seeking moves every viewer at once. It uses GStreamer to decode and Pion WebRTC to distribute, and the interesting constraint is that only the current frame travels to the browser, so nothing can be downloaded and cached.
Who is it for?
rtwatch suits a small group that wants to watch something together over the internet with a shared playhead and no copies of the video on anyone's machine, since the design makes downloading the content impossible rather than merely discouraged. It does not suit a video conferencing product or a DRM system, because there is no identity, no room model and no access control in the repository, and the whole thing assumes a GStreamer installation on the host.
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 Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

All the state is on the backend, which is what makes it different

rtwatch lets you watch videos with friends using WebRTC and lets your backend do the pausing and seeking. The project describes itself as different from any other solution because all state is stored on the backend. Only the current audio and video frame is being sent to the viewers, and there is no way they can download or cache the videos either for future usage.

That single property is the design, and everything else follows from it.

The familiar alternative is a video chat call with screen sharing, where each participant receives a copy of the media and the host controls playback through a screen share rather than by controlling the player. rtwatch is closer to a synchronised player: the server holds the position in the video, the transport state, the decisions about play, pause and seek, and each browser renders the frame it is currently being sent.

The consequence the README is most explicit about is that the video cannot be saved. If a viewer wants to keep a copy, they cannot, because they never receive the file or a stream they could capture in full. That makes the tool appropriate for content you do not want distributed, in the sense that the tool itself provides no way to distribute it.

The stated purpose is watching your favorite movie perfectly synchronized with multiple viewers. If someone pauses it pauses for everyone, and no one can and no one fast forwards only their video.

So the audience is two different groups. The first is a group of friends who want a shared viewing session and do not want to hand each other a file. The second is a developer who wants to see how a server-authoritative media synchronisation is built, because the implementation is small and readable.

The primary language is Go, the licence is MIT with a LICENSE file and a LICENSES/ directory, the homepage is pion.ly, and the last push was on 2026-10-20 with no GitHub releases.

GStreamer decodes, Pion WebRTC distributes

The dependency list in go.mod is short and it divides into three roles.

The direct dependencies are github.com/go-gst/go-gst at v1.4.0, the Go bindings for GStreamer, github.com/gorilla/websocket at v1.5.3, and github.com/pion/webrtc/v4 at v4.2.24.

So the media pipeline is GStreamer. That is the important architectural fact, because GStreamer is a C library with plugins, and the Go bindings are a thin cgo layer over it. The plugin set you install on the host therefore determines what rtwatch can decode, which is why the installation instructions are mostly about installing GStreamer plugins for your platform.

The transport is Pion's pure Go WebRTC implementation, version 4. Everything under it is indirect: pion/ice/v4 for connectivity, pion/dtls/v3 for the encryption layer, pion/srtp/v3 for media, pion/sdp/v3 for session description, pion/turn/v5 and pion/stun/v4 for server reflexive and relay candidates, pion/sctp and pion/datachannel for the data channel, and pion/rtcp and pion/rtp for the control protocols.

That list explains a behaviour of the tool rather directly. Because the video travels as RTP over a WebRTC peer connection, the server has to push the media out to each viewer over its own connection, and the playhead lives in the same process that owns those connections. There is no separate signalling server and no separate media server in the repository; main.go and main_js.go are the whole program.

The Go version floor is worth noting too: go 1.24.0 with a toolchain directive of go1.26.6. So you need a recent Go, and the toolchain directive means Go will fetch a newer toolchain if your local one is older.

The gorilla/websocket dependency is what carries the control channel. The play, pause and seek buttons in the browser send messages over that websocket, the server updates its single authoritative state, and every connected viewer is then sent the current frame at the new position.

Building it means installing GStreamer first, on every platform

The installation instructions are the longest part of the README, and that length comes from GStreamer being a system-level dependency rather than a Go module.

On Debian and Ubuntu the packages are named in one line.

`sudo apt-get install libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly`

On Windows with MinGW64 or MSYS2 it is a pacman invocation with the mingw-w64 prefixed packages.

`pacman -S mingw-w64-x86_64-gstreamer mingw-w64-x86_64-gst-libav mingw-w64-x86_64-gst-plugins-good mingw-w64-x86_64-gst-plugins-bad mingw-w64-x86_64-gst-plugins-ugly`

The Windows section then lists two error cases with their fixes, and they are both about the toolchain rather than about rtwatch. A pkg-config failure means the PATH does not include pkg-config, and the fix is to set the PATH variable to the gstreamer bin directory. The Sorry, unimplemented: 64 bit mode not compiled in error means the compiler target is wrong, and the fix is to check with g++ -v and confirm the target is x86_64-w64-mingw32.

On macOS it is two commands.

code
brew install gst-plugins-good pkg-config gst-plugins-bad gst-plugins-ugly
export PKG_CONFIG_PATH="/usr/local/opt/libffi/lib/pkgconfig"

The export of PKG_CONFIG_PATH is the macOS-specific piece, because Homebrew's libffi keeps its pkgconfig files in a location pkg-config does not look at by default.

Then the build itself is three commands, because the Go module fetch needs the cgo libraries present.

code
git clone https://github.com/pion/rtwatch.git
cd rtwatch
go build .

So the order matters: GStreamer and its development headers have to be installed before go build, because the cgo bindings compile against them.

The plugin categories are worth noting. The good, bad and ugly plugin sets are all installed, and libav among the Windows packages, which is what gives you H.264 and similar decoding. The uglies are the ones that usually pull in codec licences, which is why they are a separate package and why the Dockerfile installs them separately from the build dependencies.

Running it is one flag, and the container needs host networking

Running the tool takes a container path argument, and the README shows both a local file and a remote URI.

code
./rtwatch -container-path=/home/sean/video.mp4
> Video file '/home/sean/video.mp4' is now available on ':8080', have fun!

The flag is named container-path even though the argument is a video file, and the server prints the address it is listening on. Port 8080 is the default.

Because rtwatch uses uridecodebin from GStreamer, the argument can be a URI rather than a local path.

code
./rtwatch -container-path='https://ia800207.us.archive.org/15/items/BigBuckBunny_124/Content/big_buck_bunny_720p_surround.mp4'

uridecodebin is GStreamer's playback element that handles the URI protocol detection and demuxing, so an HTTP URL works the same way a local file does. That is what makes the Docker image's default command possible.

The Docker path is the other route, and it carries one hard requirement.

code
git clone https://github.com/pion/rtwatch.git
cd rtwatch
docker build . -t rtcwatch
docker run --net=host -it rtcwatch

This requires host networking to be enabled. By default it is off on macOS at this time, and by default this works on Linux out of the box.

Host networking matters because the WebRTC connections need to reach peers and the media path benefits from not being NATed twice. On a Linux server the default bridge networking with published ports is often fine for a local test but is a different shape from what the container is built for.

The Dockerfile itself is a two-stage build on Alpine. The build stage starts from golang:alpine and installs the GStreamer development packages, the build tools and git, then clones the repository at a branch given by a build argument and runs go install. The runtime stage is alpine:latest with the good, ugly and bad plugin sets plus gstreamer, and it copies the compiled binary in. The entrypoint is the binary itself, and the default command is a container path pointing at a Big Buck Bunny file on the Internet Archive, so the image runs something playable the moment it starts.

To watch, you open http://localhost:8080 and hit play, and the README suggests opening it in multiple tabs to see the sync between viewers. There are also Seek, Play and Pause buttons, and pressing them changes the video state for every viewer at the same time.

Where rtwatch is the wrong tool

The first wrong-tool case is a product that needs identity, rooms or access control. The repository contains no authentication, no user model, no room concept and no notion of who may join. The README describes opening a URL and hitting play, and the server serves the one video it was given. So this is a tool for a group you already trust, not a service you would expose to strangers.

The second case is a workload that needs more than a handful of viewers. The design sends the current frame to every viewer over its own WebRTC connection, from the same process that holds the state. The README's own suggestion is to open multiple tabs to see the sync, which suggests the intended scale is a handful of people in a room together. A synchronous video call product is a different problem with a different architecture, and this is not it.

The third case is a Windows or macOS developer who wants to avoid native dependencies. Because the Go binary links against GStreamer through cgo, there is no pure Go build. The Windows section of the README is largely about toolchain errors, and the macOS section requires a PKG_CONFIG_PATH fix. The Docker route reduces that friction but needs host networking, which is off by default on macOS.

The fourth case is a team that needs a content protection system. The property that the video cannot be downloaded is a real design consequence rather than a promise, because only the current frame is sent. But it is not DRM. A determined viewer can still capture what is on their screen, and there is nothing here resembling keys, licences or a forensic mark. If the requirement is a licensed streaming product, this is the wrong layer.

There is one practical point about the container image that is worth understanding before you deploy it. The Dockerfile clones the repository at build time rather than copying a local tree, and it takes the repository URL and branch as build arguments with the main branch as the default. So an image built today contains the main branch as it was when you built, and rebuilding later pulls newer code. If you want a reproducible artefact you need to pin the branch argument yourself, and the ARG defaults do not do it for you.

On maintenance, the repository is part of the Pion organisation and the tooling around it is conventional Go practice: a .golangci.yml for linting, a .goreleaser.yml for releases, a codecov.yml for coverage, renovate.json for dependency updates, and a .reuse/ directory with a LICENSES/ folder for the REUSE software licensing format. The last push was on 2026-10-20, so the code is current, and there are no GitHub releases, so the version comes from the Go module rather than a tag.

On licence, MIT with a LICENSE file at the root, and the Dockerfile carries an SPDX header naming the Pion community and the MIT identifier, so the container build is licence-clean as well.

Editorial conclusion

rtwatch suits a small group that wants to watch something together over the internet with a shared playhead and no copies of the video on anyone's machine, since the design makes downloading the content impossible rather than merely discouraged. It does not suit a video conferencing product or a DRM system, because there is no identity, no room model and no access control in the repository, and the whole thing assumes a GStreamer installation on the host. Before adopting, check that GStreamer is available on the machine, since the Go binary links against it, and note that the Docker route needs host networking, which is off by default on macOS.

Frequently asked questions

What is rtwatch?

rtwatch is a Go program that lets you watch videos in real time with friends using WebRTC, with the backend holding all the playback state so pausing and seeking apply to every viewer. Only the current audio and video frame is sent to viewers, so the video cannot be downloaded or cached.

What does rtwatch need installed to build?

GStreamer and its development packages, plus the good, bad and ugly plugin sets, and pkg-config. On Debian and Ubuntu that is libgstreamer1.0-dev, libgstreamer-plugins-base1.0-dev and the three plugin packages, on macOS it is a brew install plus a PKG_CONFIG_PATH export, and on Windows it is the mingw-w64 pacman packages.

How do I run rtwatch?

Build with go build . after installing GStreamer, then run ./rtwatch -container-path pointing at a local file or a URI, since rtwatch uses uridecodebin so a remote URL works too. The server prints the address it is listening on, which is port 8080 by default.

Can I run rtwatch with Docker?

Yes, with docker build . -t rtcwatch followed by docker run --net=host -it rtcwatch. This requires host networking to be enabled, which is off by default on macOS at this time but works on Linux out of the box.

How does rtwatch keep the viewers in sync?

All state is stored on the backend. The browser sends play, pause and seek commands over a websocket, the server updates its single authoritative position, and each connected viewer is then sent the current frame at that position, so the change applies to everyone at once.

What license is rtwatch released under?

It is released under the MIT license, with the LICENSE file at the root of the repository and a LICENSES/ directory for the REUSE licensing format. The Dockerfile carries an SPDX header identifying the Pion community as the copyright holder.

Official sources

  1. Issues
  2. License: MIT
  3. pion/rtwatch on GitHub
  4. Project website
  5. README
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.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/pion-rtwatch.svg)](https://hysenlabs.com/projects/pion-rtwatch)

Was this useful?