From 150209ed09a1546b69db1b9f005ca38b320c66da Mon Sep 17 00:00:00 2001 From: Jakub Budisky Date: Wed, 13 May 2026 21:59:35 +0200 Subject: [PATCH 1/3] Add H265 support Adds H265 as a selectable video codec with software (x265enc), VAAPI (vah265enc), and NVENC (nvh265enc) pipeline paths in both the legacy pipeline builder and the RTPCodec definition. Co-Authored-By: Claude Sonnet 4.6 --- server/internal/config/capture_pipeline.go | 33 ++++++++++++++++++++++ server/pkg/types/codec/codecs.go | 20 +++++++++++++ webpage/docs/configuration/capture.md | 4 +-- 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/server/internal/config/capture_pipeline.go b/server/internal/config/capture_pipeline.go index f7e0c476a..f3fee1a7e 100644 --- a/server/internal/config/capture_pipeline.go +++ b/server/internal/config/capture_pipeline.go @@ -181,6 +181,39 @@ func NewVideoPipeline(rtpCodec codec.RTPCodec, display string, pipelineSrc strin pipelineStr = fmt.Sprintf(videoSrc+"video/x-raw,format=NV12 ! x264enc threads=4 bitrate=%d key-int-max=60 vbv-buf-capacity=%d byte-stream=true tune=zerolatency speed-preset=veryfast ! video/x-h264,stream-format=byte-stream,profile=constrained-baseline"+pipelineStr, display, fps, bitrate, vbvbuf) } + + case codec.H265().Name: + if err := gst.CheckPlugins([]string{"ximagesrc"}); err != nil { + return "", err + } + + vbvbuf := uint(1000) + if bitrate > 1000 { + vbvbuf = bitrate + } + + switch hwenc { + case HwEncVAAPI: + if err := gst.CheckPlugins([]string{"va"}); err != nil { + return "", err + } + + pipelineStr = fmt.Sprintf(videoSrc+"video/x-raw,format=NV12 ! vah265enc rate-control=cbr bitrate=%d key-int-max=60 target-usage=7 ! video/x-h265,stream-format=byte-stream,profile=main"+pipelineStr, display, fps, bitrate) + case HwEncNVENC: + if err := gst.CheckPlugins([]string{"nvcodec"}); err != nil { + return "", err + } + + pipelineStr = fmt.Sprintf(videoSrc+"video/x-raw,format=NV12 ! nvh265enc name=encoder rc-mode=cbr preset=p2 tune=low-latency gop-size=25 bitrate=%d vbv-buffer-size=%d ! h265parse config-interval=-1 ! video/x-h265,stream-format=byte-stream,profile=main"+pipelineStr, display, fps, bitrate, vbvbuf) + default: + // https://gstreamer.freedesktop.org/documentation/x265/index.html?gi-language=c + // gstreamer1.0-plugins-bad + if err := gst.CheckPlugins([]string{"x265"}); err != nil { + return "", err + } + + pipelineStr = fmt.Sprintf(videoSrc+"x265enc bitrate=%d key-int-max=60 tune=zerolatency speed-preset=veryfast option-string=\"vbv-maxrate=%d:vbv-bufsize=%d\" ! video/x-h265,stream-format=byte-stream,profile=main"+pipelineStr, display, fps, bitrate, bitrate, vbvbuf) + } default: return "", fmt.Errorf("unknown codec %s", rtpCodec.Name) } diff --git a/server/pkg/types/codec/codecs.go b/server/pkg/types/codec/codecs.go index 063a6a718..e4f537e06 100644 --- a/server/pkg/types/codec/codecs.go +++ b/server/pkg/types/codec/codecs.go @@ -35,6 +35,8 @@ func ParseStr(codecName string) (codec RTPCodec, ok bool) { codec = AV1() case H264().Name: codec = H264() + case H265().Name: + codec = H265() case Opus().Name: codec = Opus() case G722().Name: @@ -136,6 +138,24 @@ func H264() RTPCodec { } } +func H265() RTPCodec { + return RTPCodec{ + Name: "h265", + PayloadType: 116, + Type: webrtc.RTPCodecTypeVideo, + Capability: webrtc.RTPCodecCapability{ + MimeType: webrtc.MimeTypeH265, + ClockRate: 90000, + Channels: 0, + SDPFmtpLine: "profile-id=1;level-id=93;tx-mode=SRST", + RTCPFeedback: RTCPFeedback, + }, + // https://gstreamer.freedesktop.org/documentation/x265/index.html + // gstreamer1.0-plugins-bad + Pipeline: "x265enc bitrate=4096 key-int-max=60 tune=zerolatency speed-preset=veryfast option-string=\"vbv-maxrate=4096:vbv-bufsize=4096\" ! video/x-h265,stream-format=byte-stream,profile=main", + } +} + // TODO: Profile ID. func AV1() RTPCodec { return RTPCodec{ diff --git a/webpage/docs/configuration/capture.md b/webpage/docs/configuration/capture.md index 6349943ea..c0163e8c9 100644 --- a/webpage/docs/configuration/capture.md +++ b/webpage/docs/configuration/capture.md @@ -38,7 +38,7 @@ The Gstreamer pipeline is started when the first client requests the video strea ]} comments={false} /> - is the name of the [X display](https://www.x.org/wiki/) that you want to capture. If not specified, the environment variable `DISPLAY` will be used. -- available codecs are `vp8`, `vp9`, `av1`, `h264`. [Supported video codecs](https://developer.mozilla.org/en-US/docs/Web/Media/Guides/Formats/WebRTC_codecs#supported_video_codecs) are dependent on the WebRTC implementation used by the client, `vp8` and `h264` are supported by all WebRTC implementations. +- available codecs are `vp8`, `vp9`, `av1`, `h264`, `h265`. [Supported video codecs](https://developer.mozilla.org/en-US/docs/Web/Media/Guides/Formats/WebRTC_codecs#supported_video_codecs) are dependent on the WebRTC implementation used by the client, `vp8` and `h264` are supported by all WebRTC implementations. - is a list of pipeline ids that are defined in the section. The first pipeline in the list will be the default pipeline. - is a shorthand for defining [Gstreamer pipeline description](#video.gst_pipeline) for a single pipeline. This is option is ignored if is defined. - is a dictionary of pipeline configurations. Each pipeline configuration is defined by a unique pipeline id. They can be defined in two ways: either by building the pipeline dynamically using [Expression-Driven Configuration](#video.expression) or by defining the pipeline using a [Gstreamer Pipeline Description](#video.gst_pipeline). @@ -319,7 +319,7 @@ Overview of available encoders for each codec is shown in the table below. The e | VP9 | [vp9enc](https://gstreamer.freedesktop.org/documentation/vpx/vp9enc.html?gi-language=c) | [vaapivp9enc](https://github.com/GStreamer/gstreamer-vaapi/blob/master/gst/vaapi/gstvaapiencode_vp9.c) | ? | | AV1 | [av1enc](https://gstreamer.freedesktop.org/documentation/aom/av1enc.html?gi-language=c) | ? | [nvav1enc](https://gstreamer.freedesktop.org/documentation/nvcodec/nvav1enc.html?gi-language=c) | | H264 | [x264enc](https://gstreamer.freedesktop.org/documentation/x264/index.html?gi-language=c) | [vaapih264enc](https://gstreamer.freedesktop.org/documentation/vaapi/vaapih264enc.html?gi-language=c) | [nvh264enc](https://gstreamer.freedesktop.org/documentation/nvcodec/nvh264enc.html?gi-language=c) | -| H265 | [x265enc](https://gstreamer.freedesktop.org/documentation/x265/index.html?gi-language=c) | [vaapih265enc](https://gstreamer.freedesktop.org/documentation/vaapi/vaapih265enc.html?gi-language=c) | [nvh265enc](https://gstreamer.freedesktop.org/documentation/nvcodec/nvh265enc.html?gi-language=c) | +| H265 | [x265enc](https://gstreamer.freedesktop.org/documentation/x265/index.html?gi-language=c) | [vah265enc](https://gstreamer.freedesktop.org/documentation/va/vah265enc.html?gi-language=c) | [nvh265enc](https://gstreamer.freedesktop.org/documentation/nvcodec/nvh265enc.html?gi-language=c) | ## WebRTC Audio {#audio} From ab3dd6e04c2c687e8115c0148bc192264a70cb00 Mon Sep 17 00:00:00 2001 From: Jakub Budisky Date: Wed, 13 May 2026 21:58:14 +0200 Subject: [PATCH 2/3] Replace legacy vaapih264enc with vah264enc The gstreamer-vaapi plugin suite (vaapih264enc) is superseded by the va plugin (vah264enc) in gst-plugins-bad. Switch to it and update the parameters accordingly: CBR instead of VBR for lower-latency streaming, key-int-max instead of keyframe-period, and target-usage instead of quality-level. Co-Authored-By: Claude Sonnet 4.6 --- server/internal/config/capture_pipeline.go | 4 ++-- webpage/docs/configuration/capture.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/internal/config/capture_pipeline.go b/server/internal/config/capture_pipeline.go index f3fee1a7e..3250d1166 100644 --- a/server/internal/config/capture_pipeline.go +++ b/server/internal/config/capture_pipeline.go @@ -152,11 +152,11 @@ func NewVideoPipeline(rtpCodec codec.RTPCodec, display string, pipelineSrc strin switch hwenc { case HwEncVAAPI: - if err := gst.CheckPlugins([]string{"vaapi"}); err != nil { + if err := gst.CheckPlugins([]string{"va"}); err != nil { return "", err } - pipelineStr = fmt.Sprintf(videoSrc+"video/x-raw,format=NV12 ! vaapih264enc rate-control=vbr bitrate=%d keyframe-period=180 quality-level=7 ! video/x-h264,stream-format=byte-stream,profile=constrained-baseline"+pipelineStr, display, fps, bitrate) + pipelineStr = fmt.Sprintf(videoSrc+"video/x-raw,format=NV12 ! vah264enc rate-control=cbr bitrate=%d key-int-max=60 target-usage=7 ! h264parse config-interval=-1 ! video/x-h264,stream-format=byte-stream,profile=constrained-baseline"+pipelineStr, display, fps, bitrate) case HwEncNVENC: if err := gst.CheckPlugins([]string{"nvcodec"}); err != nil { return "", err diff --git a/webpage/docs/configuration/capture.md b/webpage/docs/configuration/capture.md index c0163e8c9..a9c286cdb 100644 --- a/webpage/docs/configuration/capture.md +++ b/webpage/docs/configuration/capture.md @@ -318,7 +318,7 @@ Overview of available encoders for each codec is shown in the table below. The e | VP8 | [vp8enc](https://gstreamer.freedesktop.org/documentation/vpx/vp8enc.html?gi-language=c) | [vaapivp8enc](https://github.com/GStreamer/gstreamer-vaapi/blob/master/gst/vaapi/gstvaapiencode_vp8.c) | ? | | VP9 | [vp9enc](https://gstreamer.freedesktop.org/documentation/vpx/vp9enc.html?gi-language=c) | [vaapivp9enc](https://github.com/GStreamer/gstreamer-vaapi/blob/master/gst/vaapi/gstvaapiencode_vp9.c) | ? | | AV1 | [av1enc](https://gstreamer.freedesktop.org/documentation/aom/av1enc.html?gi-language=c) | ? | [nvav1enc](https://gstreamer.freedesktop.org/documentation/nvcodec/nvav1enc.html?gi-language=c) | -| H264 | [x264enc](https://gstreamer.freedesktop.org/documentation/x264/index.html?gi-language=c) | [vaapih264enc](https://gstreamer.freedesktop.org/documentation/vaapi/vaapih264enc.html?gi-language=c) | [nvh264enc](https://gstreamer.freedesktop.org/documentation/nvcodec/nvh264enc.html?gi-language=c) | +| H264 | [x264enc](https://gstreamer.freedesktop.org/documentation/x264/index.html?gi-language=c) | [vah264enc](https://gstreamer.freedesktop.org/documentation/va/vah264enc.html?gi-language=c) | [nvh264enc](https://gstreamer.freedesktop.org/documentation/nvcodec/nvh264enc.html?gi-language=c) | | H265 | [x265enc](https://gstreamer.freedesktop.org/documentation/x265/index.html?gi-language=c) | [vah265enc](https://gstreamer.freedesktop.org/documentation/va/vah265enc.html?gi-language=c) | [nvh265enc](https://gstreamer.freedesktop.org/documentation/nvcodec/nvh265enc.html?gi-language=c) | From 0b195927d3370e4f023228aaad1dae22b4ab2e38 Mon Sep 17 00:00:00 2001 From: Jakub Budisky Date: Sat, 16 May 2026 10:05:23 +0200 Subject: [PATCH 3/3] Unify keyframe interval to 60 frames for NVENC H264/H265 encoder paths Co-Authored-By: Claude Sonnet 4.6 --- server/internal/config/capture_pipeline.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/internal/config/capture_pipeline.go b/server/internal/config/capture_pipeline.go index 3250d1166..7998eb88e 100644 --- a/server/internal/config/capture_pipeline.go +++ b/server/internal/config/capture_pipeline.go @@ -162,7 +162,7 @@ func NewVideoPipeline(rtpCodec codec.RTPCodec, display string, pipelineSrc strin return "", err } - pipelineStr = fmt.Sprintf(videoSrc+"video/x-raw,format=NV12 ! nvh264enc name=encoder preset=2 gop-size=25 spatial-aq=true temporal-aq=true bitrate=%d vbv-buffer-size=%d rc-mode=6 ! h264parse config-interval=-1 ! video/x-h264,stream-format=byte-stream,profile=constrained-baseline"+pipelineStr, display, fps, bitrate, vbvbuf) + pipelineStr = fmt.Sprintf(videoSrc+"video/x-raw,format=NV12 ! nvh264enc name=encoder preset=2 gop-size=60 spatial-aq=true temporal-aq=true bitrate=%d vbv-buffer-size=%d rc-mode=6 ! h264parse config-interval=-1 ! video/x-h264,stream-format=byte-stream,profile=constrained-baseline"+pipelineStr, display, fps, bitrate, vbvbuf) default: // https://gstreamer.freedesktop.org/documentation/openh264/openh264enc.html?gi-language=c#openh264enc // gstreamer1.0-plugins-bad @@ -198,13 +198,13 @@ func NewVideoPipeline(rtpCodec codec.RTPCodec, display string, pipelineSrc strin return "", err } - pipelineStr = fmt.Sprintf(videoSrc+"video/x-raw,format=NV12 ! vah265enc rate-control=cbr bitrate=%d key-int-max=60 target-usage=7 ! video/x-h265,stream-format=byte-stream,profile=main"+pipelineStr, display, fps, bitrate) + pipelineStr = fmt.Sprintf(videoSrc+"video/x-raw,format=NV12 ! vah265enc rate-control=cbr bitrate=%d key-int-max=60 target-usage=7 ! h265parse config-interval=-1 ! video/x-h265,stream-format=byte-stream,profile=main"+pipelineStr, display, fps, bitrate) case HwEncNVENC: if err := gst.CheckPlugins([]string{"nvcodec"}); err != nil { return "", err } - pipelineStr = fmt.Sprintf(videoSrc+"video/x-raw,format=NV12 ! nvh265enc name=encoder rc-mode=cbr preset=p2 tune=low-latency gop-size=25 bitrate=%d vbv-buffer-size=%d ! h265parse config-interval=-1 ! video/x-h265,stream-format=byte-stream,profile=main"+pipelineStr, display, fps, bitrate, vbvbuf) + pipelineStr = fmt.Sprintf(videoSrc+"video/x-raw,format=NV12 ! nvh265enc name=encoder rc-mode=cbr preset=p2 tune=low-latency gop-size=60 bitrate=%d vbv-buffer-size=%d ! h265parse config-interval=-1 ! video/x-h265,stream-format=byte-stream,profile=main"+pipelineStr, display, fps, bitrate, vbvbuf) default: // https://gstreamer.freedesktop.org/documentation/x265/index.html?gi-language=c // gstreamer1.0-plugins-bad