Skip to content

Commit 3b40a5d

Browse files
committed
allow h265 to be published via tcp or unix socket
1 parent 9bc2a86 commit 3b40a5d

File tree

4 files changed

+68
-25
lines changed

4 files changed

+68
-25
lines changed

README.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,8 @@ lk room join --identity bot \
191191

192192
You should now see both video and audio tracks published to the room.
193193

194+
Note: To publish H.265/HEVC over sockets, use the `h265://` scheme (for example, `h265:///tmp/myvideo.sock` or `h265://127.0.0.1:16400`). Ensure your LiveKit deployment and clients support H.265 playback.
195+
194196
### Publish from TCP (i.e. gstreamer)
195197

196198
It's possible to publish from video streams coming over a TCP socket. `lk` can act as a TCP client. For example, with a gstreamer pipeline ending in `! tcpserversink port=16400` and streaming H.264.
@@ -204,11 +206,11 @@ lk room join \
204206
<room_name>
205207
```
206208
207-
### Publish H.264 simulcast track from TCP
209+
### Publish H.264/H.265 simulcast track from TCP
208210
209-
You can publish multiple H.264 video tracks from different TCP ports as a single [Simulcast](https://docs.livekit.io/home/client/tracks/advanced/#video-simulcast) track. This is done by using multiple `--publish` flags.
211+
You can publish multiple H.264 or H.265 video tracks from different TCP ports as a single [Simulcast](https://docs.livekit.io/home/client/tracks/advanced/#video-simulcast) track. This is done by using multiple `--publish` flags.
210212
211-
The track will be published in simulcast mode if multiple `--publish` flags with the syntax `h264://<host>:<port>/<width>x<height>` are passed in as arguments.
213+
The track will be published in simulcast mode if multiple `--publish` flags with the syntax `<codec>://<host>:<port>/<width>x<height>` are passed in as arguments, where `<codec>` is `h264` or `h265`. All layers must use the same codec.
212214
213215
Example:
214216
@@ -239,7 +241,7 @@ lk room join --identity <name> --url "<url>" --api-key "<key>" --api-secret "<se
239241
```
240242
241243
Notes:
242-
- LiveKit CLI can only publish simulcast tracks using H.264 codec.
244+
- LiveKit CLI can publish simulcast tracks using H.264 or H.265. Ensure your LiveKit deployment and clients support the chosen codec (HEVC/H.265 support varies by platform/browser).
243245
- You can only use multiple `--publish` flags to create a simulcast track.
244246
- Using more than 1 `--publish` flag for other types of streams will not work.
245247
- Tracks will automatically be set to HIGH/MED/LOW resolution based on the order of their width.
@@ -248,7 +250,7 @@ Notes:
248250
### Publish streams from your application
249251
250252
Using unix sockets, it's also possible to publish streams from your application. The tracks need to be encoded into
251-
a format that WebRTC clients could playback (VP8, H.264, and Opus).
253+
a format that WebRTC clients could playback (VP8, H.264, H.265, and Opus).
252254

253255
Once you are writing to the socket, you could use `ffplay` to test the stream.
254256

cmd/lk/join.go

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ var (
5959
TakesFile: true,
6060
Usage: "`FILES` to publish as tracks to room (supports .h264, .ivf, .ogg). " +
6161
"can be used multiple times to publish multiple files. " +
62-
"can publish from Unix or TCP socket using the format '<codec>://<socket_name>' or '<codec>://<host:address>' respectively. Valid codecs are \"h264\", \"vp8\", \"opus\"",
62+
"can publish from Unix or TCP socket using the format '<codec>://<socket_name>' or '<codec>://<host:address>' respectively. Valid codecs are \"h264\", \"h265\", \"vp8\", \"opus\"",
6363
},
6464
&cli.FloatFlag{
6565
Name: "fps",
@@ -285,7 +285,7 @@ func parseSocketFromName(name string) (string, string, string, error) {
285285

286286
mimeType := name[:offset]
287287

288-
if mimeType != "h264" && mimeType != "vp8" && mimeType != "opus" {
288+
if mimeType != "h264" && mimeType != "h265" && mimeType != "vp8" && mimeType != "opus" {
289289
return "", "", "", fmt.Errorf("unsupported mime type: %s", mimeType)
290290
}
291291

@@ -318,6 +318,8 @@ func publishSocket(room *lksdk.Room,
318318
switch {
319319
case strings.Contains(mimeType, "h264"):
320320
mime = webrtc.MimeTypeH264
321+
case strings.Contains(mimeType, "h265"):
322+
mime = webrtc.MimeTypeH265
321323
case strings.Contains(mimeType, "vp8"):
322324
mime = webrtc.MimeTypeVP8
323325
case strings.Contains(mimeType, "opus"):
@@ -374,20 +376,22 @@ func publishReader(room *lksdk.Room,
374376

375377
// simulcastURLParts represents the parsed components of a simulcast URL
376378
type simulcastURLParts struct {
379+
codec string // "h264" or "h265"
377380
network string // "tcp" or "unix"
378381
address string
379382
width uint32
380383
height uint32
381384
}
382385

383-
// parseSimulcastURL validates and parses a simulcast URL in the format h264://<host:port>/<width>x<height> or h264://<socket_path>/<width>x<height>
386+
// parseSimulcastURL validates and parses a simulcast URL in the format <codec>://<host:port>/<width>x<height> or <codec>://<socket_path>/<width>x<height>
384387
func parseSimulcastURL(url string) (*simulcastURLParts, error) {
385388
matches := simulcastURLRegex.FindStringSubmatch(url)
386389
if matches == nil {
387-
return nil, fmt.Errorf("simulcast URL must be in format h264://<host:port>/<width>x<height> or h264://<socket_path>/<width>x<height>, got: %s", url)
390+
return nil, fmt.Errorf("simulcast URL must be in format <codec>://<host:port>/<width>x<height> or <codec>://<socket_path>/<width>x<height> where codec is h264 or h265, got: %s", url)
388391
}
389392

390-
address, widthStr, heightStr := matches[1], matches[2], matches[3]
393+
codec := matches[1]
394+
address, widthStr, heightStr := matches[2], matches[3], matches[4]
391395

392396
// Parse dimensions
393397
width, err := strconv.ParseUint(widthStr, 10, 32)
@@ -406,14 +410,15 @@ func parseSimulcastURL(url string) (*simulcastURLParts, error) {
406410
}
407411

408412
return &simulcastURLParts{
413+
codec: codec,
409414
network: network,
410415
address: address,
411416
width: uint32(width),
412417
height: uint32(height),
413418
}, nil
414419
}
415420

416-
// createSimulcastVideoTrack creates a simulcast video track from a TCP or Unix socket H.264 streams
421+
// createSimulcastVideoTrack creates a simulcast video track from a TCP or Unix socket H.264/H.265 streams
417422
func createSimulcastVideoTrack(urlParts *simulcastURLParts, quality livekit.VideoQuality, fps float64, onComplete func()) (*lksdk.LocalTrack, error) {
418423
conn, err := net.Dial(urlParts.network, urlParts.address)
419424
if err != nil {
@@ -440,10 +445,14 @@ func createSimulcastVideoTrack(urlParts *simulcastURLParts, quality livekit.Vide
440445
Height: urlParts.height,
441446
})))
442447

443-
return lksdk.NewLocalReaderTrack(conn, webrtc.MimeTypeH264, opts...)
448+
mime := webrtc.MimeTypeH264
449+
if urlParts.codec == "h265" {
450+
mime = webrtc.MimeTypeH265
451+
}
452+
return lksdk.NewLocalReaderTrack(conn, mime, opts...)
444453
}
445454

446-
// simulcastLayer represents a parsed H.264 stream with quality info
455+
// simulcastLayer represents a parsed H.264/H.265 stream with quality info
447456
type simulcastLayer struct {
448457
url string
449458
parts *simulcastURLParts
@@ -472,6 +481,14 @@ func handleSimulcastPublish(room *lksdk.Room, urls []string, fps float64, onPubl
472481
return fmt.Errorf("no valid simulcast URLs provided")
473482
}
474483

484+
// Ensure all layers use the same codec
485+
codec := layers[0].parts.codec
486+
for _, l := range layers[1:] {
487+
if l.parts.codec != codec {
488+
return fmt.Errorf("all simulcast layers must use the same codec; expected %s, found %s", codec, l.parts.codec)
489+
}
490+
}
491+
475492
// Sort streams by width to determine quality levels
476493
sort.Slice(layers, func(i, j int) bool {
477494
return layers[i].parts.width < layers[j].parts.width
@@ -541,6 +558,6 @@ func handleSimulcastPublish(room *lksdk.Room, urls []string, fps float64, onPubl
541558
return fmt.Errorf("failed to publish simulcast track: %w", err)
542559
}
543560

544-
fmt.Printf("Successfully published H.264 simulcast track with qualities: %v\n", trackNames)
561+
fmt.Printf("Successfully published %s simulcast track with qualities: %v\n", strings.ToUpper(codec), trackNames)
545562
return nil
546563
}

cmd/lk/join_test.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ func TestParseSimulcastURL(t *testing.T) {
8888
// Test TCP format
8989
parts, err := parseSimulcastURL("h264://localhost:8080/640x480")
9090
assert.NoError(t, err, "Expected no error for valid TCP simulcast URL")
91+
assert.Equal(t, "h264", parts.codec)
9192
assert.Equal(t, "tcp", parts.network)
9293
assert.Equal(t, "localhost:8080", parts.address)
9394
assert.Equal(t, uint32(640), parts.width)
@@ -96,6 +97,7 @@ func TestParseSimulcastURL(t *testing.T) {
9697
// Test Unix socket format with multiple slashes
9798
parts, err = parseSimulcastURL("h264:///tmp/my.socket/1280x720")
9899
assert.NoError(t, err, "Expected no error for valid Unix socket simulcast URL")
100+
assert.Equal(t, "h264", parts.codec)
99101
assert.Equal(t, "unix", parts.network)
100102
assert.Equal(t, "/tmp/my.socket", parts.address)
101103
assert.Equal(t, uint32(1280), parts.width)
@@ -104,6 +106,7 @@ func TestParseSimulcastURL(t *testing.T) {
104106
// Test Unix socket format with nested paths
105107
parts, err = parseSimulcastURL("h264:///tmp/deep/nested/path/my.socket/1920x1080")
106108
assert.NoError(t, err, "Expected no error for valid nested path Unix socket simulcast URL")
109+
assert.Equal(t, "h264", parts.codec)
107110
assert.Equal(t, "unix", parts.network)
108111
assert.Equal(t, "/tmp/deep/nested/path/my.socket", parts.address)
109112
assert.Equal(t, uint32(1920), parts.width)
@@ -112,17 +115,35 @@ func TestParseSimulcastURL(t *testing.T) {
112115
// Test simple socket name without path
113116
parts, err = parseSimulcastURL("h264://mysocket/640x480")
114117
assert.NoError(t, err, "Expected no error for simple socket name")
118+
assert.Equal(t, "h264", parts.codec)
115119
assert.Equal(t, "unix", parts.network)
116120
assert.Equal(t, "mysocket", parts.address)
117121
assert.Equal(t, uint32(640), parts.width)
118122
assert.Equal(t, uint32(480), parts.height)
119123

124+
// H265 variants
125+
parts, err = parseSimulcastURL("h265://localhost:8080/640x480")
126+
assert.NoError(t, err, "Expected no error for valid TCP simulcast URL (h265)")
127+
assert.Equal(t, "h265", parts.codec)
128+
assert.Equal(t, "tcp", parts.network)
129+
assert.Equal(t, "localhost:8080", parts.address)
130+
assert.Equal(t, uint32(640), parts.width)
131+
assert.Equal(t, uint32(480), parts.height)
132+
133+
parts, err = parseSimulcastURL("h265:///tmp/my.socket/1280x720")
134+
assert.NoError(t, err, "Expected no error for valid Unix socket simulcast URL (h265)")
135+
assert.Equal(t, "h265", parts.codec)
136+
assert.Equal(t, "unix", parts.network)
137+
assert.Equal(t, "/tmp/my.socket", parts.address)
138+
assert.Equal(t, uint32(1280), parts.width)
139+
assert.Equal(t, uint32(720), parts.height)
140+
120141
// Test invalid format
121142
_, err = parseSimulcastURL("h264://localhost:8080")
122143
assert.Error(t, err, "Expected error for URL without dimensions")
123144

124145
_, err = parseSimulcastURL("opus:///tmp/socket/640x480")
125-
assert.Error(t, err, "Expected error for non-h264 protocol")
146+
assert.Error(t, err, "Expected error for non-h264/h265 protocol")
126147

127148
_, err = parseSimulcastURL("h264:///tmp/socket/invalidxinvalid")
128149
assert.Error(t, err, "Expected error for invalid dimensions")

cmd/lk/room.go

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ import (
3838
)
3939

4040
var (
41-
// simulcastURLRegex matches h264 simulcast URLs in format h264://<host:port>/<width>x<height> or h264://<socket_path>/<width>x<height>
42-
simulcastURLRegex = regexp.MustCompile(`^h264://(.+)/(\d+)x(\d+)$`)
41+
// simulcastURLRegex matches h264 or h265 simulcast URLs in format <codec>://<host:port>/<width>x<height> or <codec>://<socket_path>/<width>x<height>
42+
simulcastURLRegex = regexp.MustCompile(`^(h264|h265)://(.+)/(\d+)x(\d+)$`)
4343

4444
RoomCommands = []*cli.Command{
4545
{
@@ -157,8 +157,8 @@ var (
157157
TakesFile: true,
158158
Usage: "`FILES` to publish as tracks to room (supports .h264, .ivf, .ogg). " +
159159
"Can be used multiple times to publish multiple files. " +
160-
"Can publish from Unix or TCP socket using the format '<codec>:///<socket_path>' or '<codec>://<host:port>' respectively. Valid codecs are \"h264\", \"vp8\", \"opus\". " +
161-
"For simulcast: use 2-3 h264:// URLs with format 'h264://<host:port>/<width>x<height>' or 'h264:///path/to/<socket_path>/<width>x<height>' (quality determined by width order)",
160+
"Can publish from Unix or TCP socket using the format '<codec>:///<socket_path>' or '<codec>://<host:port>' respectively. Valid codecs are \"h264\", \"h265\", \"vp8\", \"opus\". " +
161+
"For simulcast: use 2-3 h264:// or h265:// URLs with format '<codec>://<host:port>/<width>x<height>' or '<codec>:///path/to/<socket_path>/<width>x<height>' (all layers must use the same codec; quality determined by width order)",
162162
},
163163
&cli.StringFlag{
164164
Name: "publish-data",
@@ -823,19 +823,22 @@ func joinRoom(ctx context.Context, cmd *cli.Command) error {
823823
return fmt.Errorf("no more than 3 --publish flags can be specified, got %d", len(publishUrls))
824824
}
825825

826-
// If simulcast mode, validate all URLs are h264 format with dimensions
826+
// If simulcast mode, validate all URLs are h264 or h265 format with dimensions
827827
if simulcastMode {
828828
if len(publishUrls) == 1 {
829829
return fmt.Errorf("simulcast mode requires 2-3 streams, but only 1 was provided")
830830
}
831+
var firstCodec string
831832
for i, url := range publishUrls {
832-
if !strings.HasPrefix(url, "h264://") {
833-
return fmt.Errorf("publish flag %d: simulcast mode requires h264:// URLs with dimensions (format: h264://host:port/widthxheight), got: %s", i+1, url)
834-
}
835-
// Validate the format has dimensions
836-
if _, err := parseSimulcastURL(url); err != nil {
833+
parts, err := parseSimulcastURL(url)
834+
if err != nil {
837835
return fmt.Errorf("publish flag %d: %w", i+1, err)
838836
}
837+
if i == 0 {
838+
firstCodec = parts.codec
839+
} else if parts.codec != firstCodec {
840+
return fmt.Errorf("publish flag %d: simulcast layers must use the same codec; expected %s://, got %s://", i+1, firstCodec, parts.codec)
841+
}
839842
}
840843
}
841844

0 commit comments

Comments
 (0)