├── .gitignore ├── .github ├── CODEOWNERS └── workflows │ └── ci.yaml ├── Dockerfile-dev-dependencies ├── docker ├── cef-config.sh ├── install-arch-specific-dependencies.sh └── build-gstreamer │ ├── download │ ├── patch │ ├── 0001-GstAudioAggregator-fix-structure-unref.patch │ ├── 0001-Reduce-logs-verbosity-in-webrtcbin-when-a-FEC-decode.patch │ ├── 0001-cefsrc-unmultiply-alpha.patch │ ├── 0001-baseparse-always-update-the-input-pts-if-available-from.patch │ ├── 0001-pad-Check-data-NULL-ness-when-probes-are-stopped-fixed-in-1.24.8.patch │ ├── 0001-libnice.wrap-fix-incomplete-TCP-ICE-candidate-crash.patch │ ├── 0001-gsth264parser-fix-uint32-to-int32-truncation.patch │ ├── 0001-glvideomixer-update-API-to-be-compatible-with-versio.patch │ ├── 0001-compositor-add-rounded-corners-property-for-sink-pad.patch │ └── 0001-compositor-add-box-shadow-property-for-sink-pads.patch │ ├── compile │ └── install-dependencies ├── Dockerfile-latest-dev-with-source.in ├── Dockerfile-latest-dev.in ├── Dockerfile-latest-prod-dbg.in ├── Dockerfile-latest-prod.in ├── sccache.toml ├── Dockerfile-dev-downloaded.in ├── copying.md ├── push-release.sh ├── push-latest.sh ├── push-multi-arch-latest.sh ├── README.md └── Dockerfile-prod-base.in /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | .github/ @restreamio/sre 2 | -------------------------------------------------------------------------------- /Dockerfile-dev-dependencies: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 2 | 3 | COPY docker/build-gstreamer/install-dependencies / 4 | 5 | RUN ["/install-dependencies"] 6 | -------------------------------------------------------------------------------- /docker/cef-config.sh: -------------------------------------------------------------------------------- 1 | 2 | export CEF_DOWNLOAD_URL=https://download.restream.io/gstreamer/cef 3 | export CEF_VERSION=106.1.1+g5891c70+chromium-106.0.5249.119 4 | -------------------------------------------------------------------------------- /docker/install-arch-specific-dependencies.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ARCH=$(dpkg --print-architecture) 4 | if [[ $ARCH == "amd64" ]]; then 5 | apt-get install -y --no-install-recommends \ 6 | libmfx1 \ 7 | intel-media-va-driver-non-free 8 | fi 9 | -------------------------------------------------------------------------------- /Dockerfile-latest-dev-with-source.in: -------------------------------------------------------------------------------- 1 | FROM restreamio/gstreamer:$TARGET_ARCH-dev-downloaded 2 | 3 | ARG WEBKIT_USE_SCCACHE=0 4 | 5 | ENV DEBUG=true 6 | ENV OPTIMIZATIONS=false 7 | 8 | # Compile binaries with debug symbols and keep source code 9 | RUN ["/compile"] 10 | -------------------------------------------------------------------------------- /Dockerfile-latest-dev.in: -------------------------------------------------------------------------------- 1 | FROM restreamio/gstreamer:$TARGET_ARCH-latest-dev-with-source 2 | 3 | # Only development dependencies 4 | FROM restreamio/gstreamer:$TARGET_ARCH-dev-dependencies 5 | 6 | # And binaries built with debug symbols 7 | COPY --from=0 /compiled-binaries / 8 | -------------------------------------------------------------------------------- /docker/build-gstreamer/download: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | git clone -b "$GSTREAMER_VERSION" https://gitlab.freedesktop.org/gstreamer/gstreamer.git 5 | 6 | git clone --no-checkout "$GSTREAMER_CEF_REPOSITORY" 7 | pushd gstcefsrc 8 | git checkout "$GSTREAMER_CEF_CHECKOUT" 9 | popd 10 | -------------------------------------------------------------------------------- /Dockerfile-latest-prod-dbg.in: -------------------------------------------------------------------------------- 1 | FROM restreamio/gstreamer:$TARGET_ARCH-dev-downloaded 2 | 3 | ARG WEBKIT_USE_SCCACHE=0 4 | 5 | ENV DEBUG=true 6 | ENV OPTIMIZATIONS=true 7 | 8 | RUN ["/compile"] 9 | 10 | FROM restreamio/gstreamer:$TARGET_ARCH-prod-base 11 | 12 | COPY --from=0 /compiled-binaries / 13 | -------------------------------------------------------------------------------- /Dockerfile-latest-prod.in: -------------------------------------------------------------------------------- 1 | FROM restreamio/gstreamer:$TARGET_ARCH-dev-downloaded 2 | 3 | ARG WEBKIT_USE_SCCACHE=0 4 | 5 | ENV DEBUG=false 6 | ENV OPTIMIZATIONS=true 7 | 8 | RUN ["/compile"] 9 | 10 | FROM restreamio/gstreamer:$TARGET_ARCH-prod-base 11 | 12 | COPY --from=0 /compiled-binaries / 13 | -------------------------------------------------------------------------------- /sccache.toml: -------------------------------------------------------------------------------- 1 | [dist] 2 | scheduler_url = "@@SCCACHE_SCHEDULER@@" 3 | 4 | [dist.auth] 5 | type = "token" 6 | token = "@@SCCACHE_AUTH_TOKEN@@" 7 | 8 | [[dist.toolchains]] 9 | type = "path_override" 10 | compiler_executable = "/usr/bin/clang" 11 | archive = "/toolchain-clang-12.tar.gz" 12 | archive_compiler_executable = "/usr/bin/clang-12" 13 | 14 | [[dist.toolchains]] 15 | type = "path_override" 16 | compiler_executable = "/usr/bin/clang++" 17 | archive = "/toolchain-clang-12.tar.gz" 18 | archive_compiler_executable = "/usr/bin/clang-12" 19 | -------------------------------------------------------------------------------- /Dockerfile-dev-downloaded.in: -------------------------------------------------------------------------------- 1 | FROM restreamio/gstreamer:$TARGET_ARCH-dev-dependencies 2 | 3 | ARG GSTREAMER_VERSION=1.22.12 4 | 5 | ARG GSTREAMER_CEF_REPOSITORY=https://github.com/centricular/gstcefsrc 6 | ARG GSTREAMER_CEF_CHECKOUT=cce144d984e70cc88f4624390419171fea6ca8ee 7 | 8 | COPY docker/cef-config.sh /.cef-config.sh 9 | 10 | COPY docker/sccache.toml /sccache.toml 11 | 12 | COPY docker/build-gstreamer/download /download 13 | 14 | RUN ["/download"] 15 | 16 | COPY docker/build-gstreamer/compile /compile 17 | COPY docker/build-gstreamer/patch /compile-patch 18 | -------------------------------------------------------------------------------- /copying.md: -------------------------------------------------------------------------------- 1 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. 2 | 3 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 4 | -------------------------------------------------------------------------------- /push-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | if [[ (-z "$1") || (-z "$2") || (-z "$3") ]]; then 5 | echo -e "Usage example:\n $0 amd64 1.22.12 0" 6 | exit 1 7 | fi 8 | 9 | TAG_BASENAME="restreamio/gstreamer:$1-$2" 10 | 11 | docker push $TAG_BASENAME-dev-with-source 12 | docker tag $TAG_BASENAME-dev-with-source $TAG_BASENAME.$3-dev-with-source 13 | docker push $TAG_BASENAME.$3-dev-with-source 14 | 15 | docker push $TAG_BASENAME-dev 16 | docker tag $TAG_BASENAME-dev $TAG_BASENAME.$3-dev 17 | docker push $TAG_BASENAME.$3-dev 18 | 19 | docker push $TAG_BASENAME-prod 20 | docker tag $TAG_BASENAME-prod $TAG_BASENAME.$3-prod 21 | docker push $TAG_BASENAME.$3-prod 22 | 23 | docker push $TAG_BASENAME-prod-dbg 24 | docker tag $TAG_BASENAME-prod-dbg $TAG_BASENAME.$3-prod-dbg 25 | docker push $TAG_BASENAME.$3-prod-dbg 26 | -------------------------------------------------------------------------------- /push-latest.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | echo $DATE 5 | 6 | ARCH=$(uname -m) 7 | TAG_BASENAME="restreamio/gstreamer:$ARCH" 8 | 9 | echo "$TAG_BASENAME-$DATE" > docker-tag-basename-$ARCH.txt 10 | 11 | docker push $TAG_BASENAME-latest-dev-with-source 12 | docker tag $TAG_BASENAME-latest-dev-with-source $TAG_BASENAME-$DATE-dev-with-source 13 | docker push $TAG_BASENAME-$DATE-dev-with-source 14 | 15 | docker push $TAG_BASENAME-latest-dev 16 | docker tag $TAG_BASENAME-latest-dev $TAG_BASENAME-$DATE-dev 17 | docker push $TAG_BASENAME-$DATE-dev 18 | 19 | docker push $TAG_BASENAME-latest-prod 20 | docker tag $TAG_BASENAME-latest-prod $TAG_BASENAME-$DATE-prod 21 | docker push $TAG_BASENAME-$DATE-prod 22 | 23 | docker push $TAG_BASENAME-latest-prod-dbg 24 | docker tag $TAG_BASENAME-latest-prod-dbg $TAG_BASENAME-$DATE-prod-dbg 25 | docker push $TAG_BASENAME-$DATE-prod-dbg 26 | -------------------------------------------------------------------------------- /push-multi-arch-latest.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | echo $DATE 5 | 6 | TAG_BASENAME="restreamio/gstreamer:$DATE" 7 | 8 | echo "basename tag: $TAG_BASENAME" 9 | 10 | AMD64_TAG_BASENAME=$(cat ./docker-tag-basename-x86_64.txt) 11 | ARM64_TAG_BASENAME=$(cat ./docker-tag-basename-aarch64.txt) 12 | 13 | echo "amd64 basename tag: $AMD64_TAG_BASENAME" 14 | echo "arm64 basename tag: $ARM64_TAG_BASENAME" 15 | 16 | docker manifest create $TAG_BASENAME-dev-with-source --amend $AMD64_TAG_BASENAME-dev-with-source --amend $ARM64_TAG_BASENAME-dev-with-source 17 | docker manifest push docker.io/$TAG_BASENAME-dev-with-source 18 | 19 | docker manifest create $TAG_BASENAME-dev --amend $AMD64_TAG_BASENAME-dev --amend $ARM64_TAG_BASENAME-dev 20 | docker manifest push docker.io/$TAG_BASENAME-dev 21 | 22 | docker manifest create $TAG_BASENAME-prod --amend $AMD64_TAG_BASENAME-prod --amend $ARM64_TAG_BASENAME-prod 23 | docker manifest push docker.io/$TAG_BASENAME-prod 24 | 25 | docker manifest create $TAG_BASENAME-prod-dbg --amend $AMD64_TAG_BASENAME-prod-dbg --amend $ARM64_TAG_BASENAME-prod-dbg 26 | docker manifest push docker.io/$TAG_BASENAME-prod-dbg 27 | -------------------------------------------------------------------------------- /docker/build-gstreamer/patch/0001-GstAudioAggregator-fix-structure-unref.patch: -------------------------------------------------------------------------------- 1 | From 4c5779845cb8dedf0797b78c382f8f0f8b0d19e2 Mon Sep 17 00:00:00 2001 2 | From: =?UTF-8?q?Lo=C3=AFc=20Le=20Page?= 3 | Date: Wed, 18 Jun 2025 18:46:56 +0200 4 | Subject: [PATCH] GstAudioAggregator: fix structure unref in peek_next_sample 5 | 6 | The GstStructure attached to the audio sample in peek_next_sample() was 7 | freed prematurely before usage as gst_sample_new() is taking full 8 | ownership on it. 9 | --- 10 | .../gst-plugins-base/gst-libs/gst/audio/gstaudioaggregator.c | 1 - 11 | 1 file changed, 1 deletion(-) 12 | 13 | diff --git a/subprojects/gst-plugins-base/gst-libs/gst/audio/gstaudioaggregator.c b/subprojects/gst-plugins-base/gst-libs/gst/audio/gstaudioaggregator.c 14 | index d967a3295d..811e5e8a43 100644 15 | --- a/subprojects/gst-plugins-base/gst-libs/gst/audio/gstaudioaggregator.c 16 | +++ b/subprojects/gst-plugins-base/gst-libs/gst/audio/gstaudioaggregator.c 17 | @@ -2188,7 +2188,6 @@ gst_audio_aggregator_peek_next_sample (GstAggregator * agg, 18 | 19 | sample = gst_sample_new (pad->priv->buffer, caps, &aggpad->segment, info); 20 | gst_caps_unref (caps); 21 | - gst_structure_free (info); 22 | } 23 | 24 | return sample; 25 | -- 26 | 2.43.0 27 | 28 | -------------------------------------------------------------------------------- /docker/build-gstreamer/patch/0001-Reduce-logs-verbosity-in-webrtcbin-when-a-FEC-decode.patch: -------------------------------------------------------------------------------- 1 | From 5a70b3c8d4f4c1213064d372ee48069bf957c80a Mon Sep 17 00:00:00 2001 2 | From: =?UTF-8?q?Lo=C3=AFc=20Le=20Page?= 3 | Date: Mon, 2 Dec 2024 12:25:42 +0100 4 | Subject: [PATCH] Reduce logs verbosity in webrtcbin when a FEC decoder cannot 5 | be found 6 | 7 | --- 8 | subprojects/gst-plugins-bad/ext/webrtc/gstwebrtcbin.c | 4 ++-- 9 | 1 file changed, 2 insertions(+), 2 deletions(-) 10 | 11 | diff --git a/subprojects/gst-plugins-bad/ext/webrtc/gstwebrtcbin.c b/subprojects/gst-plugins-bad/ext/webrtc/gstwebrtcbin.c 12 | index 0e315ac466..3acae76b6f 100644 13 | --- a/subprojects/gst-plugins-bad/ext/webrtc/gstwebrtcbin.c 14 | +++ b/subprojects/gst-plugins-bad/ext/webrtc/gstwebrtcbin.c 15 | @@ -5013,7 +5013,7 @@ try_match_transceiver_with_fec_decoder (GstWebRTCBin * webrtc, 16 | if (original_pt == item->pt && item->media_idx != -1 17 | && item->media_idx == trans->parent.mline) { 18 | if (trans->ulpfecdec) { 19 | - GST_FIXME_OBJECT (trans, "cannot"); 20 | + // GST_FIXME_OBJECT (trans, "cannot"); 21 | gst_clear_object (&trans->ulpfecdec); 22 | } 23 | trans->ulpfecdec = gst_object_ref (fecdec); 24 | @@ -5023,7 +5023,7 @@ try_match_transceiver_with_fec_decoder (GstWebRTCBin * webrtc, 25 | } 26 | 27 | if (!found_transceiver) { 28 | - GST_WARNING_OBJECT (trans, "failed to match fec decoder with " 29 | + GST_INFO_OBJECT (trans, "failed to match fec decoder with " 30 | "transceiver"); 31 | } 32 | } 33 | -- 34 | 2.43.0 35 | 36 | -------------------------------------------------------------------------------- /docker/build-gstreamer/patch/0001-cefsrc-unmultiply-alpha.patch: -------------------------------------------------------------------------------- 1 | From 0c69254faaa380f14588e50651a82a9e67d5731b Mon Sep 17 00:00:00 2001 2 | From: =?UTF-8?q?Lo=C3=AFc=20Le=20Page?= 3 | Date: Mon, 14 Oct 2024 18:47:48 +0200 4 | Subject: [PATCH] cefsrc: unmultiply alpha 5 | 6 | --- 7 | gstcefsrc.cc | 19 +++++++++++++++++++ 8 | 1 file changed, 19 insertions(+) 9 | 10 | diff --git a/gstcefsrc.cc b/gstcefsrc.cc 11 | index b06897e..2c4b1da 100644 12 | --- a/gstcefsrc.cc 13 | +++ b/gstcefsrc.cc 14 | @@ -116,6 +116,25 @@ class RenderHandler : public CefRenderHandler 15 | new_buffer = gst_buffer_new_allocate (NULL, element->vinfo.width * element->vinfo.height * 4, NULL); 16 | gst_buffer_fill (new_buffer, 0, buffer, w * h * 4); 17 | 18 | + // CEF output is alpha pre-multipled, 19 | + // we need to unmultiply it to be compatible with the compositor 20 | + GstMapInfo info = {}; 21 | + if (gst_buffer_map (new_buffer, &info, GST_MAP_READWRITE)) { 22 | + guint8 *data = info.data; 23 | + for (gint i = 0; i < element->vinfo.width * element->vinfo.height; ++i) 24 | + { 25 | + float alpha = data[3]; 26 | + if (alpha != 0 && alpha != 255) { 27 | + alpha = 255.f / alpha; 28 | + data[0] *= alpha; 29 | + data[1] *= alpha; 30 | + data[2] *= alpha; 31 | + } 32 | + data += 4; 33 | + } 34 | + gst_buffer_unmap (new_buffer, &info); 35 | + } 36 | + 37 | GST_OBJECT_LOCK (element); 38 | gst_buffer_replace (&(element->current_buffer), new_buffer); 39 | gst_buffer_unref (new_buffer); 40 | -- 41 | 2.43.0 42 | 43 | -------------------------------------------------------------------------------- /docker/build-gstreamer/patch/0001-baseparse-always-update-the-input-pts-if-available-from.patch: -------------------------------------------------------------------------------- 1 | From e3ef323fda57a4b4e604820f8e183fea97892acd Mon Sep 17 00:00:00 2001 2 | From: Matthew Waters 3 | Date: Tue, 29 Sep 2020 14:51:54 +1000 4 | Subject: [PATCH] baseparse: always update the input pts if available from 5 | upstream 6 | 7 | We were not which could lead to output buffers having a pts of NONE even 8 | if all the input ts were entirely coherent. This meant that certain 9 | muxers would reject the buffer without a pts and fail. 10 | --- 11 | subprojects/gstreamer/libs/gst/base/gstbaseparse.c | 5 +++-- 12 | 1 file changed, 3 insertions(+), 2 deletions(-) 13 | 14 | diff --git a/subprojects/gstreamer/libs/gst/base/gstbaseparse.c b/subprojects/gstreamer/libs/gst/base/gstbaseparse.c 15 | index 660439bacfa..04d400604ca 100644 16 | --- a/subprojects/gstreamer/libs/gst/base/gstbaseparse.c 17 | +++ b/subprojects/gstreamer/libs/gst/base/gstbaseparse.c 18 | @@ -3281,9 +3281,10 @@ gst_base_parse_chain (GstPad * pad, GstObject * parent, GstBuffer * buffer) 19 | * but interpolate in between */ 20 | pts = gst_adapter_prev_pts (parse->priv->adapter, NULL); 21 | dts = gst_adapter_prev_dts (parse->priv->adapter, NULL); 22 | - if (GST_CLOCK_TIME_IS_VALID (pts) && (parse->priv->prev_pts != pts)) { 23 | + if (GST_CLOCK_TIME_IS_VALID (pts)) { 24 | + if (parse->priv->prev_pts != pts) 25 | + updated_prev_pts = TRUE; 26 | parse->priv->prev_pts = parse->priv->next_pts = pts; 27 | - updated_prev_pts = TRUE; 28 | } 29 | 30 | if (GST_CLOCK_TIME_IS_VALID (dts) && (parse->priv->prev_dts != dts)) { 31 | -- 32 | GitLab 33 | 34 | -------------------------------------------------------------------------------- /docker/build-gstreamer/patch/0001-pad-Check-data-NULL-ness-when-probes-are-stopped-fixed-in-1.24.8.patch: -------------------------------------------------------------------------------- 1 | From 78635ce0cd981f4086d254901547ae97caa5e256 Mon Sep 17 00:00:00 2001 2 | From: Arun Raghavan 3 | Date: Tue, 10 Sep 2024 16:03:05 -0400 4 | Subject: [PATCH] pad: Check data NULL-ness when probes are stopped 5 | 6 | We were correctly handling this for buffers, but not events and queries. 7 | 8 | Part-of: 9 | --- 10 | subprojects/gstreamer/gst/gstpad.c | 6 +++--- 11 | 1 file changed, 3 insertions(+), 3 deletions(-) 12 | 13 | diff --git a/subprojects/gstreamer/gst/gstpad.c b/subprojects/gstreamer/gst/gstpad.c 14 | index 276c733c97..355ab9500a 100644 15 | --- a/subprojects/gstreamer/gst/gstpad.c 16 | +++ b/subprojects/gstreamer/gst/gstpad.c 17 | @@ -4562,7 +4562,7 @@ probe_handled: 18 | probe_stopped: 19 | { 20 | /* We unref the buffer, except if the probe handled it (CUSTOM_SUCCESS_1) */ 21 | - if (!handled) 22 | + if (data && !handled) 23 | gst_mini_object_unref (GST_MINI_OBJECT_CAST (data)); 24 | 25 | switch (ret) { 26 | @@ -5630,7 +5630,7 @@ inactive: 27 | probe_stopped: 28 | { 29 | GST_OBJECT_FLAG_SET (pad, GST_PAD_FLAG_PENDING_EVENTS); 30 | - if (ret != GST_FLOW_CUSTOM_SUCCESS_1) 31 | + if (event && ret != GST_FLOW_CUSTOM_SUCCESS_1) 32 | gst_event_unref (event); 33 | 34 | switch (ret) { 35 | @@ -6039,7 +6039,7 @@ probe_stopped: 36 | if (need_unlock) 37 | GST_PAD_STREAM_UNLOCK (pad); 38 | /* Only unref if unhandled */ 39 | - if (ret != GST_FLOW_CUSTOM_SUCCESS_1) 40 | + if (event && ret != GST_FLOW_CUSTOM_SUCCESS_1) 41 | gst_event_unref (event); 42 | 43 | switch (ret) { 44 | -- 45 | 2.43.0 46 | 47 | -------------------------------------------------------------------------------- /docker/build-gstreamer/patch/0001-libnice.wrap-fix-incomplete-TCP-ICE-candidate-crash.patch: -------------------------------------------------------------------------------- 1 | From 99c642e9de04964ac6ae3c9aa50b28b134ae8496 Mon Sep 17 00:00:00 2001 2 | From: =?UTF-8?q?Lo=C3=AFc=20Le=20Page?= 3 | Date: Mon, 20 Oct 2025 14:17:38 +0200 4 | Subject: [PATCH] libnice.wrap: fix incomplete TCP ICE candidate crash 5 | 6 | --- 7 | subprojects/libnice.wrap | 1 + 8 | ...sing-an-incomplete-TCP-ICE-candidate.patch | 30 +++++++++++++++++++ 9 | 2 files changed, 31 insertions(+) 10 | create mode 100644 subprojects/packagefiles/libnice-0.1.21/0001-agent-fix-parsing-an-incomplete-TCP-ICE-candidate.patch 11 | 12 | diff --git a/subprojects/libnice.wrap b/subprojects/libnice.wrap 13 | index eeed32daa2..2dc672b27b 100644 14 | --- a/subprojects/libnice.wrap 15 | +++ b/subprojects/libnice.wrap 16 | @@ -2,3 +2,4 @@ 17 | directory=libnice 18 | url=https://gitlab.freedesktop.org/libnice/libnice.git 19 | revision=0.1.21 20 | +diff_files = libnice-0.1.21/0001-agent-fix-parsing-an-incomplete-TCP-ICE-candidate.patch 21 | diff --git a/subprojects/packagefiles/libnice-0.1.21/0001-agent-fix-parsing-an-incomplete-TCP-ICE-candidate.patch b/subprojects/packagefiles/libnice-0.1.21/0001-agent-fix-parsing-an-incomplete-TCP-ICE-candidate.patch 22 | new file mode 100644 23 | index 0000000000..8dec6b9948 24 | --- /dev/null 25 | +++ b/subprojects/packagefiles/libnice-0.1.21/0001-agent-fix-parsing-an-incomplete-TCP-ICE-candidate.patch 26 | @@ -0,0 +1,30 @@ 27 | +From d5f6db4100fedb01f62909ee342a8ccaa1274431 Mon Sep 17 00:00:00 2001 28 | +From: =?UTF-8?q?Lo=C3=AFc=20Le=20Page?= 29 | +Date: Mon, 20 Oct 2025 13:31:54 +0200 30 | +Subject: [PATCH] agent: fix parsing an incomplete TCP ICE candidate 31 | + 32 | +If a TCP ICE candidate mline in a SDP doesn't specify the `tcptype` 33 | +field, it was crashing the agent because of the non-null assert in 34 | +`g_ascii_strcasecmp`. The patch is just ignoring an incomplete TCP 35 | +candidate just like the other malformed candidates. 36 | +--- 37 | + agent/agent.c | 3 +++ 38 | + 1 file changed, 3 insertions(+) 39 | + 40 | +diff --git a/agent/agent.c b/agent/agent.c 41 | +index 0d1f1814..7d22e22b 100644 42 | +--- a/agent/agent.c 43 | ++++ b/agent/agent.c 44 | +@@ -7613,6 +7613,9 @@ nice_agent_parse_remote_candidate_sdp (NiceAgent *agent, guint stream_id, 45 | + else if (g_ascii_strcasecmp (transport, "TCP-PASS") == 0) 46 | + ctransport = NICE_CANDIDATE_TRANSPORT_TCP_PASSIVE; 47 | + else if (g_ascii_strcasecmp (transport, "TCP") == 0) { 48 | ++ if (tcptype == NULL) 49 | ++ goto done; 50 | ++ 51 | + if (g_ascii_strcasecmp (tcptype, "so") == 0) 52 | + ctransport = NICE_CANDIDATE_TRANSPORT_TCP_SO; 53 | + else if (g_ascii_strcasecmp (tcptype, "active") == 0) 54 | +-- 55 | +2.43.0 56 | + 57 | -- 58 | 2.43.0 59 | 60 | -------------------------------------------------------------------------------- /docker/build-gstreamer/compile: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | source $HOME/.cargo/env 5 | 6 | pushd gstreamer 7 | # TODO: Hack: `-D gupnp=disabled` is for libnice, because libgupnp-igd causes memory leaks 8 | # msdk=enabled is for gst-plugings-bad to include msdk elements 9 | # with_x11=no is for gstreamer-vaapi to exclude X11-related functionality (that would otherwise require extra dependencies) 10 | MESON_OPTIONS="-Dvaapi=enabled -Dgpl=enabled -Dgst-examples=disabled -Dexamples=disabled -Dtests=disabled -Ddoc=disabled -Dqt5=disabled -Dpython=disabled -Dges=disabled -Ddevtools=disabled -Dintrospection=disabled -Dlibnice:gupnp=disabled -Dgstreamer-vaapi:x11=disabled -Dgstreamer-vaapi:encoders=enabled" 11 | 12 | ARCH=$(dpkg --print-architecture) 13 | if [[ $ARCH == "amd64" ]]; then 14 | MESON_OPTIONS="$MESON_OPTIONS -Dgst-plugins-bad:msdk=enabled " 15 | fi 16 | 17 | git apply /compile-patch/0001-libnice.wrap-fix-incomplete-TCP-ICE-candidate-crash.patch 18 | 19 | if [[ $DEBUG == 'true' ]]; then 20 | if [[ $OPTIMIZATIONS == 'true' ]]; then 21 | meson build -D prefix=/usr $MESON_OPTIONS -D buildtype=debugoptimized 22 | else 23 | meson build -D prefix=/usr $MESON_OPTIONS -D buildtype=debug 24 | fi 25 | else 26 | meson build -D prefix=/usr $MESON_OPTIONS -D buildtype=release -D b_lto=true 27 | fi 28 | 29 | git apply /compile-patch/0001-baseparse-always-update-the-input-pts-if-available-from.patch 30 | git apply /compile-patch/0001-compositor-add-rounded-corners-property-for-sink-pad.patch 31 | git apply /compile-patch/0001-compositor-add-box-shadow-property-for-sink-pads.patch 32 | git apply /compile-patch/0001-pad-Check-data-NULL-ness-when-probes-are-stopped-fixed-in-1.24.8.patch 33 | git apply /compile-patch/0001-Reduce-logs-verbosity-in-webrtcbin-when-a-FEC-decode.patch 34 | git apply /compile-patch/0001-glvideomixer-update-API-to-be-compatible-with-versio.patch 35 | git apply /compile-patch/0001-GstAudioAggregator-fix-structure-unref.patch 36 | git apply /compile-patch/0001-gsth264parser-fix-uint32-to-int32-truncation.patch 37 | 38 | # This is needed for other plugins to be built properly 39 | ninja -C build install 40 | # This is where we'll grab build artifacts from 41 | DESTDIR=/compiled-binaries ninja -C build install 42 | popd 43 | 44 | # Build the GStreamer CEF plugin 45 | pushd gstcefsrc 46 | source /.cef-config.sh 47 | GST_CEF_CMAKE_OPTS="-DCMAKE_INSTALL_PREFIX=/usr/cef -GNinja -DCEF_BUILDS_HOMEPAGE_URL=$CEF_DOWNLOAD_URL -DCEF_VERSION=$CEF_VERSION" 48 | 49 | # TODO: Build CEF Debug artifacts as well. 50 | GST_CEF_BUILD_TYPE=Release 51 | 52 | cmake -DCMAKE_BUILD_TYPE=$GST_CEF_BUILD_TYPE $GST_CEF_CMAKE_OPTS -B build -S . 53 | 54 | git apply /compile-patch/0001-cefsrc-unmultiply-alpha.patch 55 | 56 | DESTDIR=/compiled-binaries ninja -C build install 57 | rm -fr build third_party 58 | popd 59 | 60 | chown root:root /compiled-binaries/usr/cef/chrome-sandbox 61 | chmod 4755 /compiled-binaries/usr/cef/chrome-sandbox 62 | 63 | gst-inspect-1.0 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # docker-gstreamer 2 | [![Docker Pulls](https://img.shields.io/docker/pulls/restreamio/gstreamer)](https://hub.docker.com/r/restreamio/gstreamer) 3 | 4 | Ubuntu 22.04-based container images with upstream GStreamer and plugins pre-installed 5 | 6 | Following components are present: 7 | * GStreamer 8 | * gst-plugins-base 9 | * gst-plugins-good 10 | * gst-plugins-bad (with `msdk`) 11 | * gst-plugins-ugly 12 | * gst-libav 13 | * gstreamer-vaapi 14 | * libnice (newer version from git) 15 | * WPE WebKit (newer version from upstream release) 16 | * gstcefsrc 17 | 18 | GStreamer and components are tracking upstream master branches (with minor fixes on top) and are usually updated a few times a month. 19 | There are also builds of stable upstream releases available as well. 20 | 21 | Base OS is Ubuntu 22.04. 22 | 23 | NOTE: 24 | * 1.18.2.0 images and older were based on Ubuntu 20.04 25 | * 2020-12-30T23-16-11Z images and older were based on Ubuntu 20.04 26 | * 1.18.4.0 images and older were based on Ubuntu 20.10 27 | * 2021-06-08T14-12-58Z images and older were based on Ubuntu 20.10 28 | 29 | ## SCCache support 30 | 31 | The sysroot includes WPEWebKit, which is a huge project and requires a good 32 | build machine. However in case you have access to a 33 | [SCCache](https://github.com/mozilla/sccache) scheduler online, you can use it 34 | to build WPEWebKit: 35 | 36 | ```bash 37 | export SCCACHE_SCHEDULER=https://sccache.corp.com 38 | export SCCACHE_AUTH_TOKEN=s3cr3t 39 | export WEBKIT_USE_SCCACHE=1 40 | ./build-latest.sh 41 | ``` 42 | 43 | The configuration template stored in `sccache.toml` is filled with the scheduler 44 | address and authentication token supplied through the corresponding environment 45 | variables. 46 | 47 | # Builds on Docker Hub 48 | Builds use Restream-specific patches by default, but there are also vanilla upstream builds available. 49 | 50 | There are 4 kinds of images pushed to Docker Hub: 51 | * restreamio/gstreamer:x86_64-latest-dev-with-source - includes unoptimized build with debug symbols and even source code it was built with 52 | * restreamio/gstreamer:x86_64-latest-dev - same as above, but without source code for development purposes 53 | * restreamio/gstreamer:x86_64-latest-prod - optimized (`-O3` and `LTO`) build without debug symbols for production purposes 54 | * restreamio/gstreamer:x86_64-latest-prod-dbg - optimized (`-O2` only) build with debug symbols included for production purposes with better debugging experience 55 | 56 | For Linux/ARM64 builds, replace `x86_64` with `aarch64` in the Docker tags. For 57 | further convenience, multi-arch images aggregating both builds are available as 58 | well, just remove the `$ARCH-` prefix from docker labels. 59 | 60 | There are also above tags prefixed with build date for stable reference. 61 | 62 | Finally, starting with `1.18.1` there are also vanilla builds using stable upstream releases with no patches applied, whose tags you can also find on Docker Hub. 63 | Stable released have 2 tags: 64 | * regular like `1.18.1` that is a latest build of that upstream release 65 | * stable reference with one more number after regular `major.minor.patch` that starts with 0 and is incremented if there are multiple builds for the same upstream stable version (like `1.18.1.0`) 66 | 67 | ## Contribution 68 | Feel free to create issues and send pull requests, they are highly appreciated! 69 | 70 | ## License 71 | Zero-Clause BSD 72 | 73 | https://opensource.org/licenses/0BSD 74 | 75 | https://tldrlegal.com/license/bsd-0-clause-license 76 | -------------------------------------------------------------------------------- /Dockerfile-prod-base.in: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 2 | 3 | ARG DEBIAN_FRONTEND=noninteractive 4 | 5 | RUN \ 6 | apt-get update && \ 7 | apt-get dist-upgrade -y && \ 8 | apt-get install -y --no-install-recommends \ 9 | ca-certificates \ 10 | libatk1.0-0 \ 11 | libatk-bridge2.0-0 \ 12 | libatk-adaptor \ 13 | libatspi2.0-0 \ 14 | libunwind8 \ 15 | libdw1 \ 16 | libgmp10 \ 17 | libgsl27 \ 18 | libglib2.0-0 \ 19 | libcap2 \ 20 | libcups2 \ 21 | liborc-0.4-0 \ 22 | iso-codes \ 23 | libgl1 \ 24 | libgles1 \ 25 | libgles2 \ 26 | libgudev-1.0-0 \ 27 | libgbm1 \ 28 | libgraphene-1.0-dev \ 29 | libpng16-16 \ 30 | libjpeg8 \ 31 | libogg0 \ 32 | libopus0 \ 33 | libpango-1.0-0 \ 34 | libvisual-0.4-0 \ 35 | libtheora0 \ 36 | libvorbis0a \ 37 | libxkbcommon0 \ 38 | libxcomposite1 \ 39 | libxdamage1 \ 40 | libwayland-client0 \ 41 | libwayland-cursor0 \ 42 | libwayland-egl1 \ 43 | libwayland-server0 \ 44 | libharfbuzz-icu0 \ 45 | libegl1 \ 46 | libepoxy0 \ 47 | libgcrypt20 \ 48 | libwebp7 \ 49 | libwebpdemux2 \ 50 | libwebpmux3 \ 51 | libopenjp2-7 \ 52 | libwoff1 \ 53 | libxslt1.1 \ 54 | bubblewrap \ 55 | libseccomp2 \ 56 | xdg-dbus-proxy \ 57 | libsoup2.4-1 \ 58 | libvulkan1 \ 59 | libass9 \ 60 | libchromaprint1 \ 61 | libcurl3-gnutls \ 62 | libaom3 \ 63 | libbz2-1.0 \ 64 | liblcms2-2 \ 65 | libbs2b0 \ 66 | libdca0 \ 67 | libfaac0 \ 68 | libfaad2 \ 69 | libflite1 \ 70 | libssl3 \ 71 | ladspa-sdk \ 72 | libfdk-aac2 \ 73 | libgsm1 \ 74 | libkate1 \ 75 | libgme0 \ 76 | libde265-0 \ 77 | liblilv-0-0 \ 78 | libmodplug1 \ 79 | mjpegtools \ 80 | libmjpegutils-2.1-0 \ 81 | libmpcdec6 \ 82 | libdvdnav4 \ 83 | libdvdread8 \ 84 | librsvg2-2 \ 85 | librtmp1 \ 86 | libsbc1 \ 87 | libsndfile1 \ 88 | libsoundtouch1 \ 89 | libspandsp2 \ 90 | libsrt1.4-openssl \ 91 | libsrtp2-1 \ 92 | libvo-aacenc0 \ 93 | libvo-amrwbenc0 \ 94 | libwebrtc-audio-processing1 \ 95 | libofa0 \ 96 | libzvbi0 \ 97 | libopenexr25 \ 98 | libwildmidi2 \ 99 | libx265-199 \ 100 | libzbar0 \ 101 | wayland-protocols \ 102 | libaa1 \ 103 | libmp3lame0 \ 104 | libcaca0 \ 105 | libdv4 \ 106 | libmpg123-0 \ 107 | libvpx7 \ 108 | libshout3 \ 109 | libspeex1 \ 110 | libtag1v5 \ 111 | libtwolame0 \ 112 | libwavpack1 \ 113 | liba52-0.7.4 \ 114 | libx264-163 \ 115 | libopencore-amrnb0 \ 116 | libopencore-amrwb0 \ 117 | libmpeg2-4 \ 118 | libavcodec58 \ 119 | libavfilter7 \ 120 | libavformat58 \ 121 | libavutil56 \ 122 | libva2 \ 123 | libva-wayland2 \ 124 | xvfb \ 125 | libxrandr-dev \ 126 | glibc-tools 127 | 128 | COPY docker/install-arch-specific-dependencies.sh /.install-deps.sh 129 | RUN /.install-deps.sh 130 | 131 | # Add 'render' group to allow access to /dev/dri/renderD128 132 | # To have access to the Intel GPU through libva for hardware acceleration, 133 | # running user must be in the 'video' and 'render' groups. 134 | RUN \ 135 | rm -f /.install-deps.sh && \ 136 | apt-get clean && \ 137 | rm -rf /var/lib/apt/lists/* && \ 138 | groupadd -f -g 110 render 139 | -------------------------------------------------------------------------------- /docker/build-gstreamer/install-dependencies: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | export DEBIAN_FRONTEND=noninteractive 5 | export CLANG_VERSION=12 6 | 7 | sed -i 's/# deb-src/deb-src/g' /etc/apt/sources.list 8 | apt-get update 9 | apt-get dist-upgrade -y 10 | apt-get install -y --no-install-recommends \ 11 | ca-certificates \ 12 | git \ 13 | wget \ 14 | python3 \ 15 | python3-pip \ 16 | python3-dev \ 17 | gcc \ 18 | clang-$CLANG_VERSION \ 19 | cmake \ 20 | libatk1.0-dev \ 21 | libatk-bridge2.0-dev \ 22 | libatspi2.0-dev \ 23 | libcups2-dev \ 24 | libxcomposite-dev \ 25 | libxdamage-dev \ 26 | libunwind-dev \ 27 | libdw-dev \ 28 | libgmp-dev \ 29 | libgraphene-1.0-dev \ 30 | libgsl-dev \ 31 | libglib2.0-dev \ 32 | flex \ 33 | bison \ 34 | libcap-dev \ 35 | libgirepository1.0-dev \ 36 | gettext \ 37 | liborc-0.4-dev \ 38 | iso-codes \ 39 | libgl-dev \ 40 | libgles-dev \ 41 | libdrm-dev \ 42 | libgudev-1.0-dev \ 43 | libgbm-dev \ 44 | libpng-dev \ 45 | libjpeg-dev \ 46 | libogg-dev \ 47 | libopus-dev \ 48 | libpango1.0-dev \ 49 | libvisual-0.4-dev \ 50 | libtheora-dev \ 51 | libvorbis-dev \ 52 | libxkbcommon-dev \ 53 | libwayland-dev \ 54 | libepoxy-dev \ 55 | ruby \ 56 | libgcrypt20-dev \ 57 | libwebp-dev \ 58 | libopenjp2-7-dev \ 59 | libwoff-dev \ 60 | libxslt1-dev \ 61 | bubblewrap \ 62 | libseccomp-dev \ 63 | xdg-dbus-proxy \ 64 | gperf \ 65 | libsoup2.4-dev \ 66 | libvulkan-dev \ 67 | libass-dev \ 68 | libchromaprint-dev \ 69 | libcurl4-gnutls-dev \ 70 | libaom-dev \ 71 | libbz2-dev \ 72 | liblcms2-dev \ 73 | libbs2b-dev \ 74 | libdca-dev \ 75 | libfaac-dev \ 76 | libfaad-dev \ 77 | flite1-dev \ 78 | libssl-dev \ 79 | libfdk-aac-dev \ 80 | libfluidsynth-dev \ 81 | libgsm1-dev \ 82 | libkate-dev \ 83 | libgme-dev \ 84 | libde265-dev \ 85 | liblilv-dev \ 86 | libmodplug-dev \ 87 | libmjpegtools-dev \ 88 | libmpcdec-dev \ 89 | libdvdnav-dev \ 90 | libdvdread-dev \ 91 | librsvg2-dev \ 92 | librtmp-dev \ 93 | libsbc-dev \ 94 | libsndfile1-dev \ 95 | libsoundtouch-dev \ 96 | libspandsp-dev \ 97 | libsrt-gnutls-dev \ 98 | libsrtp2-dev \ 99 | libvo-aacenc-dev \ 100 | libvo-amrwbenc-dev \ 101 | libwebrtc-audio-processing-dev \ 102 | libofa0-dev \ 103 | libzvbi-dev \ 104 | libopenexr-dev \ 105 | libwildmidi-dev \ 106 | libx265-dev \ 107 | libzbar-dev \ 108 | wayland-protocols \ 109 | libaa1-dev \ 110 | libmp3lame-dev \ 111 | libcaca-dev \ 112 | libdv4-dev \ 113 | libmpg123-dev \ 114 | libvpx-dev \ 115 | libshout3-dev \ 116 | libspeex-dev \ 117 | libtag1-dev \ 118 | libtwolame-dev \ 119 | libwavpack-dev \ 120 | liba52-0.7.4-dev \ 121 | libx264-dev \ 122 | libopencore-amrnb-dev \ 123 | libopencore-amrwb-dev \ 124 | libmpeg2-4-dev \ 125 | libavcodec-dev \ 126 | libavfilter-dev \ 127 | libavformat-dev \ 128 | libavutil-dev \ 129 | libva-dev \ 130 | libudev-dev \ 131 | glibc-tools \ 132 | libqrencode-dev \ 133 | libjson-glib-dev 134 | pip3 install meson ninja hotdoc 135 | 136 | ARCH=$(dpkg --print-architecture) 137 | if [[ $ARCH == "amd64" ]]; then 138 | apt-get install -y --no-install-recommends \ 139 | libmfx-dev \ 140 | intel-media-va-driver-non-free 141 | fi 142 | 143 | apt-get clean 144 | rm -rf /var/lib/apt/lists/* 145 | 146 | # SCCache can be used to speed-up WPE compilation. You need cloud builders setup 147 | # though. SCCache binaries are available for arm64 but don't support toolchain 148 | # packaging, so skip this when building on arm64. 149 | if [[ $ARCH == "amd64" ]]; then 150 | MACHINE=$(uname -m) 151 | export SCCACHE_VERSION="0.2.14" 152 | wget https://github.com/mozilla/sccache/releases/download/$SCCACHE_VERSION/sccache-$SCCACHE_VERSION-$MACHINE-unknown-linux-musl.tar.gz 153 | tar xf sccache-$SCCACHE_VERSION-$MACHINE-unknown-linux-musl.tar.gz 154 | mv sccache-$SCCACHE_VERSION-$MACHINE-unknown-linux-musl/sccache /usr/bin 155 | rm -rf sccache-$SCCACHE_VERSION-$MACHINE-unknown-linux-musl 156 | 157 | ln -s /usr/bin/clang{-$CLANG_VERSION,} 158 | ln -s /usr/bin/clang++{-$CLANG_VERSION,} 159 | 160 | sccache --package-toolchain /usr/bin/clang-$CLANG_VERSION /toolchain-clang-$CLANG_VERSION.tar.gz 161 | fi 162 | 163 | wget -O- https://sh.rustup.rs | sh -s -- -y --default-toolchain stable 164 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Docker Build Workflow 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | pull_request: 9 | branches: 10 | - master 11 | 12 | concurrency: 13 | group: ${{ github.event.repository.name }}-ci-${{ github.event.pull_request.number }} 14 | cancel-in-progress: true 15 | 16 | env: 17 | DOCKER_TAG_FILE_PREFIX: docker-tag-basename 18 | 19 | jobs: 20 | init: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Checkout repository 24 | uses: actions/checkout@v4 25 | 26 | - name: Set environment variables 27 | run: echo "DATE=$(date -u +"%Y-%m-%dT%H-%M-%SZ")" > vars.env 28 | 29 | - name: Upload env vars artifact 30 | uses: actions/upload-artifact@v4 31 | with: 32 | name: env-vars 33 | path: ./vars.env 34 | 35 | amd64-build: 36 | needs: init 37 | runs-on: ubuntu-amd64-8core 38 | steps: 39 | - name: Checkout repository 40 | uses: actions/checkout@v4 41 | 42 | - name: Download env vars artifact 43 | uses: actions/download-artifact@v4 44 | with: 45 | name: env-vars 46 | path: ./ 47 | 48 | - name: Initialize environment variables 49 | run: | 50 | cat ./vars.env >> $GITHUB_ENV 51 | 52 | - name: Login to DockerHub 53 | uses: docker/login-action@v3 54 | with: 55 | username: ${{ secrets.DOCKER_LOGIN }} 56 | password: ${{ secrets.DOCKER_PWD }} 57 | 58 | - name: Build AMD64 image 59 | run: ./build-latest.sh 60 | 61 | - name: Push AMD64 image 62 | run: ./push-latest.sh 63 | 64 | - name: Find and upload AMD64 docker tag 65 | run: | 66 | TAG_FILE=$(ls ./${DOCKER_TAG_FILE_PREFIX}-*.txt | head -n 1) 67 | echo "TAG_FILE=$TAG_FILE" >> $GITHUB_ENV 68 | 69 | - name: Upload AMD64 docker tag 70 | uses: actions/upload-artifact@v4 71 | with: 72 | name: amd64-image-tag 73 | path: ${{ env.TAG_FILE }} 74 | 75 | arm64-build: 76 | needs: init 77 | runs-on: ubuntu-arm64-8core 78 | steps: 79 | - name: Checkout repository 80 | uses: actions/checkout@v4 81 | 82 | - name: Download env vars artifact 83 | uses: actions/download-artifact@v4 84 | with: 85 | name: env-vars 86 | path: ./ 87 | 88 | - name: Initialize environment variables 89 | run: | 90 | cat ./vars.env >> $GITHUB_ENV 91 | 92 | - name: Login to DockerHub 93 | uses: docker/login-action@v3 94 | with: 95 | username: ${{ secrets.DOCKER_LOGIN }} 96 | password: ${{ secrets.DOCKER_PWD }} 97 | 98 | - name: Build ARM64 image 99 | run: ./build-latest.sh 100 | 101 | - name: Push ARM64 image 102 | run: ./push-latest.sh 103 | 104 | - name: Find and upload AMD64 docker tag 105 | run: | 106 | TAG_FILE=$(ls ./${DOCKER_TAG_FILE_PREFIX}-*.txt | head -n 1) 107 | echo "TAG_FILE=$TAG_FILE" >> $GITHUB_ENV 108 | 109 | - name: Upload ARM64 docker tag 110 | uses: actions/upload-artifact@v4 111 | with: 112 | name: arm64-image-tag 113 | path: ${{ env.TAG_FILE }} 114 | 115 | aggregate-images: 116 | needs: 117 | - amd64-build 118 | - arm64-build 119 | runs-on: ubuntu-amd64-8core 120 | steps: 121 | - name: Checkout repository 122 | uses: actions/checkout@v4 123 | 124 | - name: Download env vars artifact 125 | uses: actions/download-artifact@v4 126 | with: 127 | name: env-vars 128 | path: ./ 129 | 130 | - name: Download AMD64 docker tag artifact 131 | uses: actions/download-artifact@v4 132 | with: 133 | name: amd64-image-tag 134 | path: ./ 135 | 136 | - name: Download ARM64 docker tag artifact 137 | uses: actions/download-artifact@v4 138 | with: 139 | name: arm64-image-tag 140 | path: ./ 141 | 142 | - name: Initialize environment variables 143 | run: | 144 | cat ./vars.env >> $GITHUB_ENV 145 | 146 | - name: Verify AMD64 and ARM64 tags 147 | run: | 148 | cat ./docker-tag-basename-x86_64.txt 149 | cat ./docker-tag-basename-aarch64.txt 150 | 151 | - name: Login to DockerHub 152 | uses: docker/login-action@v3 153 | with: 154 | username: ${{ secrets.DOCKER_LOGIN }} 155 | password: ${{ secrets.DOCKER_PWD }} 156 | 157 | - name: Push multi-architecture Docker images 158 | run: ./push-multi-arch-latest.sh 159 | -------------------------------------------------------------------------------- /docker/build-gstreamer/patch/0001-gsth264parser-fix-uint32-to-int32-truncation.patch: -------------------------------------------------------------------------------- 1 | From 282a528c7234d1cb89422db0fcce56385b0507b5 Mon Sep 17 00:00:00 2001 2 | From: =?UTF-8?q?Lo=C3=AFc=20Le=20Page?= 3 | Date: Fri, 31 Oct 2025 13:27:47 +0100 4 | Subject: [PATCH] gsth264parser: fix uint32 to int32 truncation 5 | 6 | The H264 parser is using int32 to compute the framerate numerator and 7 | denominator while the SPS block holding the original values used for 8 | this computation are stored on uint32. On corner cases it may lead to a 9 | truncation of the final values leading to an invalid framerate. 10 | 11 | This patch is shifting right the numerator and denominator until both 12 | values may be stored on int32. It may introduce a small computation 13 | error with odd values but negligeable taking into account the huge 14 | initial values > max_int32. 15 | --- 16 | .../gst-libs/gst/codecparsers/gsth264parser.c | 9 +++- 17 | .../tests/check/libs/h264parser.c | 48 +++++++++++++++++++ 18 | 2 files changed, 55 insertions(+), 2 deletions(-) 19 | 20 | diff --git a/subprojects/gst-plugins-bad/gst-libs/gst/codecparsers/gsth264parser.c b/subprojects/gst-plugins-bad/gst-libs/gst/codecparsers/gsth264parser.c 21 | index ea3c6fb995..5095bcea73 100644 22 | --- a/subprojects/gst-plugins-bad/gst-libs/gst/codecparsers/gsth264parser.c 23 | +++ b/subprojects/gst-plugins-bad/gst-libs/gst/codecparsers/gsth264parser.c 24 | @@ -2946,8 +2946,8 @@ void 25 | gst_h264_video_calculate_framerate (const GstH264SPS * sps, 26 | guint field_pic_flag, guint pic_struct, gint * fps_num, gint * fps_den) 27 | { 28 | - gint num = 0; 29 | - gint den = 1; 30 | + gint64 num = 0; 31 | + gint64 den = 1; 32 | 33 | /* To calculate framerate, we use this formula: 34 | * time_scale 1 1 35 | @@ -2998,6 +2998,11 @@ gst_h264_video_calculate_framerate (const GstH264SPS * sps, 36 | } 37 | } 38 | 39 | + while (num > G_MAXINT32 || den > G_MAXINT32) { 40 | + num >>= 1; 41 | + den >>= 1; 42 | + } 43 | + 44 | *fps_num = num; 45 | *fps_den = den; 46 | } 47 | diff --git a/subprojects/gst-plugins-bad/tests/check/libs/h264parser.c b/subprojects/gst-plugins-bad/tests/check/libs/h264parser.c 48 | index ddf4fe50d4..1ddab3bea7 100644 49 | --- a/subprojects/gst-plugins-bad/tests/check/libs/h264parser.c 50 | +++ b/subprojects/gst-plugins-bad/tests/check/libs/h264parser.c 51 | @@ -1023,6 +1023,53 @@ GST_START_TEST (test_h264_split_avc) 52 | 53 | GST_END_TEST; 54 | 55 | +// This SPS block with vui parameters defines: 56 | +// - frame_mbs_only_flag: 1 (progressive) 57 | +// - time_scale: 4294967294 (overflows int32 capacity) 58 | +// - num_units_in_tick: 71834980 59 | +// The expected framerate is 29,894678707 fps. 60 | +static const guint8 nalu_sps_with_overflow_framerate[] = { 61 | + 0x00, 0x00, 0x00, 0x01, 0x27, 0x64, 0x00, 0x1f, 62 | + 0xac, 0x72, 0x14, 0x05, 0x00, 0x5b, 0xb0, 0x11, 63 | + 0x04, 0x48, 0x1d, 0x64, 0xff, 0xff, 0xff, 0xfe, 64 | + 0xe2, 0x20, 0x00, 0x74, 0xc1, 0x00, 0x00, 0x74, 65 | + 0xc1, 0x3b, 0xde, 0xe0, 0x3e, 0x10, 0x08, 0x32, 66 | + 0xc0 67 | +}; 68 | + 69 | +GST_START_TEST (test_h264_parse_overflow_framerate) 70 | +{ 71 | + GstH264ParserResult res; 72 | + GstH264NalUnit nalu; 73 | + GstH264SPS sps; 74 | + gint fps_num, fps_den; 75 | + GstH264NalParser *const parser = gst_h264_nal_parser_new (); 76 | + 77 | + res = gst_h264_parser_identify_nalu (parser, nalu_sps_with_overflow_framerate, 78 | + 0, sizeof (nalu_sps_with_overflow_framerate), &nalu); 79 | + assert_equals_int (res, GST_H264_PARSER_NO_NAL_END); 80 | + assert_equals_int (nalu.type, GST_H264_NAL_SPS); 81 | + assert_equals_int (nalu.size, 37); 82 | + 83 | + res = gst_h264_parser_parse_sps (parser, &nalu, &sps); 84 | + assert_equals_int (res, GST_H264_PARSER_OK); 85 | + fail_unless (sps.valid); 86 | + fail_unless (sps.frame_mbs_only_flag); 87 | + fail_unless (sps.vui_parameters_present_flag); 88 | + fail_unless (sps.vui_parameters.timing_info_present_flag); 89 | + assert_equals_uint64 (sps.vui_parameters.time_scale, 4294967294); 90 | + assert_equals_uint64 (sps.vui_parameters.num_units_in_tick, 71834980); 91 | + 92 | + gst_h264_video_calculate_framerate (&sps, 0, 0, &fps_num, &fps_den); 93 | + assert_equals_int (fps_num, 2147483647); 94 | + assert_equals_int (fps_den, 71834980); 95 | + 96 | + gst_h264_sps_clear (&sps); 97 | + gst_h264_nal_parser_free (parser); 98 | +} 99 | + 100 | +GST_END_TEST; 101 | + 102 | static Suite * 103 | h264parser_suite (void) 104 | { 105 | @@ -1039,6 +1086,7 @@ h264parser_suite (void) 106 | tcase_add_test (tc_chain, test_h264_create_sei); 107 | tcase_add_test (tc_chain, test_h264_decoder_config_record); 108 | tcase_add_test (tc_chain, test_h264_split_avc); 109 | + tcase_add_test (tc_chain, test_h264_parse_overflow_framerate); 110 | 111 | return s; 112 | } 113 | -- 114 | 2.43.0 115 | 116 | -------------------------------------------------------------------------------- /docker/build-gstreamer/patch/0001-glvideomixer-update-API-to-be-compatible-with-versio.patch: -------------------------------------------------------------------------------- 1 | From dd1d86caefe98550f3abff0bd018c5f449eda90b Mon Sep 17 00:00:00 2001 2 | From: =?UTF-8?q?Lo=C3=AFc=20Le=20Page?= 3 | Date: Mon, 31 Mar 2025 17:54:51 +0200 4 | Subject: [PATCH] glvideomixer: update API to be compatible with version 1.24 5 | 6 | --- 7 | .../gst-plugins-base/ext/gl/gstglmixerbin.c | 74 +++++++++++++++++++ 8 | .../gst-plugins-base/ext/gl/gstglmixerbin.h | 7 ++ 9 | .../gst-plugins-base/ext/gl/gstglvideomixer.c | 15 +++- 10 | 3 files changed, 95 insertions(+), 1 deletion(-) 11 | 12 | diff --git a/subprojects/gst-plugins-base/ext/gl/gstglmixerbin.c b/subprojects/gst-plugins-base/ext/gl/gstglmixerbin.c 13 | index 4e8d045e57..5b55a051b3 100644 14 | --- a/subprojects/gst-plugins-base/ext/gl/gstglmixerbin.c 15 | +++ b/subprojects/gst-plugins-base/ext/gl/gstglmixerbin.c 16 | @@ -31,9 +31,11 @@ 17 | #define GST_CAT_DEFAULT gst_gl_mixer_bin_debug 18 | GST_DEBUG_CATEGORY (gst_gl_mixer_bin_debug); 19 | 20 | +#define DEFAULT_FORCE_LIVE FALSE 21 | #define DEFAULT_LATENCY 0 22 | #define DEFAULT_START_TIME_SELECTION 0 23 | #define DEFAULT_START_TIME (-1) 24 | +#define DEFAULT_MIN_UPSTREAM_LATENCY (0) 25 | 26 | typedef enum 27 | { 28 | @@ -124,6 +126,8 @@ enum 29 | PROP_START_TIME_SELECTION, 30 | PROP_START_TIME, 31 | PROP_CONTEXT, 32 | + PROP_FORCE_LIVE, 33 | + PROP_MIN_UPSTREAM_LATENCY, 34 | }; 35 | 36 | enum 37 | @@ -215,6 +219,44 @@ gst_gl_mixer_bin_class_init (GstGLMixerBinClass * klass) 38 | "Get OpenGL context", 39 | GST_TYPE_GL_CONTEXT, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); 40 | 41 | + /** 42 | + * GstGLMixerBin:force-live: 43 | + * 44 | + * Causes the element to aggregate on a timeout even when no live source is 45 | + * connected to its sinks. See #GstGLMixerBin:min-upstream-latency for a 46 | + * companion property: in the vast majority of cases where you plan to plug in 47 | + * live sources with a non-zero latency, you should set it to a non-zero value. 48 | + * 49 | + * Since: 1.24 50 | + */ 51 | + g_object_class_install_property (gobject_class, PROP_FORCE_LIVE, 52 | + g_param_spec_boolean ("force-live", 53 | + "Force Live", 54 | + "Always operate in live mode and aggregate on timeout regardless of whether any live sources are linked upstream", 55 | + DEFAULT_FORCE_LIVE, 56 | + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT_ONLY)); 57 | + 58 | + /** 59 | + * GstGLMixerBin:min-upstream-latency: 60 | + * 61 | + * Force minimum upstream latency (in nanoseconds). When sources with a 62 | + * higher latency are expected to be plugged in dynamically after the 63 | + * aggregator has started playing, this allows overriding the minimum 64 | + * latency reported by the initial source(s). This is only taken into 65 | + * account when larger than the actually reported minimum latency. 66 | + * 67 | + * Since: 1.24 68 | + */ 69 | + g_object_class_install_property (gobject_class, PROP_MIN_UPSTREAM_LATENCY, 70 | + g_param_spec_uint64 ("min-upstream-latency", "Buffer latency", 71 | + "When sources with a higher latency are expected to be plugged " 72 | + "in dynamically after the aggregator has started playing, " 73 | + "this allows overriding the minimum latency reported by the " 74 | + "initial source(s). This is only taken into account when larger " 75 | + "than the actually reported minimum latency. (nanoseconds)", 76 | + 0, G_MAXUINT64, 77 | + DEFAULT_LATENCY, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); 78 | + 79 | /** 80 | * GstMixerBin::create-element: 81 | * @object: the #GstGLMixerBin 82 | @@ -270,6 +312,12 @@ gst_gl_mixer_bin_init (GstGLMixerBin * self) 83 | 84 | if (!res) 85 | GST_ERROR_OBJECT (self, "failed to create output chain"); 86 | + 87 | + self->force_live = DEFAULT_FORCE_LIVE; 88 | + self->latency = DEFAULT_LATENCY; 89 | + self->start_time_selection = DEFAULT_START_TIME_SELECTION; 90 | + self->start_time = DEFAULT_START_TIME; 91 | + self->min_upstream_latency = DEFAULT_MIN_UPSTREAM_LATENCY; 92 | } 93 | 94 | static void 95 | @@ -448,6 +496,9 @@ gst_gl_mixer_bin_get_property (GObject * object, 96 | case PROP_MIXER: 97 | g_value_set_object (value, self->mixer); 98 | break; 99 | + case PROP_FORCE_LIVE: 100 | + g_value_set_boolean (value, self->force_live); 101 | + break; 102 | default: 103 | if (self->mixer) 104 | g_object_get_property (G_OBJECT (self->mixer), pspec->name, value); 105 | @@ -474,6 +525,29 @@ gst_gl_mixer_bin_set_property (GObject * object, 106 | } 107 | break; 108 | } 109 | + case PROP_FORCE_LIVE: 110 | + self->force_live = g_value_get_boolean (value); 111 | + break; 112 | + case PROP_LATENCY: 113 | + self->latency = g_value_get_uint64 (value); 114 | + if (self->mixer) 115 | + g_object_set_property (G_OBJECT (self->mixer), pspec->name, value); 116 | + break; 117 | + case PROP_START_TIME_SELECTION: 118 | + self->start_time_selection = g_value_get_enum (value); 119 | + if (self->mixer) 120 | + g_object_set_property (G_OBJECT (self->mixer), pspec->name, value); 121 | + break; 122 | + case PROP_START_TIME: 123 | + self->start_time = g_value_get_uint64 (value); 124 | + if (self->mixer) 125 | + g_object_set_property (G_OBJECT (self->mixer), pspec->name, value); 126 | + break; 127 | + case PROP_MIN_UPSTREAM_LATENCY: 128 | + self->min_upstream_latency = g_value_get_uint64 (value); 129 | + if (self->mixer) 130 | + g_object_set_property (G_OBJECT (self->mixer), pspec->name, value); 131 | + break; 132 | default: 133 | if (self->mixer) 134 | g_object_set_property (G_OBJECT (self->mixer), pspec->name, value); 135 | diff --git a/subprojects/gst-plugins-base/ext/gl/gstglmixerbin.h b/subprojects/gst-plugins-base/ext/gl/gstglmixerbin.h 136 | index 5e5bb60b43..06491b7247 100644 137 | --- a/subprojects/gst-plugins-base/ext/gl/gstglmixerbin.h 138 | +++ b/subprojects/gst-plugins-base/ext/gl/gstglmixerbin.h 139 | @@ -53,6 +53,13 @@ struct _GstGLMixerBin 140 | GstElement *download; 141 | GstPad *srcpad; 142 | 143 | + gboolean force_live; 144 | + 145 | + GstClockTime latency; 146 | + guint start_time_selection; 147 | + GstClockTime start_time; 148 | + GstClockTime min_upstream_latency; 149 | + 150 | GstGLMixerBinPrivate *priv; 151 | }; 152 | 153 | diff --git a/subprojects/gst-plugins-base/ext/gl/gstglvideomixer.c b/subprojects/gst-plugins-base/ext/gl/gstglvideomixer.c 154 | index de08888095..f61f09fc01 100644 155 | --- a/subprojects/gst-plugins-base/ext/gl/gstglvideomixer.c 156 | +++ b/subprojects/gst-plugins-base/ext/gl/gstglvideomixer.c 157 | @@ -462,11 +462,23 @@ GST_ELEMENT_REGISTER_DEFINE_WITH_CODE (glvideomixer, "glvideomixer", 158 | 159 | static void 160 | gst_gl_video_mixer_bin_init (GstGLVideoMixerBin * self) 161 | +{ 162 | +} 163 | + 164 | +static void 165 | +gst_gl_video_mixer_bin_constructed (GObject * self) 166 | { 167 | GstGLMixerBin *mix_bin = GST_GL_MIXER_BIN (self); 168 | 169 | + G_OBJECT_CLASS (gst_gl_video_mixer_bin_parent_class)->constructed (self); 170 | + 171 | gst_gl_mixer_bin_finish_init_with_element (mix_bin, 172 | - g_object_new (GST_TYPE_GL_VIDEO_MIXER, NULL)); 173 | + g_object_new (GST_TYPE_GL_VIDEO_MIXER, 174 | + "force-live", mix_bin->force_live, 175 | + "latency", mix_bin->latency, 176 | + "start-time-selection", mix_bin->start_time_selection, 177 | + "start-time", mix_bin->start_time, 178 | + "min-upstream-latency", mix_bin->min_upstream_latency, NULL)); 179 | } 180 | 181 | static void 182 | @@ -479,6 +491,7 @@ gst_gl_video_mixer_bin_class_init (GstGLVideoMixerBinClass * klass) 183 | 184 | mixer_class->create_input_pad = _create_video_mixer_input; 185 | 186 | + gobject_class->constructed = gst_gl_video_mixer_bin_constructed; 187 | gobject_class->set_property = gst_gl_video_mixer_bin_set_property; 188 | gobject_class->get_property = gst_gl_video_mixer_bin_get_property; 189 | 190 | -- 191 | 2.43.0 192 | 193 | -------------------------------------------------------------------------------- /docker/build-gstreamer/patch/0001-compositor-add-rounded-corners-property-for-sink-pad.patch: -------------------------------------------------------------------------------- 1 | From dd260de0def61760196b76cf33a7bda62280db40 Mon Sep 17 00:00:00 2001 2 | From: =?UTF-8?q?Lo=C3=AFc=20Le=20Page?= 3 | Date: Thu, 18 Jul 2024 18:34:55 +0200 4 | Subject: [PATCH] compositor: add rounded corners property for sink pads 5 | 6 | --- 7 | .../gst/compositor/compositor.c | 239 +++++++++++++++++- 8 | .../gst/compositor/compositor.h | 7 + 9 | 2 files changed, 240 insertions(+), 6 deletions(-) 10 | 11 | diff --git a/subprojects/gst-plugins-base/gst/compositor/compositor.c b/subprojects/gst-plugins-base/gst/compositor/compositor.c 12 | index 54a2e9dc58..cc40e50add 100644 13 | --- a/subprojects/gst-plugins-base/gst/compositor/compositor.c 14 | +++ b/subprojects/gst-plugins-base/gst/compositor/compositor.c 15 | @@ -41,6 +41,20 @@ 16 | * * "alpha": The transparency of the picture; between 0.0 and 1.0. The blending 17 | * is a simple copy when fully-transparent (0.0) and fully-opaque (1.0). (#gdouble) 18 | * * "zorder": The z-order position of the picture in the composition (#guint) 19 | + * * "border-radius": The radius in pixels of the picture rounded corners (array 20 | + * of 0 to 4 #guint). It has the same effect as the CSS "border-radius" property 21 | + * and uses the same syntax: 22 | + * - if no value is set, no corner is rounded 23 | + * - if 1 value is set, all corners are rounded with the provided radius 24 | + * - if 2 values are set, the first one is for the top-left and bottom-right 25 | + * corners, and the second one for the top-right and bottom-left corners 26 | + * - if 3 values are set, the first one is for the top-left corner, the 27 | + * second one for the top-right and bottom-left corners, and the third one 28 | + * for the bottom-right corner 29 | + * - if 4 values are set, the first one is for the top-left corner, the second 30 | + * one for the top-right corner, the third one for the bottom-right corner, 31 | + * and the fourth one for the bottom-left corner 32 | + * - all extra values in the array are just ignored 33 | * 34 | * ## Sample pipelines 35 | * |[ 36 | @@ -83,13 +97,24 @@ 37 | * "video/x-raw,format=AYUV,width=800,height=600,framerate=(fraction)10/1" ! \ 38 | * timeoverlay ! queue2 ! comp. 39 | * ]| A pipeline to demonstrate synchronized compositing (the second stream starts after 3 seconds) 40 | - * 41 | + * |[ 42 | + * gst-launch-1.0 compositor name=comp \ 43 | + * sink_0::xpos=150 sink_0::ypos=150 sink_0::border-radius="<0,50,200>" \ 44 | + * sink_1::xpos=1300 sink_1::ypos=400 sink_1::border-radius="<400>" ! \ 45 | + * video/x-raw,width=1920,height=1080,framerate=30/1 ! \ 46 | + * videoconvert ! autovideosink \ 47 | + * videotestsrc ! video/x-raw,width=1280,height=720,format=RGBA ! \ 48 | + * timeoverlay ! queue ! comp. \ 49 | + * videotestsrc ! video/x-raw,width=400,height=400,format=RGBA ! \ 50 | + * queue ! comp. 51 | + * ]| A pipeline to demonstrate rounded corners 52 | */ 53 | 54 | #ifdef HAVE_CONFIG_H 55 | #include "config.h" 56 | #endif 57 | 58 | +#include 59 | #include 60 | 61 | #include "compositor.h" 62 | @@ -194,6 +219,7 @@ gst_compositor_sizing_policy_get_type (void) 63 | #define DEFAULT_PAD_ALPHA 1.0 64 | #define DEFAULT_PAD_OPERATOR COMPOSITOR_OPERATOR_OVER 65 | #define DEFAULT_PAD_SIZING_POLICY COMPOSITOR_SIZING_POLICY_NONE 66 | +#define DEFAULT_PAD_BORDER_RADIUS 0 67 | 68 | enum 69 | { 70 | @@ -205,6 +231,7 @@ enum 71 | PROP_PAD_ALPHA, 72 | PROP_PAD_OPERATOR, 73 | PROP_PAD_SIZING_POLICY, 74 | + PROP_PAD_BORDER_RADIUS, 75 | }; 76 | 77 | G_DEFINE_TYPE (GstCompositorPad, gst_compositor_pad, 78 | @@ -238,6 +265,26 @@ gst_compositor_pad_get_property (GObject * object, guint prop_id, 79 | case PROP_PAD_SIZING_POLICY: 80 | g_value_set_enum (value, pad->sizing_policy); 81 | break; 82 | + case PROP_PAD_BORDER_RADIUS: 83 | + { 84 | + GValue radius = G_VALUE_INIT; 85 | + g_value_init (&radius, G_TYPE_UINT); 86 | + 87 | + g_value_set_uint (&radius, pad->border_radius.top_left); 88 | + gst_value_array_append_value (value, &radius); 89 | + 90 | + g_value_set_uint (&radius, pad->border_radius.top_right); 91 | + gst_value_array_append_value (value, &radius); 92 | + 93 | + g_value_set_uint (&radius, pad->border_radius.bottom_right); 94 | + gst_value_array_append_value (value, &radius); 95 | + 96 | + g_value_set_uint (&radius, pad->border_radius.bottom_left); 97 | + gst_value_array_append_value (value, &radius); 98 | + 99 | + g_value_unset (&radius); 100 | + } 101 | + break; 102 | default: 103 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); 104 | break; 105 | @@ -273,13 +320,65 @@ gst_compositor_pad_set_property (GObject * object, guint prop_id, 106 | case PROP_PAD_OPERATOR: 107 | pad->op = g_value_get_enum (value); 108 | gst_video_aggregator_pad_set_needs_alpha (GST_VIDEO_AGGREGATOR_PAD (pad), 109 | - pad->op == COMPOSITOR_OPERATOR_ADD); 110 | + pad->op == COMPOSITOR_OPERATOR_ADD 111 | + || pad->border_radius.has_rounded_corners); 112 | break; 113 | case PROP_PAD_SIZING_POLICY: 114 | pad->sizing_policy = g_value_get_enum (value); 115 | gst_video_aggregator_convert_pad_update_conversion_info 116 | (GST_VIDEO_AGGREGATOR_CONVERT_PAD (pad)); 117 | break; 118 | + case PROP_PAD_BORDER_RADIUS: 119 | + switch (gst_value_array_get_size (value)) { 120 | + case 0: 121 | + pad->border_radius.top_left = 0; 122 | + pad->border_radius.top_right = 0; 123 | + pad->border_radius.bottom_right = 0; 124 | + pad->border_radius.bottom_left = 0; 125 | + break; 126 | + case 1: 127 | + pad->border_radius.top_left = 128 | + g_value_get_uint (gst_value_array_get_value (value, 0)); 129 | + pad->border_radius.top_right = pad->border_radius.top_left; 130 | + pad->border_radius.bottom_right = pad->border_radius.top_left; 131 | + pad->border_radius.bottom_left = pad->border_radius.top_left; 132 | + break; 133 | + case 2: 134 | + pad->border_radius.top_left = 135 | + g_value_get_uint (gst_value_array_get_value (value, 0)); 136 | + pad->border_radius.top_right = 137 | + g_value_get_uint (gst_value_array_get_value (value, 1)); 138 | + pad->border_radius.bottom_right = pad->border_radius.top_left; 139 | + pad->border_radius.bottom_left = pad->border_radius.top_right; 140 | + break; 141 | + case 3: 142 | + pad->border_radius.top_left = 143 | + g_value_get_uint (gst_value_array_get_value (value, 0)); 144 | + pad->border_radius.top_right = 145 | + g_value_get_uint (gst_value_array_get_value (value, 1)); 146 | + pad->border_radius.bottom_right = 147 | + g_value_get_uint (gst_value_array_get_value (value, 2)); 148 | + pad->border_radius.bottom_left = pad->border_radius.top_right; 149 | + break; 150 | + default: 151 | + pad->border_radius.top_left = 152 | + g_value_get_uint (gst_value_array_get_value (value, 0)); 153 | + pad->border_radius.top_right = 154 | + g_value_get_uint (gst_value_array_get_value (value, 1)); 155 | + pad->border_radius.bottom_right = 156 | + g_value_get_uint (gst_value_array_get_value (value, 2)); 157 | + pad->border_radius.bottom_left = 158 | + g_value_get_uint (gst_value_array_get_value (value, 3)); 159 | + break; 160 | + } 161 | + pad->border_radius.has_rounded_corners = (pad->border_radius.top_left > 0 162 | + || pad->border_radius.top_right > 0 163 | + || pad->border_radius.bottom_right > 0 164 | + || pad->border_radius.bottom_left > 0); 165 | + gst_video_aggregator_pad_set_needs_alpha (GST_VIDEO_AGGREGATOR_PAD (pad), 166 | + pad->op == COMPOSITOR_OPERATOR_ADD 167 | + || pad->border_radius.has_rounded_corners); 168 | + break; 169 | default: 170 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); 171 | break; 172 | @@ -475,11 +574,12 @@ _pad_obscures_rectangle (GstVideoAggregator * vagg, GstVideoAggregatorPad * pad, 173 | if (!gst_video_aggregator_pad_has_current_buffer (pad)) 174 | return FALSE; 175 | 176 | - /* Can't obscure if we introduce alpha or if the format has an alpha 177 | - * component as we'd have to inspect every pixel to know if the frame is 178 | - * opaque, so assume it doesn't obscure 179 | + /* Can't obscure if we introduce alpha, rounded corners, or if the format has 180 | + * an alpha component as we'd have to inspect every pixel to know if the frame 181 | + * is opaque, so assume it doesn't obscure 182 | */ 183 | - if (cpad->alpha != 1.0 || GST_VIDEO_INFO_HAS_ALPHA (&pad->info)) 184 | + if (cpad->alpha != 1.0 || cpad->border_radius.has_rounded_corners 185 | + || GST_VIDEO_INFO_HAS_ALPHA (&pad->info)) 186 | return FALSE; 187 | 188 | /* If a converter-config is set and it is either configured to not fill any 189 | @@ -716,6 +816,34 @@ gst_compositor_pad_class_init (GstCompositorPadClass * klass) 190 | GST_TYPE_COMPOSITOR_SIZING_POLICY, DEFAULT_PAD_SIZING_POLICY, 191 | G_PARAM_READWRITE | GST_PARAM_CONTROLLABLE | G_PARAM_STATIC_STRINGS)); 192 | 193 | + /** 194 | + * GstCompositorPad:border-radius: 195 | + * 196 | + * Specifies the border radius in pixels for each of the picture corners. It 197 | + * has the same effect and syntax as the CSS "border-radius" property: 198 | + * - if no value is set, no corner is rounded 199 | + * - if 1 value is set, all corners are rounded with the provided radius 200 | + * - if 2 values are set, the first one is for the top-left and bottom-right 201 | + * corners, and the second one for the top-right and bottom-left corners 202 | + * - if 3 values are set, the first one is for the top-left corner, the 203 | + * second one for the top-right and bottom-left corners, and the third one 204 | + * for the bottom-right corner 205 | + * - if 4 values are set, the first one is for the top-left corner, the second 206 | + * one for the top-right corner, the third one for the bottom-right corner, 207 | + * and the fourth one for the bottom-left corner 208 | + * - all extra values in the array are just ignored 209 | + * 210 | + * Since: 1.26 211 | + */ 212 | + g_object_class_install_property (gobject_class, PROP_PAD_BORDER_RADIUS, 213 | + gst_param_spec_array ("border-radius", "Border Radius", 214 | + "Border radius in pixels for each of the picture corners (same syntax as the CSS property)", 215 | + g_param_spec_uint ("radius", "radius", "radius", 0, G_MAXUINT, 216 | + DEFAULT_PAD_BORDER_RADIUS, 217 | + G_PARAM_READWRITE | GST_PARAM_CONTROLLABLE | 218 | + G_PARAM_STATIC_STRINGS), 219 | + G_PARAM_READWRITE | GST_PARAM_CONTROLLABLE | G_PARAM_STATIC_STRINGS)); 220 | + 221 | vaggpadclass->prepare_frame_start = 222 | GST_DEBUG_FUNCPTR (gst_compositor_pad_prepare_frame_start); 223 | 224 | @@ -735,6 +863,16 @@ gst_compositor_pad_init (GstCompositorPad * compo_pad) 225 | compo_pad->width = DEFAULT_PAD_WIDTH; 226 | compo_pad->height = DEFAULT_PAD_HEIGHT; 227 | compo_pad->sizing_policy = DEFAULT_PAD_SIZING_POLICY; 228 | + 229 | + compo_pad->border_radius.top_left = DEFAULT_PAD_BORDER_RADIUS; 230 | + compo_pad->border_radius.top_right = DEFAULT_PAD_BORDER_RADIUS; 231 | + compo_pad->border_radius.bottom_right = DEFAULT_PAD_BORDER_RADIUS; 232 | + compo_pad->border_radius.bottom_left = DEFAULT_PAD_BORDER_RADIUS; 233 | + compo_pad->border_radius.has_rounded_corners = 234 | + (compo_pad->border_radius.top_left > 0 235 | + || compo_pad->border_radius.top_right > 0 236 | + || compo_pad->border_radius.bottom_right > 0 237 | + || compo_pad->border_radius.bottom_left > 0); 238 | } 239 | 240 | 241 | @@ -1633,6 +1771,86 @@ blend_pads (struct CompositeTask *comp) 242 | } 243 | } 244 | 245 | +static void 246 | +cut_off_rounded_corners (GstCompositorPad * pad, GstVideoFrame * frame) 247 | +{ 248 | + // All color formats with an alpha component, currently composed at this 249 | + // stage, have common characteristics: 250 | + // - the alpha component depth is always 8 or 16 bits 251 | + // - in planar mode, the alpha plane has the same width and height as the Y 252 | + // plane (no subsampling) 253 | + guint alpha_comp_depth = GST_VIDEO_FRAME_COMP_DEPTH (frame, 3); 254 | + if ((alpha_comp_depth != 8 && alpha_comp_depth != 16) 255 | + || frame->info.finfo->w_sub[3] != 0 || frame->info.finfo->h_sub[3] != 0) { 256 | + GST_FIXME_OBJECT (pad, "Unexpected source frame video format %s", 257 | + gst_video_format_to_string (frame->info.finfo->format)); 258 | + return; 259 | + } 260 | + 261 | + guint max_border_radius = MIN (MAX (0, (frame->info.width - 1) >> 1), MAX (0, 262 | + (frame->info.height - 1) >> 1)); 263 | + 264 | + guint border_radius[4] = { 265 | + MIN (max_border_radius, pad->border_radius.top_left), 266 | + MIN (max_border_radius, pad->border_radius.top_right), 267 | + MIN (max_border_radius, pad->border_radius.bottom_left), 268 | + MIN (max_border_radius, pad->border_radius.bottom_right) 269 | + }; 270 | + 271 | + guint8 *alpha_data = GST_VIDEO_FRAME_COMP_DATA (frame, 3); 272 | + guint alpha_comp_stride = GST_VIDEO_FRAME_COMP_PSTRIDE (frame, 3); 273 | + guint line_stride = GST_VIDEO_FRAME_COMP_STRIDE (frame, 3); 274 | + 275 | + for (gint i = 0; i < 4; ++i) { 276 | + guint radius = border_radius[i]; 277 | + if (radius > 0) { 278 | + guint radius2 = radius * radius; 279 | + 280 | + for (guint y = 0; y <= radius; ++y) { 281 | + guint dy = radius - y; 282 | + guint dy2 = dy * dy; 283 | + 284 | + guint8 *line = alpha_data; 285 | + if (i < 2) { 286 | + // Top 287 | + line += y * line_stride; 288 | + } else { 289 | + // Bottom 290 | + line += (frame->info.height - 1 - y) * line_stride; 291 | + } 292 | + 293 | + for (guint x = 0; x <= radius; ++x) { 294 | + guint dx = radius - x; 295 | + float dist = dx * dx + dy2; 296 | + 297 | + if (dist >= radius2) { 298 | + // The falloff function has a width of 2 pixels, so we add 1 pixel 299 | + // to the distance to correctly center the arc rasterisation. 300 | + dist = sqrt (dist) - radius + 1; 301 | + float coeff = CLAMP (1.f - dist * dist * dist / 8.f, 0.f, 1.f); 302 | + 303 | + guint8 *component; 304 | + if (i % 2) { 305 | + // Right 306 | + component = 307 | + line + alpha_comp_stride * (frame->info.width - 1 - x); 308 | + } else { 309 | + // Left 310 | + component = line + alpha_comp_stride * x; 311 | + } 312 | + 313 | + if (alpha_comp_depth == 16) { 314 | + *(guint16 *) component *= coeff; 315 | + } else { 316 | + *component *= coeff; 317 | + } 318 | + } 319 | + } 320 | + } 321 | + } 322 | + } 323 | +} 324 | + 325 | static GstFlowReturn 326 | gst_compositor_aggregate_frames (GstVideoAggregator * vagg, GstBuffer * outbuf) 327 | { 328 | @@ -1718,6 +1936,15 @@ gst_compositor_aggregate_frames (GstVideoAggregator * vagg, GstBuffer * outbuf) 329 | frames_can_copy (prepared_frame, outframe)) { 330 | gst_video_frame_copy (outframe, prepared_frame); 331 | } else { 332 | + if (compo_pad->border_radius.has_rounded_corners 333 | + && GST_VIDEO_INFO_HAS_ALPHA (&prepared_frame->info)) { 334 | + if (blend_mode == COMPOSITOR_BLEND_MODE_SOURCE) { 335 | + blend_mode = COMPOSITOR_BLEND_MODE_OVER; 336 | + } 337 | + 338 | + cut_off_rounded_corners (compo_pad, prepared_frame); 339 | + } 340 | + 341 | pads_info[n_pads].pad = compo_pad; 342 | pads_info[n_pads].prepared_frame = prepared_frame; 343 | pads_info[n_pads].blend_mode = blend_mode; 344 | diff --git a/subprojects/gst-plugins-base/gst/compositor/compositor.h b/subprojects/gst-plugins-base/gst/compositor/compositor.h 345 | index c3d2998422..1b48c1b035 100644 346 | --- a/subprojects/gst-plugins-base/gst/compositor/compositor.h 347 | +++ b/subprojects/gst-plugins-base/gst/compositor/compositor.h 348 | @@ -170,6 +170,13 @@ struct _GstCompositorPad 349 | gint width, height; 350 | gdouble alpha; 351 | GstCompositorSizingPolicy sizing_policy; 352 | + struct { 353 | + guint top_left; 354 | + guint top_right; 355 | + guint bottom_right; 356 | + guint bottom_left; 357 | + gboolean has_rounded_corners; 358 | + } border_radius; 359 | 360 | GstCompositorOperator op; 361 | 362 | -- 363 | 2.34.1 364 | 365 | -------------------------------------------------------------------------------- /docker/build-gstreamer/patch/0001-compositor-add-box-shadow-property-for-sink-pads.patch: -------------------------------------------------------------------------------- 1 | From 31cf4ec891f738fc895f561ed3c7073ee45d2478 Mon Sep 17 00:00:00 2001 2 | From: =?UTF-8?q?Lo=C3=AFc=20Le=20Page?= 3 | Date: Wed, 24 Jul 2024 17:00:24 +0200 4 | Subject: [PATCH] compositor: add box-shadow property for sink pads 5 | 6 | --- 7 | .../gst/compositor/compositor.c | 977 ++++++++++++++---- 8 | .../gst/compositor/compositor.h | 1 + 9 | 2 files changed, 792 insertions(+), 186 deletions(-) 10 | 11 | diff --git a/subprojects/gst-plugins-base/gst/compositor/compositor.c b/subprojects/gst-plugins-base/gst/compositor/compositor.c 12 | index cc40e50add..c4f9615836 100644 13 | --- a/subprojects/gst-plugins-base/gst/compositor/compositor.c 14 | +++ b/subprojects/gst-plugins-base/gst/compositor/compositor.c 15 | @@ -55,6 +55,16 @@ 16 | * one for the top-right corner, the third one for the bottom-right corner, 17 | * and the fourth one for the bottom-left corner 18 | * - all extra values in the array are just ignored 19 | + * * "box-shadow": The box shadow layers parameters for the picture. It has the 20 | + * same effect as the corresponding CSS property. Each shadow layer type is 21 | + * drawn in the same order as the one passed when setting the property. Each 22 | + * shadow layer is specified with a #GstStructure containing the following 23 | + * (optional) fields: 24 | + * - "x" the shadow offset-x value as a #gint 25 | + * - "y" the shadow offset-y value as a #gint 26 | + * - "blur" the shadow blur-radius value as a #guint 27 | + * - "spread" the shadow spread-radius value as a #gint 28 | + * - "color" the shadow color value as a #guint32 in ARGB order 29 | * 30 | * ## Sample pipelines 31 | * |[ 32 | @@ -108,6 +118,34 @@ 33 | * videotestsrc ! video/x-raw,width=400,height=400,format=RGBA ! \ 34 | * queue ! comp. 35 | * ]| A pipeline to demonstrate rounded corners 36 | + * |[ 37 | + * gst-launch-1.0 compositor name=comp \ 38 | + * sink_0::xpos=150 sink_0::ypos=150 \ 39 | + * sink_0::box-shadow="<[layer,x=15,y=30,spread=10,color=0x88AA1199]>" \ 40 | + * sink_1::xpos=1300 sink_1::ypos=400 \ 41 | + * sink_1::box-shadow="<[layer,x=50,y=50,blur=50],[layer,x=10,y=20,color=0xF500B5B5]>" ! \ 42 | + * video/x-raw,width=1920,height=1080,framerate=30/1 ! \ 43 | + * videoconvert ! autovideosink \ 44 | + * videotestsrc ! video/x-raw,width=1280,height=720,format=RGBA ! \ 45 | + * timeoverlay ! queue ! comp. \ 46 | + * videotestsrc ! video/x-raw,width=400,height=400,format=RGBA ! \ 47 | + * queue ! comp. 48 | + * ]| A pipeline to demonstrate box shadows 49 | + * |[ 50 | + * gst-launch-1.0 compositor name=comp \ 51 | + * sink_0::xpos=150 sink_0::ypos=150 \ 52 | + * sink_0::border-radius="<0,50,200>" \ 53 | + * sink_0::box-shadow="<[layer,x=15,y=30,spread=10,color=0x88AA1199]>" \ 54 | + * sink_1::xpos=1300 sink_1::ypos=400 \ 55 | + * sink_1::border-radius="<400>" \ 56 | + * sink_1::box-shadow="<[layer,x=50,y=50,blur=50],[layer,x=10,y=20,color=0xF500B5B5]>" ! \ 57 | + * video/x-raw,width=1920,height=1080,framerate=30/1 ! \ 58 | + * videoconvert ! autovideosink \ 59 | + * videotestsrc ! video/x-raw,width=1280,height=720,format=RGBA ! \ 60 | + * timeoverlay ! queue ! comp. \ 61 | + * videotestsrc ! video/x-raw,width=400,height=400,format=RGBA ! \ 62 | + * queue ! comp. 63 | + * ]| A pipeline to demonstrate box shadows and rounded corners 64 | */ 65 | 66 | #ifdef HAVE_CONFIG_H 67 | @@ -232,14 +270,24 @@ enum 68 | PROP_PAD_OPERATOR, 69 | PROP_PAD_SIZING_POLICY, 70 | PROP_PAD_BORDER_RADIUS, 71 | + PROP_PAD_BOX_SHADOW, 72 | }; 73 | 74 | +typedef struct 75 | +{ 76 | + gint x; 77 | + gint y; 78 | + guint blur; 79 | + gint spread; 80 | + guint32 color; 81 | +} BoxShadow; 82 | + 83 | G_DEFINE_TYPE (GstCompositorPad, gst_compositor_pad, 84 | GST_TYPE_VIDEO_AGGREGATOR_PARALLEL_CONVERT_PAD); 85 | 86 | static void 87 | -gst_compositor_pad_get_property (GObject * object, guint prop_id, 88 | - GValue * value, GParamSpec * pspec) 89 | +gst_compositor_pad_get_property (GObject *object, guint prop_id, 90 | + GValue *value, GParamSpec *pspec) 91 | { 92 | GstCompositorPad *pad = GST_COMPOSITOR_PAD (object); 93 | 94 | @@ -285,6 +333,31 @@ gst_compositor_pad_get_property (GObject * object, guint prop_id, 95 | g_value_unset (&radius); 96 | } 97 | break; 98 | + case PROP_PAD_BOX_SHADOW: 99 | + { 100 | + GST_OBJECT_LOCK (pad); 101 | + GArray *box_shadows = 102 | + pad->box_shadows ? g_array_ref (pad->box_shadows) : NULL; 103 | + GST_OBJECT_UNLOCK (pad); 104 | + 105 | + if (box_shadows) { 106 | + for (guint i = 0; i < box_shadows->len; ++i) { 107 | + const BoxShadow *shadow = &g_array_index (box_shadows, BoxShadow, i); 108 | + 109 | + GValue shadow_struct_value = G_VALUE_INIT; 110 | + g_value_init (&shadow_struct_value, GST_TYPE_STRUCTURE); 111 | + g_value_take_boxed (&shadow_struct_value, 112 | + gst_structure_new ("layer", "x", G_TYPE_INT, shadow->x, "y", 113 | + G_TYPE_INT, shadow->y, "blur", G_TYPE_UINT, shadow->blur, 114 | + "spread", G_TYPE_INT, shadow->spread, "color", G_TYPE_UINT, 115 | + shadow->color, NULL)); 116 | + gst_value_array_append_and_take_value (value, &shadow_struct_value); 117 | + } 118 | + 119 | + g_array_unref (box_shadows); 120 | + } 121 | + } 122 | + break; 123 | default: 124 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); 125 | break; 126 | @@ -292,8 +365,8 @@ gst_compositor_pad_get_property (GObject * object, guint prop_id, 127 | } 128 | 129 | static void 130 | -gst_compositor_pad_set_property (GObject * object, guint prop_id, 131 | - const GValue * value, GParamSpec * pspec) 132 | +gst_compositor_pad_set_property (GObject *object, guint prop_id, 133 | + const GValue *value, GParamSpec *pspec) 134 | { 135 | GstCompositorPad *pad = GST_COMPOSITOR_PAD (object); 136 | 137 | @@ -379,6 +452,76 @@ gst_compositor_pad_set_property (GObject * object, guint prop_id, 138 | pad->op == COMPOSITOR_OPERATOR_ADD 139 | || pad->border_radius.has_rounded_corners); 140 | break; 141 | + case PROP_PAD_BOX_SHADOW: 142 | + { 143 | + const guint nb_structs = gst_value_array_get_size (value); 144 | + GArray *box_shadows = NULL; 145 | + 146 | + for (guint i = 0; i < nb_structs; ++i) { 147 | + const GstStructure *shadow_struct = 148 | + gst_value_get_structure (gst_value_array_get_value (value, i)); 149 | + if (!shadow_struct) { 150 | + continue; 151 | + } 152 | + 153 | + BoxShadow shadow = { }; 154 | + if (!gst_structure_get_int (shadow_struct, "x", &shadow.x)) { 155 | + shadow.x = 0; 156 | + } 157 | + 158 | + if (!gst_structure_get_int (shadow_struct, "y", &shadow.y)) { 159 | + shadow.y = 0; 160 | + } 161 | + 162 | + if (!gst_structure_get_uint (shadow_struct, "blur", &shadow.blur)) { 163 | + int blur = 0; 164 | + if (gst_structure_get_int (shadow_struct, "blur", &blur) && blur > 0) { 165 | + shadow.blur = blur; 166 | + } else { 167 | + shadow.blur = 0; 168 | + } 169 | + } 170 | + 171 | + if (!gst_structure_get_int (shadow_struct, "spread", &shadow.spread)) { 172 | + shadow.spread = 0; 173 | + } 174 | + 175 | + if (shadow.x == 0 && shadow.y == 0 && shadow.blur == 0 176 | + && shadow.spread <= 0) { 177 | + continue; 178 | + } 179 | + 180 | + if (!gst_structure_get_uint (shadow_struct, "color", &shadow.color)) { 181 | + int color = 0; 182 | + if (gst_structure_get_int (shadow_struct, "color", &color)) { 183 | + shadow.color = (guint32) color; 184 | + } else { 185 | + shadow.color = 0xFF << 24; 186 | + } 187 | + } 188 | + 189 | + if ((shadow.color & 0xFF000000) == 0) { 190 | + continue; 191 | + } 192 | + 193 | + if (!box_shadows) { 194 | + box_shadows = g_array_sized_new (FALSE, FALSE, sizeof (BoxShadow), 2); 195 | + } 196 | + 197 | + g_array_append_vals (box_shadows, &shadow, 1); 198 | + } 199 | + 200 | + GST_OBJECT_LOCK (pad); 201 | + GArray *array = pad->box_shadows; 202 | + pad->box_shadows = box_shadows; 203 | + box_shadows = array; 204 | + GST_OBJECT_UNLOCK (pad); 205 | + 206 | + if (box_shadows) { 207 | + g_array_unref (box_shadows); 208 | + } 209 | + } 210 | + break; 211 | default: 212 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); 213 | break; 214 | @@ -386,9 +529,26 @@ gst_compositor_pad_set_property (GObject * object, guint prop_id, 215 | } 216 | 217 | static void 218 | -_mixer_pad_get_output_size (GstCompositor * comp, GstCompositorPad * comp_pad, 219 | - gint out_par_n, gint out_par_d, gint * width, gint * height, 220 | - gint * x_offset, gint * y_offset) 221 | +gst_compositor_pad_dispose (GObject *object) 222 | +{ 223 | + GstCompositorPad *pad = GST_COMPOSITOR_PAD (object); 224 | + 225 | + GST_OBJECT_LOCK (pad); 226 | + GArray *box_shadows = pad->box_shadows; 227 | + pad->box_shadows = NULL; 228 | + GST_OBJECT_UNLOCK (pad); 229 | + 230 | + if (box_shadows) { 231 | + g_array_unref (box_shadows); 232 | + } 233 | + 234 | + G_OBJECT_CLASS (gst_compositor_pad_parent_class)->dispose (object); 235 | +} 236 | + 237 | +static void 238 | +_mixer_pad_get_output_size (GstCompositor *comp, GstCompositorPad *comp_pad, 239 | + gint out_par_n, gint out_par_d, gint *width, gint *height, 240 | + gint *x_offset, gint *y_offset) 241 | { 242 | GstVideoAggregatorPad *vagg_pad = GST_VIDEO_AGGREGATOR_PAD (comp_pad); 243 | gint pad_width, pad_height; 244 | @@ -538,30 +698,70 @@ is_rectangle_contained (const GstVideoRectangle rect1, 245 | return FALSE; 246 | } 247 | 248 | -static GstVideoRectangle 249 | -clamp_rectangle (gint x, gint y, gint w, gint h, gint outer_width, 250 | - gint outer_height) 251 | +static void 252 | +extend_rectangle_with_box_shadows (GstVideoRectangle *rect, 253 | + GstCompositorPad *pad) 254 | { 255 | - gint x2 = x + w; 256 | - gint y2 = y + h; 257 | - GstVideoRectangle clamped; 258 | + GST_OBJECT_LOCK (pad); 259 | + GArray *box_shadows = 260 | + pad->box_shadows ? g_array_ref (pad->box_shadows) : NULL; 261 | + GST_OBJECT_UNLOCK (pad); 262 | + 263 | + if (box_shadows) { 264 | + gint xmin = rect->x; 265 | + gint xmax = rect->x + rect->w; 266 | + gint ymin = rect->y; 267 | + gint ymax = rect->y + rect->h; 268 | + 269 | + for (guint i = 0; i < box_shadows->len; ++i) { 270 | + const BoxShadow *shadow = &g_array_index (box_shadows, BoxShadow, i); 271 | + 272 | + gint spread_limit = -2 * shadow->spread; 273 | + if (rect->w <= spread_limit || rect->h <= spread_limit) { 274 | + // No shadow because spread is bigger than the image rect 275 | + continue; 276 | + } 277 | + 278 | + gint blur_extent = 2 * shadow->blur; 279 | + xmin = MIN (xmin, rect->x + shadow->x - shadow->spread - blur_extent); 280 | + ymin = MIN (ymin, rect->y + shadow->y - shadow->spread - blur_extent); 281 | + xmax = 282 | + MAX (xmax, 283 | + rect->x + shadow->x + rect->w + shadow->spread + blur_extent); 284 | + ymax = 285 | + MAX (ymax, 286 | + rect->y + shadow->y + rect->h + shadow->spread + blur_extent); 287 | + } 288 | + 289 | + rect->x = xmin; 290 | + rect->y = ymin; 291 | + rect->w = xmax - xmin; 292 | + rect->h = ymax - ymin; 293 | + 294 | + g_array_unref (box_shadows); 295 | + } 296 | +} 297 | + 298 | +static void 299 | +clamp_rectangle (GstVideoRectangle *rect, gint outer_width, gint outer_height) 300 | +{ 301 | + gint x2 = rect->x + rect->w; 302 | + gint y2 = rect->y + rect->h; 303 | 304 | /* Clamp the x/y coordinates of this frame to the output boundaries to cover 305 | * the case where (say, with negative xpos/ypos or w/h greater than the output 306 | * size) the non-obscured portion of the frame could be outside the bounds of 307 | * the video itself and hence not visible at all */ 308 | - clamped.x = CLAMP (x, 0, outer_width); 309 | - clamped.y = CLAMP (y, 0, outer_height); 310 | - clamped.w = CLAMP (x2, 0, outer_width) - clamped.x; 311 | - clamped.h = CLAMP (y2, 0, outer_height) - clamped.y; 312 | - 313 | - return clamped; 314 | + rect->x = CLAMP (rect->x, 0, outer_width); 315 | + rect->y = CLAMP (rect->y, 0, outer_height); 316 | + rect->w = CLAMP (x2, 0, outer_width) - rect->x; 317 | + rect->h = CLAMP (y2, 0, outer_height) - rect->y; 318 | } 319 | 320 | /* Call this with the lock taken */ 321 | static gboolean 322 | -_pad_obscures_rectangle (GstVideoAggregator * vagg, GstVideoAggregatorPad * pad, 323 | - const GstVideoRectangle rect) 324 | +_pad_image_fully_hide_underneath_rectangle (GstVideoAggregator *vagg, 325 | + GstVideoAggregatorPad *pad, const GstVideoRectangle rect) 326 | { 327 | GstVideoRectangle pad_rect; 328 | GstCompositorPad *cpad = GST_COMPOSITOR_PAD (pad); 329 | @@ -605,8 +805,47 @@ _pad_obscures_rectangle (GstVideoAggregator * vagg, GstVideoAggregatorPad * pad, 330 | pad_rect.x += x_offset; 331 | pad_rect.y += y_offset; 332 | 333 | - if (!is_rectangle_contained (rect, pad_rect)) 334 | - return FALSE; 335 | + if (!is_rectangle_contained (rect, pad_rect)) { 336 | + // The pad image rect doesn't fully contain the underneath rectangle, 337 | + // but maybe one of the pad's shadow layers may still hide it. 338 | + GST_OBJECT_LOCK (cpad); 339 | + GArray *box_shadows = 340 | + cpad->box_shadows ? g_array_ref (cpad->box_shadows) : NULL; 341 | + GST_OBJECT_UNLOCK (cpad); 342 | + 343 | + if (!box_shadows) { 344 | + return FALSE; 345 | + } 346 | + 347 | + gboolean is_hidding = FALSE; 348 | + GstVideoRectangle shadow_rect; 349 | + for (guint i = 0; i < box_shadows->len; ++i) { 350 | + const BoxShadow *shadow = &g_array_index (box_shadows, BoxShadow, i); 351 | + 352 | + if ((shadow->color & 0xFF000000) != 0xFF000000) { 353 | + continue; 354 | + } 355 | + 356 | + gint blur_extent = 2 * shadow->blur; 357 | + shadow_rect.x = pad_rect.x + shadow->x - shadow->spread + blur_extent; 358 | + shadow_rect.y = pad_rect.y + shadow->y - shadow->spread + blur_extent; 359 | + 360 | + gint opaque_extent = 2 * (shadow->spread - blur_extent); 361 | + shadow_rect.w = pad_rect.w + opaque_extent; 362 | + shadow_rect.h = pad_rect.h + opaque_extent; 363 | + 364 | + if (is_rectangle_contained (rect, shadow_rect)) { 365 | + is_hidding = TRUE; 366 | + break; 367 | + } 368 | + } 369 | + 370 | + g_array_unref (box_shadows); 371 | + 372 | + if (!is_hidding) { 373 | + return FALSE; 374 | + } 375 | + } 376 | 377 | GST_DEBUG_OBJECT (pad, "Pad %s %ix%i@(%i,%i) obscures rect %ix%i@(%i,%i)", 378 | GST_PAD_NAME (pad), pad_rect.w, pad_rect.h, pad_rect.x, pad_rect.y, 379 | @@ -616,16 +855,16 @@ _pad_obscures_rectangle (GstVideoAggregator * vagg, GstVideoAggregatorPad * pad, 380 | } 381 | 382 | static void 383 | -gst_compositor_pad_prepare_frame_start (GstVideoAggregatorPad * pad, 384 | - GstVideoAggregator * vagg, GstBuffer * buffer, 385 | - GstVideoFrame * prepared_frame) 386 | +gst_compositor_pad_prepare_frame_start (GstVideoAggregatorPad *pad, 387 | + GstVideoAggregator *vagg, GstBuffer *buffer, GstVideoFrame *prepared_frame) 388 | { 389 | GstCompositorPad *cpad = GST_COMPOSITOR_PAD (pad); 390 | gint width, height; 391 | gboolean frame_obscured = FALSE; 392 | GList *l; 393 | - /* The rectangle representing this frame, clamped to the video's boundaries. 394 | - * Due to the clamping, this is different from the frame width/height above. */ 395 | + /* The rectangle representing this frame, including box shadows, clamped 396 | + * to the video's boundaries. Due to the clamping, this is different from the 397 | + * frame width/height above. */ 398 | GstVideoRectangle frame_rect; 399 | 400 | /* There's three types of width/height here: 401 | @@ -652,9 +891,13 @@ gst_compositor_pad_prepare_frame_start (GstVideoAggregatorPad * pad, 402 | if (gst_aggregator_pad_is_inactive (GST_AGGREGATOR_PAD (pad))) 403 | return; 404 | 405 | - frame_rect = clamp_rectangle (cpad->xpos + cpad->x_offset, 406 | - cpad->ypos + cpad->y_offset, width, height, 407 | - GST_VIDEO_INFO_WIDTH (&vagg->info), GST_VIDEO_INFO_HEIGHT (&vagg->info)); 408 | + frame_rect.x = cpad->xpos + cpad->x_offset; 409 | + frame_rect.y = cpad->ypos + cpad->y_offset; 410 | + frame_rect.w = width; 411 | + frame_rect.h = height; 412 | + extend_rectangle_with_box_shadows (&frame_rect, cpad); 413 | + clamp_rectangle (&frame_rect, GST_VIDEO_INFO_WIDTH (&vagg->info), 414 | + GST_VIDEO_INFO_HEIGHT (&vagg->info)); 415 | 416 | if (frame_rect.w == 0 || frame_rect.h == 0) { 417 | GST_DEBUG_OBJECT (pad, "Resulting frame is zero-width or zero-height " 418 | @@ -685,7 +928,7 @@ gst_compositor_pad_prepare_frame_start (GstVideoAggregatorPad * pad, 419 | continue; 420 | } 421 | 422 | - if (_pad_obscures_rectangle (vagg, l->data, frame_rect)) { 423 | + if (_pad_image_fully_hide_underneath_rectangle (vagg, l->data, frame_rect)) { 424 | frame_obscured = TRUE; 425 | break; 426 | } 427 | @@ -701,8 +944,8 @@ gst_compositor_pad_prepare_frame_start (GstVideoAggregatorPad * pad, 428 | } 429 | 430 | static void 431 | -gst_compositor_pad_create_conversion_info (GstVideoAggregatorConvertPad * pad, 432 | - GstVideoAggregator * vagg, GstVideoInfo * conversion_info) 433 | +gst_compositor_pad_create_conversion_info (GstVideoAggregatorConvertPad *pad, 434 | + GstVideoAggregator *vagg, GstVideoInfo *conversion_info) 435 | { 436 | GstCompositor *self = GST_COMPOSITOR (vagg); 437 | GstCompositorPad *cpad = GST_COMPOSITOR_PAD (pad); 438 | @@ -763,7 +1006,7 @@ gst_compositor_pad_create_conversion_info (GstVideoAggregatorConvertPad * pad, 439 | } 440 | 441 | static void 442 | -gst_compositor_pad_class_init (GstCompositorPadClass * klass) 443 | +gst_compositor_pad_class_init (GstCompositorPadClass *klass) 444 | { 445 | GObjectClass *gobject_class = (GObjectClass *) klass; 446 | GstVideoAggregatorPadClass *vaggpadclass = 447 | @@ -773,6 +1016,7 @@ gst_compositor_pad_class_init (GstCompositorPadClass * klass) 448 | 449 | gobject_class->set_property = gst_compositor_pad_set_property; 450 | gobject_class->get_property = gst_compositor_pad_get_property; 451 | + gobject_class->dispose = gst_compositor_pad_dispose; 452 | 453 | g_object_class_install_property (gobject_class, PROP_PAD_XPOS, 454 | g_param_spec_int ("xpos", "X Position", "X Position of the picture", 455 | @@ -844,6 +1088,33 @@ gst_compositor_pad_class_init (GstCompositorPadClass * klass) 456 | G_PARAM_STATIC_STRINGS), 457 | G_PARAM_READWRITE | GST_PARAM_CONTROLLABLE | G_PARAM_STATIC_STRINGS)); 458 | 459 | + /** 460 | + * GstCompositorPad:box-shadow: 461 | + * 462 | + * Specifies box shadows for the picture. It has the same effect as the CSS 463 | + * "box-shadow" property. Each shadow layer type is drawn in the same order 464 | + * as the one passed when setting the property. Each shadow layer is 465 | + * specified with a #GstStructure containing the following (optional) fields: 466 | + * - "x" the shadow offset-x value as a #gint 467 | + * - "y" the shadow offset-y value as a #gint 468 | + * - "blur" the shadow blur-radius value as a #guint 469 | + * - "spread" the shadow spread-radius value as a #gint 470 | + * - "color" the shadow color value as a #guint32 in ARGB order 471 | + * 472 | + * Since: 1.26 473 | + */ 474 | + g_object_class_install_property (gobject_class, PROP_PAD_BOX_SHADOW, 475 | + gst_param_spec_array ("box-shadow", "Box Shadow", 476 | + "Box shadow layers (one GstStructure per layer) specifying the " 477 | + "\"x\" (gint), \"y\" (gint), \"blur\" (guint), \"spread\" (gint) " 478 | + "and \"color\" (guint32 in ARGB order) " 479 | + "parameters (same syntax as the CSS property)", 480 | + g_param_spec_boxed ("layer", "Box Shadow Layer", 481 | + "Box Shadow Layer", GST_TYPE_STRUCTURE, 482 | + G_PARAM_READWRITE | GST_PARAM_CONTROLLABLE | 483 | + G_PARAM_STATIC_STRINGS), 484 | + G_PARAM_READWRITE | GST_PARAM_CONTROLLABLE | G_PARAM_STATIC_STRINGS)); 485 | + 486 | vaggpadclass->prepare_frame_start = 487 | GST_DEBUG_FUNCPTR (gst_compositor_pad_prepare_frame_start); 488 | 489 | @@ -854,7 +1125,7 @@ gst_compositor_pad_class_init (GstCompositorPadClass * klass) 490 | } 491 | 492 | static void 493 | -gst_compositor_pad_init (GstCompositorPad * compo_pad) 494 | +gst_compositor_pad_init (GstCompositorPad *compo_pad) 495 | { 496 | compo_pad->xpos = DEFAULT_PAD_XPOS; 497 | compo_pad->ypos = DEFAULT_PAD_YPOS; 498 | @@ -891,8 +1162,8 @@ enum 499 | }; 500 | 501 | static void 502 | -gst_compositor_get_property (GObject * object, 503 | - guint prop_id, GValue * value, GParamSpec * pspec) 504 | +gst_compositor_get_property (GObject *object, 505 | + guint prop_id, GValue *value, GParamSpec *pspec) 506 | { 507 | GstCompositor *self = GST_COMPOSITOR (object); 508 | 509 | @@ -917,8 +1188,8 @@ gst_compositor_get_property (GObject * object, 510 | } 511 | 512 | static void 513 | -gst_compositor_set_property (GObject * object, 514 | - guint prop_id, const GValue * value, GParamSpec * pspec) 515 | +gst_compositor_set_property (GObject *object, 516 | + guint prop_id, const GValue *value, GParamSpec *pspec) 517 | { 518 | GstCompositor *self = GST_COMPOSITOR (object); 519 | 520 | @@ -950,7 +1221,7 @@ GST_ELEMENT_REGISTER_DEFINE (compositor, "compositor", GST_RANK_PRIMARY + 1, 521 | GST_TYPE_COMPOSITOR); 522 | 523 | static gboolean 524 | -set_functions (GstCompositor * self, const GstVideoInfo * info) 525 | +set_functions (GstCompositor *self, const GstVideoInfo *info) 526 | { 527 | gint offset[GST_VIDEO_MAX_COMPONENTS] = { 0, }; 528 | gint scale[GST_VIDEO_MAX_COMPONENTS] = { 0, }; 529 | @@ -1295,7 +1566,7 @@ set_functions (GstCompositor * self, const GstVideoInfo * info) 530 | } 531 | 532 | static GstCaps * 533 | -_fixate_caps (GstAggregator * agg, GstCaps * caps) 534 | +_fixate_caps (GstAggregator *agg, GstCaps *caps) 535 | { 536 | GstVideoAggregator *vagg = GST_VIDEO_AGGREGATOR (agg); 537 | GList *l; 538 | @@ -1395,7 +1666,7 @@ gst_parallelized_task_thread_func (gpointer data) 539 | } 540 | 541 | static void 542 | -gst_parallelized_task_runner_join (GstParallelizedTaskRunner * self) 543 | +gst_parallelized_task_runner_join (GstParallelizedTaskRunner *self) 544 | { 545 | gboolean joined = FALSE; 546 | 547 | @@ -1412,7 +1683,7 @@ gst_parallelized_task_runner_join (GstParallelizedTaskRunner * self) 548 | } 549 | 550 | static void 551 | -gst_parallelized_task_runner_free (GstParallelizedTaskRunner * self) 552 | +gst_parallelized_task_runner_free (GstParallelizedTaskRunner *self) 553 | { 554 | gst_parallelized_task_runner_join (self); 555 | 556 | @@ -1425,7 +1696,7 @@ gst_parallelized_task_runner_free (GstParallelizedTaskRunner * self) 557 | } 558 | 559 | static GstParallelizedTaskRunner * 560 | -gst_parallelized_task_runner_new (guint n_threads, GstTaskPool * pool, 561 | +gst_parallelized_task_runner_new (guint n_threads, GstTaskPool *pool, 562 | gboolean async_tasks) 563 | { 564 | GstParallelizedTaskRunner *self; 565 | @@ -1469,7 +1740,7 @@ gst_parallelized_task_runner_new (guint n_threads, GstTaskPool * pool, 566 | } 567 | 568 | static void 569 | -gst_parallelized_task_runner_finish (GstParallelizedTaskRunner * self) 570 | +gst_parallelized_task_runner_finish (GstParallelizedTaskRunner *self) 571 | { 572 | g_return_if_fail (self->func != NULL); 573 | 574 | @@ -1480,8 +1751,8 @@ gst_parallelized_task_runner_finish (GstParallelizedTaskRunner * self) 575 | } 576 | 577 | static void 578 | -gst_parallelized_task_runner_run (GstParallelizedTaskRunner * self, 579 | - GstParallelizedTaskFunc func, gpointer * task_data) 580 | +gst_parallelized_task_runner_run (GstParallelizedTaskRunner *self, 581 | + GstParallelizedTaskFunc func, gpointer *task_data) 582 | { 583 | guint n_threads = self->n_threads; 584 | 585 | @@ -1521,7 +1792,7 @@ gst_parallelized_task_runner_run (GstParallelizedTaskRunner * self, 586 | } 587 | 588 | static gboolean 589 | -_negotiated_caps (GstAggregator * agg, GstCaps * caps) 590 | +_negotiated_caps (GstAggregator *agg, GstCaps *caps) 591 | { 592 | GstCompositor *compositor = GST_COMPOSITOR (agg); 593 | GstVideoAggregator *vagg = GST_VIDEO_AGGREGATOR (agg); 594 | @@ -1620,7 +1891,7 @@ _negotiated_caps (GstAggregator * agg, GstCaps * caps) 595 | } 596 | 597 | static gboolean 598 | -gst_composior_stop (GstAggregator * agg) 599 | +gst_composior_stop (GstAggregator *agg) 600 | { 601 | GstCompositor *self = GST_COMPOSITOR (agg); 602 | 603 | @@ -1631,7 +1902,7 @@ gst_composior_stop (GstAggregator * agg) 604 | } 605 | 606 | static gboolean 607 | -_should_draw_background (GstVideoAggregator * vagg) 608 | +_should_draw_background (GstVideoAggregator *vagg) 609 | { 610 | GstVideoRectangle bg_rect; 611 | gboolean draw = TRUE; 612 | @@ -1651,7 +1922,7 @@ _should_draw_background (GstVideoAggregator * vagg) 613 | (l->data)) == NULL) 614 | continue; 615 | 616 | - if (_pad_obscures_rectangle (vagg, l->data, bg_rect)) { 617 | + if (_pad_image_fully_hide_underneath_rectangle (vagg, l->data, bg_rect)) { 618 | draw = FALSE; 619 | break; 620 | } 621 | @@ -1661,7 +1932,7 @@ _should_draw_background (GstVideoAggregator * vagg) 622 | } 623 | 624 | static gboolean 625 | -frames_can_copy (const GstVideoFrame * frame1, const GstVideoFrame * frame2) 626 | +frames_can_copy (const GstVideoFrame *frame1, const GstVideoFrame *frame2) 627 | { 628 | if (GST_VIDEO_FRAME_FORMAT (frame1) != GST_VIDEO_FRAME_FORMAT (frame2)) 629 | return FALSE; 630 | @@ -1672,14 +1943,157 @@ frames_can_copy (const GstVideoFrame * frame1, const GstVideoFrame * frame2) 631 | return TRUE; 632 | } 633 | 634 | -struct CompositePadInfo 635 | +typedef struct 636 | +{ 637 | + BoxShadow shadow; 638 | + GstVideoRectangle dest_rect; 639 | + GstVideoFrame frame; 640 | +} BoxShadowFrame; 641 | + 642 | +typedef struct 643 | { 644 | GstVideoFrame *prepared_frame; 645 | GstCompositorPad *pad; 646 | GstCompositorBlendMode blend_mode; 647 | -}; 648 | 649 | -struct CompositeTask 650 | + gboolean has_rounded_corners; 651 | + guint border_radius[4]; 652 | + 653 | + BoxShadowFrame *box_shadows; 654 | + guint box_shadows_len; 655 | +} CompositePadInfo; 656 | + 657 | +static gboolean 658 | +extract_composite_pad_info (GstCompositorPad *pad, CompositePadInfo *info) 659 | +{ 660 | + GstVideoFrame *prepared_frame = 661 | + gst_video_aggregator_pad_get_prepared_frame (GST_VIDEO_AGGREGATOR_PAD 662 | + (pad)); 663 | + if (!prepared_frame) { 664 | + return FALSE; 665 | + } 666 | + 667 | + memset (info, 0, sizeof (CompositePadInfo)); 668 | + info->prepared_frame = prepared_frame; 669 | + info->pad = pad; 670 | + 671 | + switch (pad->op) { 672 | + case COMPOSITOR_OPERATOR_SOURCE: 673 | + info->blend_mode = COMPOSITOR_BLEND_MODE_SOURCE; 674 | + break; 675 | + case COMPOSITOR_OPERATOR_ADD: 676 | + info->blend_mode = COMPOSITOR_BLEND_MODE_ADD; 677 | + break; 678 | + case COMPOSITOR_OPERATOR_OVER: 679 | + default: 680 | + info->blend_mode = COMPOSITOR_BLEND_MODE_OVER; 681 | + break; 682 | + } 683 | + 684 | + // Rounded corners 685 | + if (pad->border_radius.has_rounded_corners 686 | + && GST_VIDEO_INFO_HAS_ALPHA (&prepared_frame->info)) { 687 | + // All color formats with an alpha component, currently composed at this 688 | + // stage if the rounded corners are enabled, have common characteristics: 689 | + // - the alpha component depth is always 8 or 16 bits 690 | + // - in planar mode, the alpha plane has the same width and height as the 691 | + // Y plane (no subsampling) 692 | + guint alpha_comp_depth = GST_VIDEO_FRAME_COMP_DEPTH (prepared_frame, 3); 693 | + if ((alpha_comp_depth != 8 && alpha_comp_depth != 16) 694 | + || prepared_frame->info.finfo->w_sub[3] != 0 695 | + || prepared_frame->info.finfo->h_sub[3] != 0) { 696 | + GST_FIXME_OBJECT (pad, 697 | + "Unexpected source frame video format %s, disabling rounding corners for this pad", 698 | + gst_video_format_to_string (prepared_frame->info.finfo->format)); 699 | + } else { 700 | + guint max_border_radius = 701 | + MIN (MAX (0, (prepared_frame->info.width - 1) >> 1), MAX (0, 702 | + (prepared_frame->info.height - 1) >> 1)); 703 | + 704 | + info->border_radius[0] = 705 | + MIN (max_border_radius, pad->border_radius.top_left); 706 | + info->border_radius[1] = 707 | + MIN (max_border_radius, pad->border_radius.top_right); 708 | + info->border_radius[2] = 709 | + MIN (max_border_radius, pad->border_radius.bottom_left); 710 | + info->border_radius[3] = 711 | + MIN (max_border_radius, pad->border_radius.bottom_right); 712 | + info->has_rounded_corners = (info->border_radius[0] > 0 713 | + || info->border_radius[1] > 0 || info->border_radius[2] > 0 714 | + || info->border_radius[3] > 0); 715 | + 716 | + if (info->has_rounded_corners 717 | + && info->blend_mode == COMPOSITOR_BLEND_MODE_SOURCE) { 718 | + info->blend_mode = COMPOSITOR_BLEND_MODE_OVER; 719 | + } 720 | + } 721 | + } 722 | + // Shadows 723 | + GST_OBJECT_LOCK (pad); 724 | + GArray *box_shadows = 725 | + pad->box_shadows ? g_array_ref (pad->box_shadows) : NULL; 726 | + GST_OBJECT_UNLOCK (pad); 727 | + 728 | + if (box_shadows) { 729 | + info->box_shadows = g_new0 (BoxShadowFrame, box_shadows->len); 730 | + 731 | + for (guint i = 0; i < box_shadows->len; ++i) { 732 | + BoxShadowFrame *shadow_frame = info->box_shadows + info->box_shadows_len; 733 | + shadow_frame->shadow = g_array_index (box_shadows, BoxShadow, i); 734 | + 735 | + gint spread_limit = -2 * shadow_frame->shadow.spread; 736 | + if (prepared_frame->info.width <= spread_limit 737 | + || prepared_frame->info.height <= spread_limit) { 738 | + // No shadow because the substracted spread is bigger than the pad image 739 | + continue; 740 | + } 741 | + 742 | + shadow_frame->dest_rect.w = prepared_frame->info.width - spread_limit; 743 | + shadow_frame->dest_rect.h = prepared_frame->info.height - spread_limit; 744 | + 745 | + shadow_frame->shadow.blur = 746 | + MIN (shadow_frame->shadow.blur, MIN (shadow_frame->dest_rect.w >> 2, 747 | + shadow_frame->dest_rect.h >> 2)); 748 | + 749 | + gint blur_extent = 2 * shadow_frame->shadow.blur; 750 | + shadow_frame->dest_rect.x = 751 | + pad->xpos + pad->x_offset + shadow_frame->shadow.x - 752 | + shadow_frame->shadow.spread - blur_extent; 753 | + shadow_frame->dest_rect.y = 754 | + pad->ypos + pad->y_offset + shadow_frame->shadow.y - 755 | + shadow_frame->shadow.spread - blur_extent; 756 | + 757 | + gint total_blur_extent = 2 * blur_extent; 758 | + shadow_frame->dest_rect.w += total_blur_extent; 759 | + shadow_frame->dest_rect.h += total_blur_extent; 760 | + 761 | + gsize size = 4 * shadow_frame->dest_rect.w * shadow_frame->dest_rect.h; 762 | + guint8 *data = g_malloc0 (size); 763 | + GstBuffer *buffer = gst_buffer_new_wrapped (data, size); 764 | + 765 | + GstVideoInfo video_info; 766 | +#if G_BYTE_ORDER == G_LITTLE_ENDIAN 767 | + GstVideoFormat video_format = GST_VIDEO_FORMAT_BGRA; 768 | +#else 769 | + GstVideoFormat video_format = GST_VIDEO_FORMAT_ARGB; 770 | +#endif 771 | + if (gst_video_info_set_format (&video_info, video_format, 772 | + shadow_frame->dest_rect.w, shadow_frame->dest_rect.h) 773 | + && gst_video_frame_map (&shadow_frame->frame, &video_info, buffer, 774 | + GST_MAP_READWRITE)) { 775 | + info->box_shadows_len++; 776 | + } 777 | + 778 | + gst_buffer_unref (buffer); 779 | + } 780 | + 781 | + g_array_unref (box_shadows); 782 | + } 783 | + 784 | + return TRUE; 785 | +} 786 | + 787 | +typedef struct 788 | { 789 | GstCompositor *compositor; 790 | GstVideoFrame *out_frame; 791 | @@ -1687,12 +2101,12 @@ struct CompositeTask 792 | guint dst_line_end; 793 | gboolean draw_background; 794 | guint n_pads; 795 | - struct CompositePadInfo *pads_info; 796 | -}; 797 | + CompositePadInfo *pads_info; 798 | +} CompositeTask; 799 | 800 | static void 801 | -_draw_background (GstCompositor * comp, GstVideoFrame * outframe, 802 | - guint y_start, guint y_end, BlendFunction * composite) 803 | +_draw_background (GstCompositor *comp, GstVideoFrame *outframe, 804 | + guint y_start, guint y_end, BlendFunction *composite) 805 | { 806 | *composite = comp->blend; 807 | 808 | @@ -1750,96 +2164,83 @@ _draw_background (GstCompositor * comp, GstVideoFrame * outframe, 809 | } 810 | 811 | static void 812 | -blend_pads (struct CompositeTask *comp) 813 | +cut_off_rounded_corners (const GstVideoRectangle *visible_rect, 814 | + const guint *border_radius, GstVideoFrame *frame) 815 | { 816 | - BlendFunction composite; 817 | - guint i; 818 | - 819 | - composite = comp->compositor->blend; 820 | - 821 | - if (comp->draw_background) { 822 | - _draw_background (comp->compositor, comp->out_frame, comp->dst_line_start, 823 | - comp->dst_line_end, &composite); 824 | - } 825 | - 826 | - for (i = 0; i < comp->n_pads; i++) { 827 | - composite (comp->pads_info[i].prepared_frame, 828 | - comp->pads_info[i].pad->xpos + comp->pads_info[i].pad->x_offset, 829 | - comp->pads_info[i].pad->ypos + comp->pads_info[i].pad->y_offset, 830 | - comp->pads_info[i].pad->alpha, comp->out_frame, comp->dst_line_start, 831 | - comp->dst_line_end, comp->pads_info[i].blend_mode); 832 | - } 833 | -} 834 | - 835 | -static void 836 | -cut_off_rounded_corners (GstCompositorPad * pad, GstVideoFrame * frame) 837 | -{ 838 | - // All color formats with an alpha component, currently composed at this 839 | - // stage, have common characteristics: 840 | - // - the alpha component depth is always 8 or 16 bits 841 | - // - in planar mode, the alpha plane has the same width and height as the Y 842 | - // plane (no subsampling) 843 | - guint alpha_comp_depth = GST_VIDEO_FRAME_COMP_DEPTH (frame, 3); 844 | - if ((alpha_comp_depth != 8 && alpha_comp_depth != 16) 845 | - || frame->info.finfo->w_sub[3] != 0 || frame->info.finfo->h_sub[3] != 0) { 846 | - GST_FIXME_OBJECT (pad, "Unexpected source frame video format %s", 847 | - gst_video_format_to_string (frame->info.finfo->format)); 848 | - return; 849 | - } 850 | - 851 | - guint max_border_radius = MIN (MAX (0, (frame->info.width - 1) >> 1), MAX (0, 852 | - (frame->info.height - 1) >> 1)); 853 | - 854 | - guint border_radius[4] = { 855 | - MIN (max_border_radius, pad->border_radius.top_left), 856 | - MIN (max_border_radius, pad->border_radius.top_right), 857 | - MIN (max_border_radius, pad->border_radius.bottom_left), 858 | - MIN (max_border_radius, pad->border_radius.bottom_right) 859 | - }; 860 | - 861 | + gboolean use_16bits_per_component = 862 | + (GST_VIDEO_FRAME_COMP_DEPTH (frame, 3) == 16); 863 | guint8 *alpha_data = GST_VIDEO_FRAME_COMP_DATA (frame, 3); 864 | guint alpha_comp_stride = GST_VIDEO_FRAME_COMP_PSTRIDE (frame, 3); 865 | guint line_stride = GST_VIDEO_FRAME_COMP_STRIDE (frame, 3); 866 | 867 | + g_assert (visible_rect->x >= 0); 868 | + g_assert (visible_rect->y >= 0); 869 | + g_assert (visible_rect->w > 0); 870 | + g_assert (visible_rect->h > 0); 871 | + 872 | + guint last_visible_line = visible_rect->y + visible_rect->h - 1; 873 | + guint last_visible_col = visible_rect->x + visible_rect->w - 1; 874 | + 875 | for (gint i = 0; i < 4; ++i) { 876 | guint radius = border_radius[i]; 877 | if (radius > 0) { 878 | - guint radius2 = radius * radius; 879 | + guint first_line, last_line; 880 | + if (i < 2) { 881 | + // Top corners 882 | + first_line = visible_rect->y; 883 | + last_line = MIN (radius, last_visible_line); 884 | + } else { 885 | + // Bottom corners 886 | + first_line = MAX (frame->info.height - 1 - radius, visible_rect->y); 887 | + last_line = last_visible_line; 888 | + } 889 | 890 | - for (guint y = 0; y <= radius; ++y) { 891 | - guint dy = radius - y; 892 | - guint dy2 = dy * dy; 893 | + guint first_col, last_col; 894 | + if (i % 2) { 895 | + // Right corners 896 | + first_col = MAX (frame->info.width - 1 - radius, visible_rect->x); 897 | + last_col = last_visible_col; 898 | + } else { 899 | + // Left corners 900 | + first_col = visible_rect->x; 901 | + last_col = MIN (radius, last_visible_col); 902 | + } 903 | + 904 | + guint radius2 = radius * radius; 905 | 906 | - guint8 *line = alpha_data; 907 | + for (guint y = first_line; y <= last_line; ++y) { 908 | + guint dy; 909 | if (i < 2) { 910 | - // Top 911 | - line += y * line_stride; 912 | + // Top corners 913 | + dy = radius - y; 914 | } else { 915 | - // Bottom 916 | - line += (frame->info.height - 1 - y) * line_stride; 917 | + // Bottom corners 918 | + dy = radius - (frame->info.height - 1 - y); 919 | } 920 | 921 | - for (guint x = 0; x <= radius; ++x) { 922 | - guint dx = radius - x; 923 | + guint dy2 = dy * dy; 924 | + guint8 *line = alpha_data + y * line_stride; 925 | + 926 | + for (guint x = first_col; x <= last_col; ++x) { 927 | + guint dx; 928 | + if (i % 2) { 929 | + // Right corners 930 | + dx = radius - (frame->info.width - 1 - x); 931 | + } else { 932 | + // Left corners 933 | + dx = radius - x; 934 | + } 935 | + 936 | float dist = dx * dx + dy2; 937 | 938 | if (dist >= radius2) { 939 | // The falloff function has a width of 2 pixels, so we add 1 pixel 940 | - // to the distance to correctly center the arc rasterisation. 941 | + // to the distance to correctly center the arc rasterization. 942 | dist = sqrt (dist) - radius + 1; 943 | float coeff = CLAMP (1.f - dist * dist * dist / 8.f, 0.f, 1.f); 944 | 945 | - guint8 *component; 946 | - if (i % 2) { 947 | - // Right 948 | - component = 949 | - line + alpha_comp_stride * (frame->info.width - 1 - x); 950 | - } else { 951 | - // Left 952 | - component = line + alpha_comp_stride * x; 953 | - } 954 | - 955 | - if (alpha_comp_depth == 16) { 956 | + guint8 *component = line + x * alpha_comp_stride; 957 | + if (use_16bits_per_component) { 958 | *(guint16 *) component *= coeff; 959 | } else { 960 | *component *= coeff; 961 | @@ -1851,15 +2252,236 @@ cut_off_rounded_corners (GstCompositorPad * pad, GstVideoFrame * frame) 962 | } 963 | } 964 | 965 | +static float 966 | +compute_rounded_corner_square_dist (guint x, guint y, guint width, guint height, 967 | + guint radius, gint corner_index) 968 | +{ 969 | + guint dy; 970 | + if (corner_index < 2) { 971 | + // Top corners 972 | + if (y > radius) { 973 | + return -1.f; 974 | + } 975 | + dy = radius - y; 976 | + } else { 977 | + // Bottom corners 978 | + if (y < height - 1 - radius) { 979 | + return -1.f; 980 | + } 981 | + dy = radius - (height - 1 - y); 982 | + } 983 | + 984 | + guint dx; 985 | + if (corner_index % 2) { 986 | + // Right corners 987 | + if (x < width - 1 - radius) { 988 | + return -1.f; 989 | + } 990 | + dx = radius - (width - 1 - x); 991 | + } else { 992 | + // Left corners 993 | + if (x > radius) { 994 | + return -1.f; 995 | + } 996 | + dx = radius - x; 997 | + } 998 | + 999 | + return dx * dx + dy * dy; 1000 | +} 1001 | + 1002 | +static float 1003 | +compute_box_shadow_alpha_coeff (guint x, guint y, 1004 | + const BoxShadowFrame *shadow, float blur_coeff, const guint *border_radius) 1005 | +{ 1006 | + float alpha_coeff = 1.f; 1007 | + guint blur_extent = shadow->shadow.blur << 1; 1008 | + guint blur_falloff_length = blur_extent << 1; 1009 | + gboolean is_at_corner = FALSE; 1010 | + 1011 | + for (gint i = 0; i < 4; ++i) { 1012 | + guint radius = border_radius[i]; 1013 | + if (radius > MAX (0, -shadow->shadow.spread)) { 1014 | + radius += shadow->shadow.spread; 1015 | + 1016 | + if (shadow->shadow.blur > 0) { 1017 | + radius += blur_extent; 1018 | + if (radius <= blur_falloff_length) { 1019 | + continue; 1020 | + } 1021 | + } 1022 | + 1023 | + float dist = 1024 | + compute_rounded_corner_square_dist (x, y, shadow->dest_rect.w, 1025 | + shadow->dest_rect.h, radius, i); 1026 | + if (dist < 0.f) { 1027 | + continue; 1028 | + } 1029 | + 1030 | + if (shadow->shadow.blur > 0) { 1031 | + guint blur_start_radius = radius - blur_falloff_length; 1032 | + if (dist >= blur_start_radius * blur_start_radius) { 1033 | + dist = sqrt (dist) - blur_start_radius + 1; 1034 | + alpha_coeff *= exp (-dist * dist * dist / blur_coeff); 1035 | + is_at_corner = TRUE; 1036 | + } 1037 | + } else if (dist >= radius * radius) { 1038 | + // The falloff function has a width of 2 pixels, so we add 1 pixel 1039 | + // to the distance to correctly center the arc rasterization. 1040 | + dist = sqrt (dist) - radius + 1; 1041 | + alpha_coeff *= CLAMP (1.f - dist * dist * dist / 8.f, 0.f, 1.f); 1042 | + is_at_corner = TRUE; 1043 | + } 1044 | + } 1045 | + } 1046 | + 1047 | + if (!is_at_corner && shadow->shadow.blur > 0) { 1048 | + float dist = 0.f; 1049 | + if (x < blur_falloff_length) { 1050 | + dist = blur_falloff_length - x; 1051 | + } else if (x + blur_falloff_length >= shadow->dest_rect.w) { 1052 | + dist = x - (shadow->dest_rect.w - 1 - blur_falloff_length); 1053 | + } 1054 | + if (dist > 0.f) { 1055 | + alpha_coeff *= exp (-dist * dist * dist / blur_coeff); 1056 | + } 1057 | + 1058 | + dist = 0.f; 1059 | + if (y < blur_falloff_length) { 1060 | + dist = blur_falloff_length - y; 1061 | + } else if (y + blur_falloff_length >= shadow->dest_rect.h) { 1062 | + dist = y - (shadow->dest_rect.h - 1 - blur_falloff_length); 1063 | + } 1064 | + if (dist > 0.f) { 1065 | + alpha_coeff *= exp (-dist * dist * dist / blur_coeff); 1066 | + } 1067 | + } 1068 | + 1069 | + return alpha_coeff; 1070 | +} 1071 | + 1072 | +static void 1073 | +fill_shadow_frame (const GstVideoRectangle *visible_rect, 1074 | + const BoxShadowFrame *shadow, const CompositePadInfo *info) 1075 | +{ 1076 | + g_assert (visible_rect->x >= 0); 1077 | + g_assert (visible_rect->y >= 0); 1078 | + g_assert (visible_rect->w > 0); 1079 | + g_assert (visible_rect->h > 0); 1080 | + g_assert (shadow->dest_rect.w > 0); 1081 | + g_assert (shadow->dest_rect.h > 0); 1082 | + 1083 | + guint8 *image_data = GST_VIDEO_FRAME_PLANE_DATA (&shadow->frame, 0); 1084 | + guint line_stride = GST_VIDEO_FRAME_PLANE_STRIDE (&shadow->frame, 0); 1085 | + guint alpha_comp_offset = GST_VIDEO_FRAME_COMP_POFFSET (&shadow->frame, 3); 1086 | + 1087 | + guint last_visible_line = visible_rect->y + visible_rect->h - 1; 1088 | + guint last_visible_col = visible_rect->x + visible_rect->w - 1; 1089 | + 1090 | + gboolean modify_alpha = (shadow->shadow.blur > 0 1091 | + || info->has_rounded_corners); 1092 | + 1093 | + float blur_coeff = 1.f; 1094 | + if (shadow->shadow.blur > 0) { 1095 | + // To optimize the rendering, the shadow blur effect is not a real gaussian 1096 | + // blur but an approximation of the blur result as it looks in a web browser 1097 | + // when using the CSS box-shadow property. 1098 | + // The approximated equation used to compute the blurred pixel alpha coeff 1099 | + // is: alpha(d) = exp(- d.d.d / blur_coeff) with d the distance between the 1100 | + // pixel and the closest non-blurred pixel. 1101 | + blur_coeff = 1102 | + 12 * shadow->shadow.blur * shadow->shadow.blur * shadow->shadow.blur + 1103 | + 8; 1104 | + } 1105 | + 1106 | + for (guint y = visible_rect->y; y <= last_visible_line; ++y) { 1107 | + guint8 *line = image_data + y * line_stride; 1108 | + 1109 | + for (guint x = visible_rect->x; x <= last_visible_col; ++x) { 1110 | + guint32 *pixel = (guint32 *) line + x; 1111 | + *pixel = shadow->shadow.color; 1112 | + 1113 | + if (modify_alpha) { 1114 | + float coeff = compute_box_shadow_alpha_coeff (x, y, shadow, blur_coeff, 1115 | + info->border_radius); 1116 | + *((guint8 *) pixel + alpha_comp_offset) *= coeff; 1117 | + } 1118 | + } 1119 | + } 1120 | +} 1121 | + 1122 | +static gboolean 1123 | +has_visible_src_rect (GstVideoRectangle *visible_src_rect_out, 1124 | + const GstVideoRectangle *dest_rect, const CompositeTask *task) 1125 | +{ 1126 | + visible_src_rect_out->x = MAX (0, -dest_rect->x); 1127 | + visible_src_rect_out->y = MAX (0, (gint) task->dst_line_start - dest_rect->y); 1128 | + visible_src_rect_out->w = 1129 | + MIN (dest_rect->w, task->out_frame->info.width - dest_rect->x); 1130 | + visible_src_rect_out->h = 1131 | + MIN (dest_rect->h, (gint) task->dst_line_end - dest_rect->y); 1132 | + 1133 | + visible_src_rect_out->w -= visible_src_rect_out->x; 1134 | + visible_src_rect_out->h -= visible_src_rect_out->y; 1135 | + 1136 | + return (visible_src_rect_out->w > 0 && visible_src_rect_out->h > 0); 1137 | +} 1138 | + 1139 | +static void 1140 | +blend_pads (CompositeTask *comp) 1141 | +{ 1142 | + BlendFunction composite = comp->compositor->blend; 1143 | + 1144 | + // Draw background 1145 | + if (comp->draw_background) { 1146 | + _draw_background (comp->compositor, comp->out_frame, comp->dst_line_start, 1147 | + comp->dst_line_end, &composite); 1148 | + } 1149 | + // Compose all layers above the background 1150 | + GstVideoRectangle visible_src_rect; 1151 | + for (guint i = 0; i < comp->n_pads; ++i) { 1152 | + const CompositePadInfo *info = comp->pads_info + i; 1153 | + 1154 | + const GstVideoRectangle dest_rect = { 1155 | + info->pad->xpos + info->pad->x_offset, 1156 | + info->pad->ypos + info->pad->y_offset, 1157 | + info->prepared_frame->info.width, 1158 | + info->prepared_frame->info.height 1159 | + }; 1160 | + 1161 | + // Cut-off rounded corners from the input pad frame 1162 | + if (info->has_rounded_corners) { 1163 | + if (has_visible_src_rect (&visible_src_rect, &dest_rect, comp)) { 1164 | + cut_off_rounded_corners (&visible_src_rect, info->border_radius, 1165 | + info->prepared_frame); 1166 | + } 1167 | + } 1168 | + // Compute and compose the box shadows 1169 | + for (guint j = 0; j < info->box_shadows_len; ++j) { 1170 | + BoxShadowFrame *shadow = info->box_shadows + j; 1171 | + if (has_visible_src_rect (&visible_src_rect, &shadow->dest_rect, comp)) { 1172 | + fill_shadow_frame (&visible_src_rect, shadow, info); 1173 | + composite (&shadow->frame, shadow->dest_rect.x, shadow->dest_rect.y, 1174 | + info->pad->alpha, comp->out_frame, comp->dst_line_start, 1175 | + comp->dst_line_end, COMPOSITOR_BLEND_MODE_OVER); 1176 | + } 1177 | + } 1178 | + 1179 | + // Compose the input pad frame itself 1180 | + composite (info->prepared_frame, dest_rect.x, dest_rect.y, info->pad->alpha, 1181 | + comp->out_frame, comp->dst_line_start, comp->dst_line_end, 1182 | + info->blend_mode); 1183 | + } 1184 | +} 1185 | + 1186 | static GstFlowReturn 1187 | -gst_compositor_aggregate_frames (GstVideoAggregator * vagg, GstBuffer * outbuf) 1188 | +gst_compositor_aggregate_frames (GstVideoAggregator *vagg, GstBuffer *outbuf) 1189 | { 1190 | GstCompositor *compositor = GST_COMPOSITOR (vagg); 1191 | GList *l; 1192 | GstVideoFrame out_frame, intermediate_frame, *outframe; 1193 | gboolean draw_background; 1194 | guint drawn_a_pad = FALSE; 1195 | - struct CompositePadInfo *pads_info; 1196 | + CompositePadInfo *pads_info; 1197 | guint i, n_pads = 0; 1198 | 1199 | if (!gst_video_frame_map (&out_frame, &vagg->info, outbuf, GST_MAP_WRITE)) { 1200 | @@ -1902,52 +2524,25 @@ gst_compositor_aggregate_frames (GstVideoAggregator * vagg, GstBuffer * outbuf) 1201 | if (n_pads == 0) 1202 | draw_background = TRUE; 1203 | 1204 | - pads_info = g_newa (struct CompositePadInfo, n_pads); 1205 | + pads_info = g_new0 (CompositePadInfo, n_pads + 1); 1206 | n_pads = 0; 1207 | 1208 | for (l = GST_ELEMENT (vagg)->sinkpads; l; l = l->next) { 1209 | - GstVideoAggregatorPad *pad = l->data; 1210 | - GstCompositorPad *compo_pad = GST_COMPOSITOR_PAD (pad); 1211 | - GstVideoFrame *prepared_frame = 1212 | - gst_video_aggregator_pad_get_prepared_frame (pad); 1213 | - GstCompositorBlendMode blend_mode = COMPOSITOR_BLEND_MODE_OVER; 1214 | - 1215 | - switch (compo_pad->op) { 1216 | - case COMPOSITOR_OPERATOR_SOURCE: 1217 | - blend_mode = COMPOSITOR_BLEND_MODE_SOURCE; 1218 | - break; 1219 | - case COMPOSITOR_OPERATOR_OVER: 1220 | - blend_mode = COMPOSITOR_BLEND_MODE_OVER; 1221 | - break; 1222 | - case COMPOSITOR_OPERATOR_ADD: 1223 | - blend_mode = COMPOSITOR_BLEND_MODE_ADD; 1224 | - break; 1225 | - default: 1226 | - g_assert_not_reached (); 1227 | - break; 1228 | - } 1229 | + GstCompositorPad *compo_pad = GST_COMPOSITOR_PAD (l->data); 1230 | + CompositePadInfo *compo_info = pads_info + n_pads; 1231 | 1232 | - if (prepared_frame != NULL) { 1233 | + if (extract_composite_pad_info (compo_pad, compo_info)) { 1234 | /* If this is the first pad we're drawing, and we didn't draw the 1235 | - * background, and @prepared_frame has the same format, height, and width 1236 | - * as @outframe, then we can just copy it as-is. Subsequent pads (if any) 1237 | - * will be composited on top of it. */ 1238 | + * background, and there is neither rounded corners nor shadows, and 1239 | + * @prepared_frame has the same format, height, and width as @outframe, 1240 | + * then we can just copy it as-is. Subsequent pads (if any) will be 1241 | + * composited on top of it. */ 1242 | if (!drawn_a_pad && !draw_background && 1243 | - frames_can_copy (prepared_frame, outframe)) { 1244 | - gst_video_frame_copy (outframe, prepared_frame); 1245 | + !compo_info->has_rounded_corners && 1246 | + !compo_info->box_shadows && 1247 | + frames_can_copy (compo_info->prepared_frame, outframe)) { 1248 | + gst_video_frame_copy (outframe, compo_info->prepared_frame); 1249 | } else { 1250 | - if (compo_pad->border_radius.has_rounded_corners 1251 | - && GST_VIDEO_INFO_HAS_ALPHA (&prepared_frame->info)) { 1252 | - if (blend_mode == COMPOSITOR_BLEND_MODE_SOURCE) { 1253 | - blend_mode = COMPOSITOR_BLEND_MODE_OVER; 1254 | - } 1255 | - 1256 | - cut_off_rounded_corners (compo_pad, prepared_frame); 1257 | - } 1258 | - 1259 | - pads_info[n_pads].pad = compo_pad; 1260 | - pads_info[n_pads].prepared_frame = prepared_frame; 1261 | - pads_info[n_pads].blend_mode = blend_mode; 1262 | n_pads++; 1263 | } 1264 | drawn_a_pad = TRUE; 1265 | @@ -1957,13 +2552,13 @@ gst_compositor_aggregate_frames (GstVideoAggregator * vagg, GstBuffer * outbuf) 1266 | { 1267 | guint n_threads, lines_per_thread; 1268 | guint out_height; 1269 | - struct CompositeTask *tasks; 1270 | - struct CompositeTask **tasks_p; 1271 | + CompositeTask *tasks; 1272 | + CompositeTask **tasks_p; 1273 | 1274 | n_threads = compositor->blend_runner->n_threads; 1275 | 1276 | - tasks = g_newa (struct CompositeTask, n_threads); 1277 | - tasks_p = g_newa (struct CompositeTask *, n_threads); 1278 | + tasks = g_newa (CompositeTask, n_threads); 1279 | + tasks_p = g_newa (CompositeTask *, n_threads); 1280 | 1281 | out_height = GST_VIDEO_FRAME_HEIGHT (outframe); 1282 | lines_per_thread = (out_height + n_threads - 1) / n_threads; 1283 | @@ -1988,6 +2583,16 @@ gst_compositor_aggregate_frames (GstVideoAggregator * vagg, GstBuffer * outbuf) 1284 | (GstParallelizedTaskFunc) blend_pads, (gpointer *) tasks_p); 1285 | } 1286 | 1287 | + for (guint i = 0; i < n_pads; ++i) { 1288 | + CompositePadInfo *pad_info = pads_info + i; 1289 | + 1290 | + for (guint j = 0; j < pad_info->box_shadows_len; ++j) { 1291 | + gst_video_frame_unmap (&pad_info->box_shadows[j].frame); 1292 | + } 1293 | + g_free (pad_info->box_shadows); 1294 | + } 1295 | + g_free (pads_info); 1296 | + 1297 | GST_OBJECT_UNLOCK (vagg); 1298 | 1299 | if (compositor->intermediate_frame) { 1300 | @@ -2003,8 +2608,8 @@ gst_compositor_aggregate_frames (GstVideoAggregator * vagg, GstBuffer * outbuf) 1301 | } 1302 | 1303 | static GstPad * 1304 | -gst_compositor_request_new_pad (GstElement * element, GstPadTemplate * templ, 1305 | - const gchar * req_name, const GstCaps * caps) 1306 | +gst_compositor_request_new_pad (GstElement *element, GstPadTemplate *templ, 1307 | + const gchar *req_name, const GstCaps *caps) 1308 | { 1309 | GstPad *newpad; 1310 | 1311 | @@ -2028,7 +2633,7 @@ could_not_create: 1312 | } 1313 | 1314 | static void 1315 | -gst_compositor_release_pad (GstElement * element, GstPad * pad) 1316 | +gst_compositor_release_pad (GstElement *element, GstPad *pad) 1317 | { 1318 | GstCompositor *compositor; 1319 | 1320 | @@ -2043,7 +2648,7 @@ gst_compositor_release_pad (GstElement * element, GstPad * pad) 1321 | } 1322 | 1323 | static gboolean 1324 | -src_pad_mouse_event (GstElement * element, GstPad * pad, gpointer user_data) 1325 | +src_pad_mouse_event (GstElement *element, GstPad *pad, gpointer user_data) 1326 | { 1327 | GstVideoAggregator *vagg = GST_VIDEO_AGGREGATOR_CAST (element); 1328 | GstCompositor *comp = GST_COMPOSITOR (element); 1329 | @@ -2086,7 +2691,7 @@ src_pad_mouse_event (GstElement * element, GstPad * pad, gpointer user_data) 1330 | } 1331 | 1332 | static gboolean 1333 | -_src_event (GstAggregator * agg, GstEvent * event) 1334 | +_src_event (GstAggregator *agg, GstEvent *event) 1335 | { 1336 | GstNavigationEventType event_type; 1337 | 1338 | @@ -2116,7 +2721,7 @@ _src_event (GstAggregator * agg, GstEvent * event) 1339 | } 1340 | 1341 | static gboolean 1342 | -_sink_query (GstAggregator * agg, GstAggregatorPad * bpad, GstQuery * query) 1343 | +_sink_query (GstAggregator *agg, GstAggregatorPad *bpad, GstQuery *query) 1344 | { 1345 | switch (GST_QUERY_TYPE (query)) { 1346 | case GST_QUERY_ALLOCATION:{ 1347 | @@ -2158,7 +2763,7 @@ _sink_query (GstAggregator * agg, GstAggregatorPad * bpad, GstQuery * query) 1348 | } 1349 | 1350 | static void 1351 | -gst_compositor_finalize (GObject * object) 1352 | +gst_compositor_finalize (GObject *object) 1353 | { 1354 | GstCompositor *compositor = GST_COMPOSITOR (object); 1355 | 1356 | @@ -2171,7 +2776,7 @@ gst_compositor_finalize (GObject * object) 1357 | 1358 | /* GObject boilerplate */ 1359 | static void 1360 | -gst_compositor_class_init (GstCompositorClass * klass) 1361 | +gst_compositor_class_init (GstCompositorClass *klass) 1362 | { 1363 | GObjectClass *gobject_class = (GObjectClass *) klass; 1364 | GstElementClass *gstelement_class = (GstElementClass *) klass; 1365 | @@ -2265,7 +2870,7 @@ gst_compositor_class_init (GstCompositorClass * klass) 1366 | } 1367 | 1368 | static void 1369 | -gst_compositor_init (GstCompositor * self) 1370 | +gst_compositor_init (GstCompositor *self) 1371 | { 1372 | /* initialize variables */ 1373 | self->background = DEFAULT_BACKGROUND; 1374 | @@ -2275,7 +2880,7 @@ gst_compositor_init (GstCompositor * self) 1375 | 1376 | /* GstChildProxy implementation */ 1377 | static GObject * 1378 | -gst_compositor_child_proxy_get_child_by_index (GstChildProxy * child_proxy, 1379 | +gst_compositor_child_proxy_get_child_by_index (GstChildProxy *child_proxy, 1380 | guint index) 1381 | { 1382 | GstCompositor *compositor = GST_COMPOSITOR (child_proxy); 1383 | @@ -2291,7 +2896,7 @@ gst_compositor_child_proxy_get_child_by_index (GstChildProxy * child_proxy, 1384 | } 1385 | 1386 | static guint 1387 | -gst_compositor_child_proxy_get_children_count (GstChildProxy * child_proxy) 1388 | +gst_compositor_child_proxy_get_children_count (GstChildProxy *child_proxy) 1389 | { 1390 | guint count = 0; 1391 | GstCompositor *compositor = GST_COMPOSITOR (child_proxy); 1392 | @@ -2315,7 +2920,7 @@ gst_compositor_child_proxy_init (gpointer g_iface, gpointer iface_data) 1393 | 1394 | /* Element registration */ 1395 | static gboolean 1396 | -plugin_init (GstPlugin * plugin) 1397 | +plugin_init (GstPlugin *plugin) 1398 | { 1399 | GST_DEBUG_CATEGORY_INIT (gst_compositor_debug, "compositor", 0, "compositor"); 1400 | 1401 | diff --git a/subprojects/gst-plugins-base/gst/compositor/compositor.h b/subprojects/gst-plugins-base/gst/compositor/compositor.h 1402 | index 1b48c1b035..72185e5a4c 100644 1403 | --- a/subprojects/gst-plugins-base/gst/compositor/compositor.h 1404 | +++ b/subprojects/gst-plugins-base/gst/compositor/compositor.h 1405 | @@ -177,6 +177,7 @@ struct _GstCompositorPad 1406 | guint bottom_left; 1407 | gboolean has_rounded_corners; 1408 | } border_radius; 1409 | + GArray *box_shadows; 1410 | 1411 | GstCompositorOperator op; 1412 | 1413 | -- 1414 | 2.43.0 1415 | 1416 | --------------------------------------------------------------------------------