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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions