Skip to content

Commit

Permalink
#3750 re-use the same functions and encoder options
Browse files Browse the repository at this point in the history
for both the plain encoder and the capture-and-encode one,
also try to find a common encoding, not just 'h264' every time,
expose the 'profile' and other key attributes to the client
  • Loading branch information
totaam committed May 11, 2023
1 parent dd0c5af commit d30d3e9
Show file tree
Hide file tree
Showing 5 changed files with 253 additions and 180 deletions.
64 changes: 53 additions & 11 deletions xpra/codecs/gstreamer/capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,20 @@
from queue import Queue, Empty, Full
from gi.repository import GObject # @UnresolvedImport

from xpra.gst_common import import_gst, GST_FLOW_OK
from xpra.util import typedict
from xpra.gst_common import (
import_gst, GST_FLOW_OK, get_element_str,\
get_default_appsink_attributes, get_all_plugin_names,
get_caps_str,
)
from xpra.gtk_common.gobject_util import n_arg_signal
from xpra.gst_pipeline import Pipeline
from xpra.codecs.codec_constants import get_profile
from xpra.codecs.gstreamer.codec_common import (
get_version, get_type, get_info,
init_module, cleanup_module,
get_video_encoder_caps, get_video_encoder_options,
get_gst_encoding,
)
from xpra.codecs.image_wrapper import ImageWrapper
from xpra.log import Logger
Expand Down Expand Up @@ -83,7 +91,8 @@ def on_new_sample(self, _bus):
except Full:
log("image queue is already full")
else:
self.emit("new-image", self.frames, self.pixel_format, image)
client_info = {"frame" : self.frames}
self.emit("new-image", self.pixel_format, image, client_info)
return GST_FLOW_OK

def on_new_preroll(self, _appsink):
Expand Down Expand Up @@ -114,20 +123,50 @@ class CaptureAndEncode(Capture):
and encode it to a video stream
"""

def create_pipeline(self,
capture_element:str="ximagesrc",
encode_element:str="x264enc pass=4 speed-preset=1 tune=4 byte-stream=true quantizer=51 qp-max=51 qp-min=50"):
#encode_element="x264enc threads=8 pass=4 speed-preset=1 tune=zerolatency byte-stream=true quantizer=51 qp-max=51 qp-min=50"):
#encode_element="vp8enc deadline=1 min-quantizer=60 max-quantizer=63 cq-level=61"):
#encode_element="vp9enc deadline=1 error-resilient=1 min-quantizer=60 end-usage=2"):
def create_pipeline(self, capture_element:str="ximagesrc"):
#encode_element:str="x264enc pass=4 speed-preset=1 tune=4 byte-stream=true quantizer=51 qp-max=51 qp-min=50"):
#encode_element="x264enc threads=8 pass=4 speed-preset=1 tune=zerolatency byte-stream=true quantizer=51 qp-max=51 qp-min=50"):
#encode_element="vp8enc deadline=1 min-quantizer=60 max-quantizer=63 cq-level=61"):
#encode_element="vp9enc deadline=1 error-resilient=1 min-quantizer=60 end-usage=2"):
#desktopcast does this:
#https://github.com/seijikun/desktopcast/blob/9ae61739cedce078d197011f770f8e94d9a9a8b2/src/stream_server.rs#LL162C18-L162C18
#" ! videoconvert ! queue leaky=2 ! x264enc threads={} tune=zerolatency speed-preset=2 bframes=0 ! video/x-h264,profile=high ! queue ! rtph264pay name=pay0 pt=96",

#we are overloading "pixel_format" as "encoding":
encoding = self.pixel_format
encoder = {
"h264" : "x264enc",
"vp8" : "vp8enc",
"vp9" : "vp9enc",
"av1" : "av1enc",
}.get(encoding)
if not encoder:
raise ValueError(f"no encoder defined for {encoding}")
if encoder not in get_all_plugin_names():
raise RuntimeError(f"encoder {encoder} is not available")
options = typedict({
"speed" : 100,
"quality" : 100,
})
self.profile = get_profile(options, encoding, csc_mode="YUV444P", default_profile="high" if encoder=="x264enc" else None)
eopts = get_video_encoder_options(encoder, self.profile, options)
vcaps = get_video_encoder_caps(encoder)
self.extra_client_info = vcaps.copy()
if self.profile:
vcaps["profile"] = self.profile
self.extra_client_info["profile"] = self.profile
gst_encoding = get_gst_encoding(encoding) #ie: "hevc" -> "h265"
elements = [
capture_element, #ie: ximagesrc or pipewiresrc
#"videorate",
#"video/x-raw,framerate=20/1",
#"queue leaky=2 max-size-buffers=1",
"videoconvert",
encode_element,
"appsink name=sink emit-signals=true max-buffers=1 drop=false sync=false async=true qos=true",
"queue leaky=2",
get_element_str(encoder, eopts),
get_caps_str(f"video/x-{gst_encoding}", vcaps),
#"appsink name=sink emit-signals=true max-buffers=1 drop=false sync=false async=true qos=true",
get_element_str("appsink", get_default_appsink_attributes()),
]
if not self.setup_pipeline_and_bus(elements):
raise RuntimeError("failed to setup gstreamer pipeline")
Expand All @@ -145,7 +184,10 @@ def on_new_sample(self, _bus):
if size:
data = buf.extract_dup(0, size)
self.frames += 1
self.emit("new-image", self.frames, "h264", data)
client_info = self.extra_client_info
client_info["frame"] = self.frames
self.extra_client_info = {}
self.emit("new-image", self.pixel_format, data, client_info)
return GST_FLOW_OK

def on_new_preroll(self, _appsink):
Expand Down
162 changes: 160 additions & 2 deletions xpra/codecs/gstreamer/codec_common.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
# This file is part of Xpra.
# Copyright (C) 2014-2022 Antoine Martin <[email protected]>
# Copyright (C) 2014-2023 Antoine Martin <[email protected]>
# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
# later version. See the file COPYING for details.

import os
from queue import Queue, Empty

from xpra.util import typedict, envint
from xpra.util import typedict, envint, parse_simple_dict
from xpra.os_util import OSX
from xpra.gst_common import import_gst, GST_FLOW_OK
from xpra.gst_pipeline import Pipeline
from xpra.log import Logger
Expand All @@ -17,6 +19,108 @@
FRAME_QUEUE_INITIAL_TIMEOUT = envint("XPRA_GSTREAMER_FRAME_QUEUE_INITIAL_TIMEOUT", 3)


def get_default_encoder_options():
options = {
"vaapih264enc" : {
"max-bframes" : 0, #int(options.boolget("b-frames", False))
#"tune" : 3, #low-power
#"rate-control" : 8, #qvbr
"compliance-mode" : 0, #restrict-buf-alloc (1) – Restrict the allocation size of coded-buffer
#"keyframe-period" : 9999,
#"prediction-type" : 1, #hierarchical-p (1) – Hierarchical P frame encode
#"quality-factor" : 10,
#"quality-level" : 50,
#"bitrate" : 2000,
#"prediction-type" : 1, #Hierarchical P frame encode
#"keyframe-period" : 4294967295,
"aud" : True,
},
"vaapih265enc" : {
"max-bframes" : 0, #int(options.boolget("b-frames", False))
#"tune" : 3, #low-power
#"rate-control" : 8, #qvbr
},
"x264enc" : {
"speed-preset" : "ultrafast",
"tune" : "zerolatency",
"byte-stream" : True,
"threads" : 1,
"key-int-max" : 15,
"intra-refresh" : True,
},
"vp8enc" : {
"deadline" : 1,
"error-resilient" : 0,
},
"vp9enc" : {
"deadline" : 1,
"error-resilient" : 0,
"lag-in-frames" : 0,
"cpu-used" : 16,
},
"nvh264enc" : {
"zerolatency" : True,
"rc-mode" : 3, #vbr
"preset" : 5, #low latency, high performance
"bframes" : 0,
"aud" : True,
},
"nvh265enc" : {
"zerolatency" : True,
"rc-mode" : 3, #vbr
"preset" : 5, #low latency, high performance
#should be in GStreamer 1.18, but somehow missing?
#"bframes" : 0,
"aud" : True,
},
"nvd3d11h264enc" : {
"bframes" : 0,
"aud" : True,
"preset" : 5, #low latency, high performance
"zero-reorder-delay" : True,
},
"nvd3d11h265enc" : {
"bframes" : 0,
"aud" : True,
"preset" : 5, #low latency, high performance
"zero-reorder-delay" : True,
},
"svtav1enc" : {
# "speed" : 12,
# "gop-size" : 251,
"intra-refresh" : 1, #open gop
# "lookahead" : 0,
# "rc" : 1, #vbr
},
"svtvp9enc" : {
},
#"svthevcenc" : {
# "b-pyramid" : 0,
# "baselayer-mode" : 1,
# "enable-open-gop" : True,
# "key-int-max" : 255,
# "lookahead" : 0,
# "pred-struct" : 0,
# "rc" : 1, #vbr
# "speed" : 9,
# "tune" : 0,
# }
}
if not OSX:
options["av1enc"] = {
"cpu-used" : 5,
"end-usage" : 2, #cq
}
#now apply environment overrides:
for element in options.keys():
enc_options_str = os.environ.get(f"XPRA_{element.upper()}_OPTIONS", "")
if enc_options_str:
encoder_options = parse_simple_dict(enc_options_str)
log(f"user overridden options for {element}: {encoder_options}")
options[element] = encoder_options
return options


def get_version():
return (5, 0)

Expand All @@ -33,6 +137,60 @@ def cleanup_module():
log("gstreamer.cleanup_module()")


def get_gst_rgb_format(rgb_format : str) -> str:
if rgb_format in (
"NV12",
"RGBA", "BGRA", "ARGB", "ABGR",
"RGB", "BGR",
"RGB15", "RGB16", "BGR15",
"r210",
"BGRP", "RGBP",
):
#identical name:
return rgb_format
#translate to gstreamer name:
return {
"YUV420P" : "I420",
"YUV444P" : "Y444",
"BGRX" : "BGRx",
"XRGB" : "xRGB",
"XBGR" : "xBGR",
"YUV400" : "GRAY8",
#"RGB8P"
}[rgb_format]


def get_video_encoder_caps(encoder="x264enc"):
if encoder=="av1enc":
return {
"alignment" : "tu",
"stream-format" : "obu-stream",
}
return {
"alignment" : "au",
"stream-format" : "byte-stream",
}

def get_video_encoder_options(encoder:str="x264", profile:str=None, options:typedict=None):
eopts = get_default_encoder_options().get(encoder, {})
eopts["name"] = "encoder"
if encoder=="x264enc" and options:
from xpra.codecs.codec_constants import get_x264_quality, get_x264_preset
q = get_x264_quality(options.intget("quality", 50), profile)
s = options.intget("speed", 50)
eopts.update({
"pass" : "qual",
"quantizer" : q,
"speed-preset" : get_x264_preset(s),
})
#if "bframes" in self.encoder_options:
# eopts["bframes"] = int(options.boolget("b-frames", False))
return eopts

def get_gst_encoding(encoding):
return {"hevc" : "h265"}.get(encoding, encoding)


class VideoPipeline(Pipeline):
__generic_signals__ : dict = Pipeline.__generic_signals__.copy()
"""
Expand Down
Loading

0 comments on commit d30d3e9

Please sign in to comment.