Skip to content

Commit

Permalink
#3837 add color range metadata to frames
Browse files Browse the repository at this point in the history
for backwards compatibility, the client must expose the ranges it supports for each encoding,
if it does not, then we assume the old defaults
  • Loading branch information
totaam committed May 17, 2024
1 parent 9ca4a94 commit 0ad554d
Show file tree
Hide file tree
Showing 13 changed files with 196 additions and 52 deletions.
4 changes: 3 additions & 1 deletion xpra/client/gl/backing.py
Original file line number Diff line number Diff line change
Expand Up @@ -1352,6 +1352,8 @@ def do_video_paint(self, img,
super().do_video_paint(img, x, y, enc_width, enc_height, width, height, options, callbacks)
return
shader = f"{pixel_format}_to_RGB"
if img.get_full_range():
shader += "_FULL"
flush = options.intget("flush", 0)
encoding = options.strget("encoding")
self.with_gfx_context(self.gl_paint_planar, shader, flush, encoding, img,
Expand Down Expand Up @@ -1391,7 +1393,7 @@ def gl_paint_planar(self, context, shader: str, flush: int, encoding: str, img,
flush, img, (x, y, enc_width, enc_height), width, height)
fire_paint_callbacks(callbacks, False, message)

def update_planar_textures(self, width: int, height: int, img, pixel_format, scaling=False, pbo=False) -> None:
def update_planar_textures(self, width: int, height: int, img, pixel_format: str, scaling=False, pbo=False) -> None:
if len(self.textures) == 0:
raise RuntimeError("no OpenGL textures")
upload_formats = PIXEL_UPLOAD_FORMAT[pixel_format]
Expand Down
9 changes: 9 additions & 0 deletions xpra/client/mixins/encodings.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,15 @@ def get_encodings_caps(self) -> dict[str, Any]:
log("supported full csc_modes=%s", full_csc_modes)
caps["full_csc_modes"] = full_csc_modes

# advertise that this clients can handle both 'full' and 'studio' csc range:
ranges = ("full", "studio")
csc_ranges = {}
for encoding in self.get_core_encodings():
# ranges are meaningless for rgb or png:
if encoding.find("rgb") < 0 and encoding.find("png") < 0:
csc_ranges[encoding] = ranges
caps["csc-ranges"] = csc_ranges

if "h264" in self.get_core_encodings():
# some profile options: "baseline", "main", "high", "high10", ...
# set the default to "high10" for YUV420P
Expand Down
4 changes: 3 additions & 1 deletion xpra/codecs/avif/decoder.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,9 @@ def decompress(data, options=None, yuv=False) -> ImageWrapper:
may_save_image("avif", data)
if decoder.imageCount>1:
log.warn("Warning: more than one image in avif data")
return ImageWrapper(0, 0, width, height, memoryview(pixels), rgb_format, bpp, stride, planes=ImageWrapper.PACKED)
img = ImageWrapper(0, 0, width, height, memoryview(pixels), rgb_format, bpp, stride, planes=ImageWrapper.PACKED)
img.set_full_range(image.yuvRange == AVIF_RANGE_FULL)
return img
finally:
avifDecoderDestroy(decoder)

Expand Down
7 changes: 5 additions & 2 deletions xpra/codecs/avif/encoder.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ from xpra.codecs.avif.avif cimport (
AVIF_RESULT_OK, AVIF_RESULT,
AVIF_VERSION_MAJOR, AVIF_VERSION_MINOR, AVIF_VERSION_PATCH,
AVIF_QUANTIZER_LOSSLESS, AVIF_QUANTIZER_BEST_QUALITY, AVIF_QUANTIZER_WORST_QUALITY,
AVIF_RANGE_FULL,
AVIF_RANGE_LIMITED, AVIF_RANGE_FULL,
avifResult,
avifRWData,
AVIF_PIXEL_FORMAT_NONE, AVIF_PIXEL_FORMAT_YUV400,
Expand Down Expand Up @@ -138,7 +138,10 @@ def encode(coding: str, image: ImageWrapper, options=None) -> Tuple:
rgb.pixels = <uint8_t*> (<uintptr_t> int(bc))
log("avif.encode(%s, %s, %s) pixels=%#x", coding, image, options, int(bc))
try:
avif_image.yuvRange = AVIF_RANGE_FULL
if image.get_full_range():
avif_image.yuvRange = AVIF_RANGE_FULL
else:
avif_image.yuvRange = AVIF_RANGE_LIMITED
if grayscale:
avif_image.yuvFormat = AVIF_PIXEL_FORMAT_YUV400
client_options["subsampling"] = "YUV400"
Expand Down
15 changes: 11 additions & 4 deletions xpra/codecs/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def __init__(self, x: int, y: int, width: int, height: int, pixels, pixel_format
self.freed: bool = False
self.timestamp: int = int(monotonic() * 1000)
self.palette = palette
self.full_range = False
if width <= 0 or height <= 0:
raise ValueError(f"invalid geometry {x},{y},{width},{height}")

Expand Down Expand Up @@ -113,6 +114,9 @@ def get_planes(self) -> PlanarFormat:
def get_palette(self):
return self.palette

def get_full_range(self) -> bool:
return self.full_range

def get_gpu_buffer(self):
return None

Expand All @@ -130,21 +134,24 @@ def get_timestamp(self) -> int:
""" time in millis """
return self.timestamp

def set_timestamp(self, timestamp: int):
def set_timestamp(self, timestamp: int) -> None:
self.timestamp = timestamp

def set_planes(self, planes: PlanarFormat):
def set_planes(self, planes: PlanarFormat) -> None:
self.planes = planes

def set_rowstride(self, rowstride: int):
def set_rowstride(self, rowstride: int) -> None:
self.rowstride = rowstride

def set_pixel_format(self, pixel_format):
def set_pixel_format(self, pixel_format) -> None:
self.pixel_format = pixel_format

def set_palette(self, palette) -> None:
self.palette = palette

def set_full_range(self, full_range: bool) -> None:
self.full_range = full_range

def set_pixels(self, pixels) -> None:
if self.freed:
raise RuntimeError("image wrapper has already been freed")
Expand Down
104 changes: 81 additions & 23 deletions xpra/codecs/libyuv/converter.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ cdef extern from "libyuv/convert_from_argb.h" namespace "libyuv":
uint8_t* dst_rgb24, int dst_stride_rgb24,
int width, int height) nogil

int J420ToRGB24(const uint8_t* src_y, int src_stride_y,
const uint8_t* src_u, int src_stride_u,
const uint8_t* src_v, int src_stride_v,
uint8_t* dst_rgb24, int dst_stride_rgb24,
int width, int height) nogil

int I420ToRGBA(const uint8_t* src_y, int src_stride_y,
const uint8_t* src_u, int src_stride_u,
const uint8_t* src_v, int src_stride_v,
Expand All @@ -80,6 +86,12 @@ cdef extern from "libyuv/convert_from_argb.h" namespace "libyuv":
uint8_t* dst_abgr, int dst_stride_abgr,
int width, int height) nogil

int J420ToABGR(const uint8_t* src_y, int src_stride_y,
const uint8_t* src_u, int src_stride_u,
const uint8_t* src_v, int src_stride_v,
uint8_t* dst_abgr, int dst_stride_abgr,
int width, int height) nogil

int I444ToARGB(const uint8_t* src_y, int src_stride_y,
const uint8_t* src_u, int src_stride_u,
const uint8_t* src_v, int src_stride_v,
Expand Down Expand Up @@ -305,6 +317,7 @@ cdef class Converter:
cdef int src_height
cdef int dst_width
cdef int dst_height
cdef int dst_full_range
cdef uint8_t yuv_scaling
cdef uint8_t rgb_scaling
cdef int planes
Expand Down Expand Up @@ -333,7 +346,8 @@ cdef class Converter:
cdef object __weakref__

def init_context(self, int src_width, int src_height, src_format: str,
int dst_width, int dst_height, dst_format: str, options: typedict) -> None:
int dst_width, int dst_height, dst_format: str,
options: typedict) -> None:
log("libyuv.Converter.init_context%s", (
src_width, src_height, src_format, dst_width, dst_height, dst_format, options))
if src_format not in COLORSPACES:
Expand All @@ -348,6 +362,7 @@ cdef class Converter:
self.src_height = src_height
self.dst_width = dst_width
self.dst_height = dst_height
self.dst_full_range = int("full" in options.strtupleget("ranges"))
self.out_buffer_size = 0
self.scaled_buffer_size = 0
self.time = 0
Expand Down Expand Up @@ -629,6 +644,7 @@ cdef class Converter:
cdef int iplanes = image.get_planes()
cdef int width = image.get_width()
cdef int height = image.get_height()
cdef int full_range = image.get_full_range()
if iplanes!=3:
raise ValueError(f"invalid number of planes: {iplanes} for {self.src_format}")
if self.src_format not in ("YUV420P", "YUV444P"):
Expand Down Expand Up @@ -663,13 +679,21 @@ cdef class Converter:
if self.dst_format=="RGB" and self.src_format=="YUV420P":
matched = 1
with nogil:
r = I420ToRGB24(<const uint8_t*> y, y_stride,
<const uint8_t*> u, u_stride,
<const uint8_t*> v, v_stride,
rgb, rowstride,
width, height)
if full_range:
r = J420ToRGB24(<const uint8_t*> y, y_stride,
<const uint8_t*> u, u_stride,
<const uint8_t*> v, v_stride,
rgb, rowstride,
width, height)
else:
r = I420ToRGB24(<const uint8_t*> y, y_stride,
<const uint8_t*> u, u_stride,
<const uint8_t*> v, v_stride,
rgb, rowstride,
width, height)
elif self.dst_format=="XBGR" and self.src_format=="YUV420P":
matched = 1
# we can't handle full-range here!
with nogil:
r = I420ToRGBA(<const uint8_t*> y, y_stride,
<const uint8_t*> u, u_stride,
Expand All @@ -679,14 +703,28 @@ cdef class Converter:
elif self.dst_format=="BGRX" and self.src_format=="YUV444P":
matched = 1
with nogil:
r = I444ToARGB(<const uint8_t*> y, y_stride,
<const uint8_t*> u, u_stride,
<const uint8_t*> v, v_stride,
rgb, rowstride,
width, height)
if full_range:
r = J444ToARGB(<const uint8_t*> y, y_stride,
<const uint8_t*> u, u_stride,
<const uint8_t*> v, v_stride,
rgb, rowstride,
width, height)
else:
r = I444ToARGB(<const uint8_t*> y, y_stride,
<const uint8_t*> u, u_stride,
<const uint8_t*> v, v_stride,
rgb, rowstride,
width, height)
elif self.dst_format=="RGBX" and self.src_format=="YUV420P":
matched = 1
with nogil:
if full_range:
r = J420ToABGR(<const uint8_t*> y, y_stride,
<const uint8_t*> u, u_stride,
<const uint8_t*> v, v_stride,
rgb, rowstride,
width, height)
else:
r = I420ToABGR(<const uint8_t*> y, y_stride,
<const uint8_t*> u, u_stride,
<const uint8_t*> v, v_stride,
Expand All @@ -695,11 +733,18 @@ cdef class Converter:
elif self.src_format=="YUV444P" and self.dst_format=="RGBX":
matched = 1
with nogil:
r = I444ToABGR(<const uint8_t*> y, y_stride,
<const uint8_t*> u, u_stride,
<const uint8_t*> v, v_stride,
rgb, rowstride,
width, height)
if full_range:
r = J444ToABGR(<const uint8_t*> y, y_stride,
<const uint8_t*> u, u_stride,
<const uint8_t*> v, v_stride,
rgb, rowstride,
width, height)
else:
r = I444ToABGR(<const uint8_t*> y, y_stride,
<const uint8_t*> u, u_stride,
<const uint8_t*> v, v_stride,
rgb, rowstride,
width, height)
if not matched:
raise RuntimeError(f"unexpected formats: src={self.src_format}, dst={self.dst_format}")
if r!=0:
Expand Down Expand Up @@ -744,22 +789,34 @@ cdef class Converter:
out_planes[i] = <uint8_t*> (memalign_ptr(<uintptr_t> output_buffer) + self.out_offsets[i])
#get pointer to input:
cdef int result = -1
cdef int full_range = self.dst_full_range
cdef const uint8_t* src
with buffer_context(pixels) as bc:
src = <const uint8_t*> (<uintptr_t> int(bc))
with nogil:
if self.dst_format=="NV12":
# we can't handle full-range here!
full_range = 0
result = ARGBToNV12(src, stride,
out_planes[0], self.out_stride[0],
out_planes[1], self.out_stride[1],
width, height)
elif self.dst_format=="YUV420P":
result = ARGBToJ420(src, stride,
out_planes[0], self.out_stride[0],
out_planes[1], self.out_stride[1],
out_planes[2], self.out_stride[2],
width, height)
if full_range:
result = ARGBToJ420(src, stride,
out_planes[0], self.out_stride[0],
out_planes[1], self.out_stride[1],
out_planes[2], self.out_stride[2],
width, height)
else:
result = ARGBToI420(src, stride,
out_planes[0], self.out_stride[0],
out_planes[1], self.out_stride[1],
out_planes[2], self.out_stride[2],
width, height)
elif self.dst_format=="YUV444P":
# we can't handle full-range here!
full_range = 0
result = ARGBToI444(src, stride,
out_planes[0], self.out_stride[0],
out_planes[1], self.out_stride[1],
Expand All @@ -768,9 +825,9 @@ cdef class Converter:
else:
raise RuntimeError(f"unexpected src format {self.src_format}")
if result!=0:
raise RuntimeError(f"libyuv ARGBToJ420/NV12 failed and returned {result}")
raise RuntimeError(f"libyuv ARGB to {self.dst_format} failed and returned {result}")
cdef double elapsed = monotonic()-start
log("libyuv.ARGBToI420/NV12 took %.1fms", 1000.0*elapsed)
log("libyuv.ARGB to %s took %.1fms", self.dst_format, 1000.0*elapsed)
self.time += elapsed
cdef object planes = []
cdef object strides = []
Expand Down Expand Up @@ -804,6 +861,7 @@ cdef class Converter:
self.frames += 1
out_image = YUVImageWrapper(0, 0, self.dst_width, self.dst_height, planes, self.dst_format, 24, strides, 1, self.planes)
out_image.cython_buffer = <uintptr_t> output_buffer
out_image.set_full_range(bool(full_range))
return out_image


Expand Down
4 changes: 3 additions & 1 deletion xpra/codecs/vpx/decoder.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,9 @@ cdef class Decoder:
self.encoding, self.frames, elapsed, VPX_COLOR_SPACES.get(img.cs, img.cs),
VPX_COLOR_RANGES.get(img.range, img.range), self.dst_format,
)
return ImageWrapper(0, 0, self.width, self.height, pixels, self.get_colorspace(), 24, strides, 1, ImageWrapper.PLANAR_3)
image = ImageWrapper(0, 0, self.width, self.height, pixels, self.get_colorspace(), 24, strides, 1, ImageWrapper.PLANAR_3)
image.set_full_range(img.range == VPX_CR_FULL_RANGE)
return image

def codec_error_str(self) -> str:
return vpx_codec_error(self.context).decode("latin1")
Expand Down
13 changes: 9 additions & 4 deletions xpra/codecs/vpx/encoder.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ from xpra.codecs.vpx.vpx cimport (
vpx_codec_version_str, vpx_codec_build_config,
VPX_IMG_FMT_I420, VPX_IMG_FMT_I444, VPX_IMG_FMT_I44416,
vpx_image_t,
VPX_CS_BT_709, VPX_CR_FULL_RANGE,
VPX_CS_BT_709, VPX_CR_FULL_RANGE, VPX_CR_STUDIO_RANGE,
vpx_codec_err_to_string, vpx_codec_control_,
)
from libc.stdint cimport uint8_t, int64_t
Expand Down Expand Up @@ -567,6 +567,7 @@ cdef class Encoder:
assert self.context!=NULL
pixels = image.get_pixels()
istrides = image.get_rowstride()
cdef int full_range = int(image.get_full_range())
pf = image.get_pixel_format().replace("A", "X")
if pf != self.src_format:
raise ValueError(f"expected {self.src_format} but got {image.get_pixel_format()}")
Expand Down Expand Up @@ -604,9 +605,10 @@ cdef class Encoder:
py_buf[i].len, "YUV"[i], istrides[i]*(self.height//ydiv))
pic_in[i] = <uint8_t *> py_buf[i].buf
strides[i] = istrides[i]
return self.do_compress_image(pic_in, strides), {
return self.do_compress_image(pic_in, strides, full_range), {
"csc" : self.src_format,
"frame" : int(self.frames),
"full-range" : bool(full_range),
#"quality" : min(99+self.lossless, self.quality),
#"speed" : self.speed,
}
Expand All @@ -615,7 +617,7 @@ cdef class Encoder:
if py_buf[i].buf:
PyBuffer_Release(&py_buf[i])

cdef do_compress_image(self, uint8_t *pic_in[3], int strides[3]):
cdef do_compress_image(self, uint8_t *pic_in[3], int strides[3], int full_range):
#actual compression (no gil):
cdef vpx_codec_iter_t iter = NULL
cdef int flags = 0
Expand All @@ -627,7 +629,10 @@ cdef class Encoder:
image.h = self.height
image.fmt = self.pixfmt
image.cs = VPX_CS_BT_709
image.range = VPX_CR_FULL_RANGE
if full_range:
image.range = VPX_CR_FULL_RANGE
else:
image.range = VPX_CR_STUDIO_RANGE
for i in range(3):
image.planes[i] = pic_in[i]
image.stride[i] = strides[i]
Expand Down
Loading

0 comments on commit 0ad554d

Please sign in to comment.