Skip to content

Commit 0d5abe1

Browse files
encoded_video_ingest
1 parent f231c0c commit 0d5abe1

5 files changed

Lines changed: 922 additions & 0 deletions

File tree

CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,8 @@ add_subdirectory(simple_joystick_sender)
9393
add_subdirectory(simple_joystick_receiver)
9494
add_subdirectory(ping_pong_ping)
9595
add_subdirectory(ping_pong_pong)
96+
<<<<<<< Updated upstream
97+
=======
98+
add_subdirectory(user_timestamped_video)
99+
add_subdirectory(encoded-video-ingest)
100+
>>>>>>> Stashed changes
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Copyright 2026 LiveKit, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
add_executable(EncodedVideoIngestProducer
16+
producer.cpp
17+
)
18+
19+
target_include_directories(EncodedVideoIngestProducer PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
20+
target_link_libraries(EncodedVideoIngestProducer PRIVATE ${LIVEKIT_CORE_TARGET})
21+
22+
livekit_copy_windows_runtime_dlls(EncodedVideoIngestProducer)
23+
24+
add_executable(EncodedVideoIngestConsumer
25+
consumer.cpp
26+
)
27+
28+
target_include_directories(EncodedVideoIngestConsumer PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
29+
target_link_libraries(EncodedVideoIngestConsumer PRIVATE ${LIVEKIT_CORE_TARGET})
30+
if(WIN32)
31+
target_link_libraries(EncodedVideoIngestConsumer PRIVATE ws2_32)
32+
endif()
33+
34+
livekit_copy_windows_runtime_dlls(EncodedVideoIngestConsumer)

encoded-video-ingest/README.md

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
# encoded-video-ingest
2+
3+
End-to-end demo of encoded video ingest in the LiveKit C++ SDK. The
4+
producer uses `livekit::EncodedTcpIngest`, the high-level SDK helper that owns
5+
the TCP reconnect loop, H.26x access-unit splitting, keyframe feedback, stats,
6+
and track publishing.
7+
8+
## Prerequisites
9+
10+
- A LiveKit server, such as `livekit-server --dev`.
11+
- GStreamer 1.22+ with `good`, `bad`, `ugly`, and `libav` plugins.
12+
- Two LiveKit tokens for the same room:
13+
14+
```bash
15+
export LIVEKIT_URL=ws://localhost:7880
16+
export PRODUCER_TOKEN="$(lk token create -r encoded-video-demo -i encoded-sender --join --publish)"
17+
export CONSUMER_TOKEN="$(lk token create -r encoded-video-demo -i encoded-receiver --join --subscribe)"
18+
```
19+
20+
# Setup Validation
21+
22+
**Before bringing LiveKit into the picture**, confirm your camera encode path
23+
and a basic H.264 decode preview work in pure GStreamer. The **send** and
24+
**receive** commands below use the **same UDP port (5005)** on purpose:
25+
`udpsink` sends RTP to `127.0.0.1:5005` and `udpsrc` binds `port=5005` for a
26+
quick local check.
27+
28+
That is only for this camera-validation hop. In the full LiveKit demo below,
29+
**port 5005** is reserved for **TCP** from the camera pipeline into the
30+
producer, and **port 5006** is where the consumer serves **decoded I420** to a
31+
separate GStreamer visualizer.
32+
33+
### Send - camera to RTP/UDP 5005
34+
35+
macOS (`avfvideosrc`). Linux: replace the source with
36+
`v4l2src device=/dev/video0`. Windows: `mfvideosrc device-index=0`. If the
37+
camera cannot produce 640x480 natively, add `videoscale ! videorate !` before
38+
`x264enc` and relax the first caps filter as needed.
39+
40+
```bash
41+
gst-launch-1.0 -v \
42+
avfvideosrc ! \
43+
video/x-raw,width=640,height=480,framerate=30/1 ! \
44+
videoconvert ! \
45+
x264enc tune=zerolatency bitrate=1000 speed-preset=ultrafast key-int-max=30 ! \
46+
video/x-h264,profile=baseline ! \
47+
rtph264pay pt=96 config-interval=1 ! \
48+
udpsink host=127.0.0.1 port=5005
49+
```
50+
51+
### Receive - RTP/UDP 5005 to display
52+
53+
```bash
54+
gst-launch-1.0 -v \
55+
udpsrc port=5005 caps="application/x-rtp,media=video,encoding-name=H264,payload=96" ! \
56+
rtph264depay ! \
57+
avdec_h264 ! \
58+
videoconvert ! \
59+
autovideosink
60+
```
61+
62+
On macOS, if `autovideosink` hangs at `PREROLLING`, replace it with
63+
`osxvideosink`.
64+
65+
This path validates camera, encoder, and decoder. It is **not** the same wire
66+
format as the ingest path: the demo ingest uses **TCP** and **Annex-B** with
67+
**AUD-delimited** access units. For that path use `x265enc` with AUD enabled,
68+
`h265parse`, and `tcpserversink` as shown below.
69+
70+
### Debugging a blank / green receive window
71+
72+
Before blaming the network, collapse encode to decode into a single local
73+
pipeline. A green square here means the encoder is being fed buffers it cannot
74+
consume, such as the wrong pixel format, GL memory, or no frames at all:
75+
76+
```bash
77+
gst-launch-1.0 -v \
78+
avfvideosrc device-index=0 ! \
79+
video/x-raw,width=640,height=480,format=NV12,framerate=30/1 ! \
80+
videoconvert ! \
81+
x264enc tune=zerolatency speed-preset=ultrafast bitrate=1000 key-int-max=60 aud=true ! \
82+
h264parse config-interval=1 ! avdec_h264 ! videoconvert ! autovideosink sync=false
83+
```
84+
85+
Common causes of a green or all-black preview:
86+
87+
- **macOS camera permission.** Grant your terminal app Camera access in
88+
*System Settings -> Privacy & Security -> Camera* and relaunch it.
89+
- **`memory:GLMemory` on the source pad.** Pinning `format=NV12` on the first
90+
caps filter forces a CPU buffer.
91+
- **Caps pinned to a mode the camera cannot produce.** Run
92+
`gst-device-monitor-1.0 Video/Source` and pick a listed `video/x-raw` mode.
93+
94+
## 1. Encode a webcam to TCP port 5005 using H.265
95+
96+
`tcpserversink` listens on **TCP** port **5005**. Stop any other TCP listener
97+
on that port before starting this command.
98+
99+
```bash
100+
gst-launch-1.0 -v \
101+
avfvideosrc device-index=0 ! \
102+
video/x-raw,width=640,height=480,format=NV12,framerate=30/1 ! \
103+
videoconvert ! \
104+
x265enc tune=zerolatency speed-preset=ultrafast bitrate=1000 key-int-max=60 \
105+
option-string="aud=1:repeat-headers=1" ! \
106+
h265parse config-interval=1 ! \
107+
video/x-h265,stream-format=byte-stream,alignment=au ! \
108+
tcpserversink host=0.0.0.0 port=5005
109+
```
110+
111+
Linux: replace `avfvideosrc device-index=0` with
112+
`v4l2src device=/dev/video0`. Windows: use `mfvideosrc device-index=0`.
113+
114+
## 2. Run the producer
115+
116+
```bash
117+
LIVEKIT_URL=ws://localhost:7880 LIVEKIT_TOKEN="$PRODUCER_TOKEN" \
118+
./build/encoded-video-ingest/EncodedVideoIngestProducer \
119+
--tcp-host 127.0.0.1 --tcp-port 5005 \
120+
--width 640 --height 480 \
121+
--codec h265
122+
```
123+
124+
The producer starts `EncodedTcpIngest`, publishes a camera track named
125+
`encoded-video`, and logs stats every two seconds.
126+
127+
## 3. Run the consumer
128+
129+
```bash
130+
LIVEKIT_URL=ws://localhost:7880 LIVEKIT_TOKEN="$CONSUMER_TOKEN" \
131+
./build/encoded-video-ingest/EncodedVideoIngestConsumer \
132+
--tcp-port 5006 \
133+
--from encoded-sender
134+
```
135+
136+
The consumer subscribes to `encoded-sender` and serves decoded I420 frames on
137+
TCP port 5006.
138+
139+
## 4. View the consumer output
140+
141+
```bash
142+
gst-launch-1.0 -v \
143+
tcpclientsrc host=127.0.0.1 port=5006 ! \
144+
rawvideoparse width=640 height=480 format=i420 framerate=30/1 ! \
145+
videoconvert ! autovideosink sync=false
146+
```
147+
148+
`rawvideoparse` needs the exact width and height that the producer declared.
149+
The consumer output is raw I420, not H.265, so do not pipe port 5006 through
150+
`h265parse`.

0 commit comments

Comments
 (0)