Skip to content

Single-stream h265:// publish negotiates SDP but SFU never forwards RTP (subscriber stays muted) #837

@aphexcx

Description

@aphexcx

Summary

lk room join --publish h265://host:port (single-stream, no WxH suffix) successfully connects, publishes a track, and the SFU completes SDP negotiation with subscribers — but the SFU never starts forwarding RTP. The subscriber's video track stays muted: true indefinitely and 0 bytes ever arrive at the receiver. The same exact pipeline with h264://host:port works perfectly. Confirmed against livekit-server v1.11.0 and livekit-cli v2.16.2, with Chrome 147 on Mac as subscriber, ARM64 Ubuntu 20.04 as publisher host.

Reproduction

# Bridge an H.265 RTSP source to a TCP socket as raw HEVC bytestream:
ffmpeg -fflags +genpts -rtsp_transport udp -i rtsp://camera/video1 \
  -an -c:v copy -f hevc 'tcp://127.0.0.1:9001?listen=1' &

# Publish the bytestream as a single-stream H.265 track:
LIVEKIT_URL=ws://localhost:7880 \
LIVEKIT_API_KEY=key LIVEKIT_API_SECRET=secret \
  lk room join --identity rtsp-pub --room test \
    --publish 'h265://127.0.0.1:9001' --fps 30

Subscribe via Chrome (>= 136 for native H.265 WebRTC support) using livekit-client. The track will be subscribed (TrackSubscribed fires with codec: video/H265), but no RTP arrives.

What works

  • lk ... --publish 'h264://127.0.0.1:9001' (after re-encoding source to H.264) → renders perfectly, 30fps, 0 packet loss, 1.1ms decode time on Mac.
  • The exact same SFU forwarding path is used for both codecs (verified in livekit/pkg/sfu/{receiver.go,downtrack.go,forwarder.go}Forwarder.DetermineCodec handles H264 and H265 the same).

Root cause

The publisher-side classification logic in server-sdk-go at v2/publication.go:482-516 only treats a published codec as "primary" if p.MimeType() ends with the codec name (e.g. h265). Otherwise it falls through into the backup-codec branch, which finds no matching backup track for an H.265-only publish and exits silently — never unblocking RTP forwarding to the subscriber.

MimeType() returns the empty string when TrackInfo.MimeType and TrackInfo.Codecs[].MimeType are not set in the publisher's AddTrackRequest. TrackInfo.MimeType is only populated when SimulcastCodecs was supplied to AddTrackRequest.

In livekit-cli's publishReader (cmd/lk/join.go:393):

pub, err = room.LocalParticipant.PublishTrack(track, &lksdk.TrackPublicationOptions{})

TrackPublicationOptions{} is empty — no WithSimulcast(...), no codec metadata. For H.264 this works because the SDK has H.264 inference paths; for H.265 the SDK requires explicit metadata or the MimeType() check fails.

In createSimulcastVideoTrack (cmd/lk/join.go:480) the multi-stream path correctly sets:

opts = append(opts, lksdk.ReaderTrackWithSampleOptions(lksdk.WithSimulcast("simulcast", &livekit.VideoLayer{
    Quality: quality, Width: urlParts.width, Height: urlParts.height,
})))

…which is why publishing 2-3 H.265 simulcast streams works around this issue.

Diagnostic evidence

Server log on publish (note videoLayerMode: MODE_UNUSED, single layer at HIGH):

mediaTrack published  mime=video/H265  trackInfo={..., codecs: [{mimeType: video/H265,
  layers: [{quality: HIGH, ssrc: ...}], videoLayerMode: MODE_UNUSED}],
  backupCodecPolicy: PREFER_REGRESSION}

Publisher SDK log on subscriber join (note mime: ""):

INFO  handling subscribed quality update  trackID=... mime=""
        subscribedCodecs=[{codec: h265, qualities: [LOW,MED,HIGH]}]
WARN  subscriber requested backup codec but no track found  trackID=... codec=h265

The same mime: "" log + WARN appears for H.264 single-stream too, but for H.264 the SFU forwards anyway. So the WARN itself is noise; the underlying bug only blocks H.265.

Proposed fix

In publishReader (or wherever single-stream H.265 publishes flow), call WithSimulcast(...) with a single layer marked Quality: HIGH (W/H can be 0/0 if not known) so the publisher SDK populates SimulcastCodecs metadata in AddTrackRequest. Alternatively, fix at the SDK level so H.265 single-stream PublishTrack infers the codec MimeType the same way H.264 does.

Happy to send a PR if you'd like — wanted to surface the diagnosis first since it's a multi-layer issue (lk-cli + server-sdk-go publication classification).

Environment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions