Compare commits

..

42 commits
main ... 0.17

Author SHA1 Message Date
Sebastian Dröge
6c9815f059 gstreamer: Fix -sys dependency version 2021-09-13 12:16:58 +03:00
Sebastian Dröge
0b7b8bd60f Update versions to 0.17.4 2021-09-13 12:04:11 +03:00
Sebastian Dröge
ad0dbadc74 Update CHANGELOG.md for 0.17.4 2021-09-13 12:03:48 +03:00
Sebastian Dröge
8deac14876 gstreamer/device_provider: Add constructor for device provider metadata 2021-09-13 11:36:12 +03:00
Sebastian Dröge
3b40b8e2d5 Update dependencies 2021-08-30 12:49:43 +03:00
Sebastian Dröge
e009fd0a4c Require gstreamer-sys 0.17.3 for the fixed tracer version bound 2021-08-23 09:10:10 +03:00
Sebastian Dröge
9f5c9adadf Update version numbers for 0.17.3 2021-08-23 09:10:07 +03:00
Sebastian Dröge
69eecdcc83 Update CHANGELOG.md for 0.17.3 2021-08-23 08:55:30 +03:00
Jordan Petridis
c5771f0373 tracer: add rustdoc-stripper-ignore-next annotations 2021-08-22 22:21:11 +03:00
Sebastian Dröge
1ae8b0d4a2 gstreamer: Add Value::deserialize_with_pspec() from 1.20 2021-08-22 21:57:19 +03:00
Sebastian Dröge
dd4330f899 gstreamer: Value::deserialize() needs to take the target type as parameter
Otherwise it will always fail.
2021-08-22 21:57:13 +03:00
Edward Hervey
562c8563ce gstreamer: Add serde support for more enums and flags
PadDirection, PadPresence, URITYpe, Rank
2021-08-22 21:57:05 +03:00
Simonas Kazlauskas
e1f197c91c Bindings to GstTracer and GstTracerFactory
Fixes https://gitlab.freedesktop.org/gstreamer/gstreamer-rs/-/issues/147
2021-08-22 21:56:48 +03:00
Simonas Kazlauskas
6cf9b9a3d3 Make plugin date-time optional
It is described as optional in
https://gstreamer.freedesktop.org/documentation/gstreamer/gstplugin.html?gi-language=c#GstPluginDesc
and is also pretty difficult to provide for Rust plugins, I feel. For C
stuff the expectation is that build system would set something like
GST_PACKAGE_RELEASE_DATETIME=`date -u "+%Y-%m-%dT%H:%MZ"` which is
terrible.
2021-08-22 21:56:42 +03:00
Sebastian Dröge
35d2a2852b Update Cargo.lock 2021-08-05 18:01:05 +03:00
Sebastian Dröge
52b5f30b29 Update versions to 0.17.2 2021-08-05 16:46:10 +03:00
Sebastian Dröge
0fb2f605b6 Update CHANGELOG.md for 0.17.2 2021-08-05 16:46:03 +03:00
Vivia Nikolaidou
7868b700ee structure, caps: Implement 1.20 serialize functions 2021-08-05 16:08:54 +03:00
Vivia Nikolaidou
6c46db6e61 event: Implement gap_flags 2021-08-05 16:08:49 +03:00
Guillaume Gomez
e3ad81d473 Fix (new) clippy warnings 2021-08-05 14:36:36 +03:00
Fabio Valentini
566cfb879b gstreamer-audio: fix audio_format code for big-endian targets 2021-08-05 14:34:25 +03:00
Sebastian Dröge
5189a4d175 Fix various needless-borrow clippy warnings 2021-08-05 14:33:49 +03:00
Sebastian Dröge
3fc45a9254 base: Fix new clippy warning
error: statement can be reduced
  --> gstreamer-base/src/flow_combiner.rs:6:1
   |
6  | / glib::wrapper! {
7  | |     #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
8  | |     #[doc(alias = "GstFlowCombiner")]
9  | |     pub struct FlowCombiner(Shared<ffi::GstFlowCombiner>);
...  |
31 | |     }
32 | | }
   | |_^
2021-08-05 14:33:41 +03:00
Sebastian Dröge
c1905f2f24 Add Cargo.lock file for the stable branch 2021-08-05 14:01:09 +03:00
Sebastian Dröge
061f85d410 examples/tutorials: Use cocoa crate to initialize the shared NSApplication instance
This is required for OpenGL to work nowadays on macOS. Simply running an
CFRunLoop on the main thread is not sufficient.

Thanks to Philippe Normand for testing this on macOS and making sure it
actually compiles and works.
2021-07-13 11:06:24 +03:00
Sebastian Dröge
3b03ab0660 Update CHANGELOG.md for 0.17.1 2021-07-13 09:17:06 +03:00
Sebastian Dröge
e535924367 Update versions to 0.17.1 2021-07-13 09:15:11 +03:00
Olivier Crête
810ff23bed subclass: Declare minimum to be 1.19 for v1_20 2021-07-13 09:02:00 +03:00
Sebastian Dröge
c44fde7279 gstreamer/element: Include ElementExtManual::request_pad_simple() in the docs too
Currently only the v1_20 ElementExt::request_pad_simple() appears in the
docs but it's available from the other trait in older versions.
2021-06-30 13:27:47 +03:00
François Laignel
f33594bdd9 gstreamer-*/CHANGELOG.md use symlinks 2021-06-29 15:46:52 +02:00
François Laignel
9a0be6f1a8 CHANGELOG: mention fix-getters migration tools 2021-06-29 12:50:21 +02:00
Sebastian Dröge
cb526abb5a controller: Add missing README.md 2021-06-28 23:56:26 +03:00
Sebastian Dröge
7de8a64fc0 Update CHANGELOG.md for 0.17.0 2021-06-28 23:36:39 +03:00
Sebastian Dröge
b0c6fd1543 Regenerate with latest gir-files 2021-06-28 19:35:15 +03:00
Sebastian Dröge
b58b9d234c webrtc: Remove unneeded dependencies on gst-sys and gobject-sys 2021-06-28 19:35:01 +03:00
Sebastian Dröge
223a12597c webrtc: Fix manual data channel code
The corresponding API shouldn't have been public and is not public
anymore with 1.20.
2021-06-28 19:34:25 +03:00
Sebastian Dröge
72da9ea826 webrtc: Fix minimal version of WebRTCRTPSender 2021-06-28 19:34:25 +03:00
Sebastian Dröge
864072220b gstreamer: Mark all kinds of miniobject memory management convenience functions as existing since 1.18.3
Previously they were only available as inline functions in the headers.
2021-06-28 19:34:25 +03:00
Sebastian Dröge
11d04ad7b6 Update gst-gir-files 2021-06-28 19:34:25 +03:00
Prakash Duggaraju
ecf9a43692 rtsp-server: Add support for subclassing GstRtspMountPoints
Implement MountPointsImpl and MountPointsExt to allow subclassing.
Update the example to show usage.
2021-06-28 10:33:59 +03:00
Sebastian Dröge
2aeaf6fa7a ci: Add 0.17 release for the docs 2021-06-25 09:00:24 +03:00
Sebastian Dröge
e403542a28 Update versions from git to releases 2021-06-25 08:59:54 +03:00
1183 changed files with 48628 additions and 114176 deletions

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
target/
**/*.rs.bk
Cargo.lock

View file

@ -17,7 +17,7 @@
# Updating the nightly image should be done by simply running a scheduled ci
# pipeline on the upstream repo with the $UPDATE_NIGHTLY variable defined.
.templates_sha: &templates_sha b2e24205598dc1d80b5f2c88cf7618051e30e9fd
.templates_sha: &templates_sha 322bf2b8f29b6491caeb13861201e96969ddc169
include:
- project: 'freedesktop/ci-templates'
@ -26,40 +26,18 @@ include:
- local: "ci/images_template.yml"
workflow:
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
- if: $CI_MERGE_REQUEST_IID
# don't create a pipeline if its a commit pipeline, on a branch and that branch has
# open merge requests (bc we will get a MR build instead)
- if: $CI_OPEN_MERGE_REQUESTS
when: never
- if: $CI_COMMIT_TAG
- if: $CI_COMMIT_BRANCH
default:
interruptible: true
variables:
FDO_UPSTREAM_REPO: gstreamer/gstreamer-rs
# DIY CI-templates like setup for windows
WINDOWS_RUST_MINIMUM_IMAGE: "$CI_REGISTRY_IMAGE/windows:$GST_RS_IMG_TAG-main-$GST_RS_MSRV"
WINDOWS_RUST_MINIMUM_UPSTREAM_IMAGE: "$CI_REGISTRY/$FDO_UPSTREAM_REPO/windows:$GST_RS_IMG_TAG-main-$GST_RS_MSRV"
WINDOWS_RUST_STABLE_IMAGE: "$CI_REGISTRY_IMAGE/windows:$GST_RS_IMG_TAG-main-$GST_RS_STABLE"
WINDOWS_RUST_STABLE_UPSTREAM_IMAGE: "$CI_REGISTRY/$FDO_UPSTREAM_REPO/windows:$GST_RS_IMG_TAG-main-$GST_RS_STABLE"
RUST_DOCS_FLAGS: "--cfg docsrs --extern-html-root-url=muldiv=https://docs.rs/muldiv/1.0.0/muldiv/ -Z unstable-options --generate-link-to-definition"
NAMESPACE: gstreamer
RUST_DOCS_FLAGS: "--extern-html-root-url=muldiv=https://docs.rs/muldiv/1.0.0/muldiv/ -Z unstable-options"
# format is <branch>=<name>
# the name is used in the URL
# latest release must be at the top
# (only relevant on main branch)
# (only relevant on master branch)
RELEASES:
0.22=0.22
0.17=0.17
0.16=0.16
stages:
- "trigger"
- "container-base"
- "container-final"
- "lint"
@ -67,56 +45,34 @@ stages:
- "extras"
- "deploy"
# This is an empty job that is used to trigger the pipeline.
trigger:
image: alpine:latest
stage: 'trigger'
variables:
GIT_STRATEGY: none
tags: [ 'placeholder-job' ]
script:
- echo "Trigger job done, now running the pipeline."
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
# If the MR is assigned to the Merge bot, trigger the pipeline automatically
- if: '$CI_MERGE_REQUEST_ASSIGNEES == "gstreamer-merge-bot"'
# Require explicit action to trigger tests post merge
- if: '$CI_PROJECT_NAMESPACE == "gstreamer" && $CI_COMMIT_BRANCH == "main"'
when: 'manual'
# When the assignee isn't the merge bot, require an explicit action to trigger the pipeline
# to avoid wasting CI resources
- if: '$CI_MERGE_REQUEST_ASSIGNEES != "gstreamer-merge-bot"'
when: 'manual'
allow_failure: false
.debian:12:
needs: []
.debian:10:
variables:
FDO_DISTRIBUTION_VERSION: 'bookworm-slim'
FDO_DISTRIBUTION_VERSION: 10
before_script:
- source ./ci/env.sh
- mkdir .cargo && echo -e "[net]\ngit-fetch-with-cli = true" > .cargo/config
.debian:12-base:
extends: .debian:12
.debian:10-base:
extends: .debian:10
variables:
FDO_DISTRIBUTION_TAG: 'base-$GST_RS_IMG_TAG'
.debian:12-stable:
extends: .debian:12
.debian:10-stable:
extends: .debian:10
variables:
RUST_IMAGE_FULL: "1"
FDO_DISTRIBUTION_TAG: '$GST_RS_STABLE-$GST_RS_IMG_TAG'
FDO_DISTRIBUTION_EXEC: 'bash ci/install-rust.sh $GST_RS_STABLE $RUST_IMAGE_FULL'
.debian:12-msrv:
extends: .debian:12
.debian:10-msrv:
extends: .debian:10
variables:
FDO_DISTRIBUTION_TAG: '$GST_RS_MSRV-$GST_RS_IMG_TAG'
FDO_DISTRIBUTION_EXEC: 'bash ci/install-rust.sh $GST_RS_MSRV $RUST_IMAGE_FULL'
.debian:12-nightly:
extends: .debian:12
.debian:10-nightly:
extends: .debian:10
variables:
FDO_DISTRIBUTION_TAG: 'nightly-$GST_RS_IMG_TAG'
FDO_DISTRIBUTION_EXEC: 'bash ci/install-rust.sh nightly $RUST_IMAGE_FULL'
@ -126,63 +82,41 @@ trigger:
- .fdo.container-build@debian
stage: container-base
variables:
FDO_DISTRIBUTION_PACKAGES: >-
build-essential curl python3-setuptools libglib2.0-dev libxml2-dev
libdrm-dev libegl1-mesa-dev libgl1-mesa-dev libgbm-dev libgles2-mesa-dev
libgl1-mesa-dri libegl-dev libgl1-mesa-glx libwayland-egl1-mesa xz-utils
libssl-dev git wget ca-certificates ninja-build python3-pip flex bison
libglib2.0-dev libx11-dev libx11-xcb-dev libsoup2.4-dev libvorbis-dev
libogg-dev libtheora-dev libmatroska-dev libvpx-dev libopus-dev
libgraphene-1.0-dev libjpeg-dev libwayland-dev wayland-protocols
python3-gi libavcodec-dev libavformat-dev libavutil-dev libavfilter-dev
libswscale-dev yasm libx264-dev libfontconfig-dev libfreetype-dev
libxkbcommon-dev libxi-dev libxcb-render0-dev libxcb-shm0-dev
libxcb1-dev libxext-dev libxrender-dev libxrandr-dev libxcursor-dev
libxdamage-dev libxfixes-dev libxinerama-dev libgudev-1.0-dev
libpango1.0-dev libcairo2-dev libjson-glib-dev libgdk-pixbuf-2.0-dev
libtiff-dev libpng-dev libjpeg-dev libepoxy-dev libsass-dev sassc
libcsound64-dev llvm clang nasm libsodium-dev libwebp-dev
libflac-dev
FDO_DISTRIBUTION_EXEC: >-
bash ci/install-gst.sh &&
bash ci/install-dav1d.sh &&
pip3 install --break-system-packages git+http://gitlab.freedesktop.org/freedesktop/ci-templates &&
pip3 install --break-system-packages tomli
FDO_DISTRIBUTION_PACKAGES: "build-essential curl python3-setuptools liborc-0.4-dev libglib2.0-dev libxml2-dev libgtk-3-dev libegl1-mesa libgles2-mesa libgl1-mesa-dri libgl1-mesa-glx libwayland-egl1-mesa xz-utils libssl-dev git wget ca-certificates ninja-build python3-pip flex bison libglib2.0-dev"
FDO_DISTRIBUTION_EXEC: 'bash ci/install-gst.sh && pip3 install git+http://gitlab.freedesktop.org/freedesktop/ci-templates'
.build-final-image:
extends:
- .fdo.container-build@debian
stage: container-final
variables:
FDO_BASE_IMAGE: '$CI_REGISTRY_IMAGE/debian/bookworm-slim:base-$GST_RS_IMG_TAG'
FDO_BASE_IMAGE: '$CI_REGISTRY_IMAGE/debian/10:base-$GST_RS_IMG_TAG'
build-base:
extends:
- .build-base-image
- .debian:12-base
- .debian:10-base
build-stable:
needs: ["build-base"]
extends:
- .build-final-image
- .debian:12-stable
- .debian:10-stable
build-msrv:
needs: ["build-base"]
extends:
- .build-final-image
- .debian:12-msrv
- .debian:10-msrv
build-nightly:
needs: ["build-base"]
extends:
- .build-final-image
- .debian:12-nightly
- .debian:10-nightly
update-nightly:
extends: build-nightly
rules:
- if: $UPDATE_NIGHTLY == "1"
only:
variables:
- $UPDATE_NIGHTLY == "1"
variables:
FDO_FORCE_REBUILD: 1
@ -194,21 +128,103 @@ update-nightly:
.img-stable:
extends:
- .debian:12-stable
- .debian:10-stable
- .dist-debian-container
.img-msrv:
extends:
- .debian:12-msrv
- .debian:10-msrv
- .dist-debian-container
.img-nightly:
extends:
- .debian:12-nightly
- .debian:10-nightly
- .dist-debian-container
# GST_PLUGINS_RS_TOKEN is a variable of type 'Var' defined in gstreamer-rs CI
# settings and containing a gst-plugins-rs pipeline trigger token
.plugins-update:
stage: deploy
script:
- |
# FDO_DISTRIBUTION_IMAGE still has indirections
- echo $FDO_DISTRIBUTION_IMAGE
- DISTRO_IMAGE=$(eval echo ${FDO_DISTRIBUTION_IMAGE})
- echo $DISTRO_IMAGE
# retrieve the infos from the registry
- JSON_IMAGE=$(skopeo inspect docker://$DISTRO_IMAGE)
- IMAGE_PIPELINE_ID=$(echo $JSON_IMAGE | jq -r '.Labels["fdo.pipeline_id"]')
- echo $IMAGE_PIPELINE_ID
- echo $CI_PIPELINE_ID
- |
if [[ x"$IMAGE_PIPELINE_ID" == x"$CI_PIPELINE_ID" ]]; then
echo "Image has been updated, notify gst-plugins-rs"
curl -X POST -F "token=$GST_PLUGINS_RS_TOKEN" -F "ref=master" -F "variables[UPDATE_IMG]=$UPDATE_IMG" https://gitlab.freedesktop.org/api/v4/projects/1400/trigger/pipeline
else
echo "Image has not been updated, ignore"
fi
rules:
- if: '$CI_COMMIT_REF_NAME == "master" && $CI_PROJECT_PATH == "gstreamer/gstreamer-rs"'
# Those jobs need to use another image as ours doesn't have 'skopeo'
# and it's not easily installable in Debian stable for now.
plugins-update-stable:
extends:
- .plugins-update
- .img-stable
image: registry.freedesktop.org/freedesktop/ci-templates/buildah:2020-03-04
variables:
UPDATE_IMG: "stable"
plugins-update-msrv:
extends:
- .plugins-update
- .img-msrv
image: registry.freedesktop.org/freedesktop/ci-templates/buildah:2020-03-04
variables:
UPDATE_IMG: "msrv"
plugins-update-nightly:
extends:
- .plugins-update
- .img-nightly
image: registry.freedesktop.org/freedesktop/ci-templates/buildah:2020-03-04
variables:
UPDATE_IMG: "nightly"
.cargo_test_var: &cargo_test
- ./ci/run-cargo-test.sh
- rustc --version
# First build and test all the crates with their relevant features
# Keep features in sync with below
- |
get_features() {
crate=$1
if [ "$crate" = "gstreamer" ]; then
echo "--features=ser_de,v1_20"
else
echo "--features=v1_20"
fi
}
for crate in gstreamer* gstreamer-gl/{egl,wayland,x11}; do
if [ -e $crate/Cargo.toml ]; then
if [ -n "$ALL_FEATURES" ]; then
FEATURES="$(get_features $crate)"
else
FEATURES=""
fi
echo "Building and testing $crate with $FEATURES"
cargo build --locked --color=always --manifest-path $crate/Cargo.toml $FEATURES
G_DEBUG=fatal_warnings cargo test --color=always --manifest-path $crate/Cargo.toml $FEATURES
fi
done
- |
if [ -n "$EXAMPLES_TUTORIALS" ]; then
cargo build --locked --color=always --manifest-path examples/Cargo.toml --bins --examples --all-features
cargo build --locked --color=always --manifest-path tutorials/Cargo.toml --bins --examples --all-features
fi
.cargo test:
stage: "test"
@ -219,21 +235,11 @@ test msrv:
extends:
- '.cargo test'
- .img-msrv
needs:
- job: 'trigger'
artifacts: false
- job: 'build-msrv'
artifacts: false
test stable:
extends:
- '.cargo test'
- .img-stable
needs:
- job: 'trigger'
artifacts: false
- job: 'build-stable'
artifacts: false
test stable all-features:
variables:
@ -242,24 +248,12 @@ test stable all-features:
extends:
- '.cargo test'
- .img-stable
needs:
- job: 'trigger'
artifacts: false
- job: 'build-stable'
artifacts: false
test nightly:
allow_failure: true
extends:
- '.cargo test'
- .img-nightly
needs:
- job: 'trigger'
artifacts: false
- job: 'build-nightly'
artifacts: false
test nightly all-features:
allow_failure: true
@ -269,142 +263,166 @@ test nightly all-features:
extends:
- '.cargo test'
- .img-nightly
needs:
- job: 'trigger'
artifacts: false
- job: 'build-nightly'
artifacts: false
.cargo test sys:
stage: "test"
script:
- ./ci/run-sys-cargo-test.sh
- rustc --version
- |
get_features() {
module=${1%%/sys}
echo "--features=v1_20"
}
# First build and test all the crates with their relevant features
# Keep features in sync with below
for crate in gstreamer*/sys gstreamer-gl/*/sys; do
if [ -e $crate/Cargo.toml ]; then
echo "Building $crate with $(get_features $crate)"
cargo build --locked --color=always --manifest-path $crate/Cargo.toml $(get_features $crate)
fi
done
# Run tests for crates we can currently run.
# Other tests are broken currently.
for crate in gstreamer/sys \
gstreamer-app/sys \
gstreamer-audio/sys \
gstreamer-base/sys \
gstreamer-check/sys \
gstreamer-controller/sys \
gstreamer-gl/sys \
gstreamer-gl/egl/sys \
gstreamer-gl/wayland/sys \
gstreamer-gl/x11/sys \
gstreamer-mpegts/sys \
gstreamer-net/sys \
gstreamer-pbutils/sys \
gstreamer-player/sys \
gstreamer-rtsp-server/sys \
gstreamer-rtsp/sys \
gstreamer-sdp/sys \
gstreamer-tag/sys \
gstreamer-video/sys \
gstreamer-webrtc/sys; do
echo "Testing $crate with $(get_features $crate)"
cargo test --locked --color=always --manifest-path $crate/Cargo.toml $(get_features $crate)
done
test stable sys:
extends:
- '.cargo test sys'
- .img-stable
needs:
- job: 'trigger'
artifacts: false
- job: 'build-stable'
artifacts: false
test msrv sys:
extends:
- '.cargo test sys'
- .img-msrv
needs:
- job: 'trigger'
artifacts: false
- job: 'build-msrv'
artifacts: false
test nightly sys:
extends:
- '.cargo test sys'
- .img-nightly
needs:
- job: 'trigger'
artifacts: false
- job: 'build-nightly'
artifacts: false
rustfmt:
extends: .img-stable
stage: "lint"
tags: [ 'placeholder-job' ]
script:
- cargo fmt --version
- cargo fmt -- --color=always --check
needs:
- job: 'build-stable'
artifacts: false
check commits:
extends: .img-stable
stage: "lint"
tags: [ 'placeholder-job' ]
script:
- ci-fairy check-commits --textwidth 0 --no-signed-off-by
needs:
- job: 'build-stable'
artifacts: false
typos:
extends: .img-stable
stage: "lint"
tags: [ 'placeholder-job' ]
script:
- typos
needs:
- job: 'build-stable'
artifacts: false
clippy:
extends: .img-stable
stage: 'extras'
variables:
CLIPPY_LINTS: -D warnings -W unknown-lints
needs:
- job: 'trigger'
artifacts: false
- job: 'build-stable'
artifacts: false
CLIPPY_LINTS: -A clippy::missing_safety_doc -A clippy::manual_range_contains -A clippy::upper_case_acronyms -A clippy::wrong_self_convention -D warnings
script:
- ./ci/run-clippy.sh
- cargo clippy --version
# Keep features in sync with above
- |
get_features() {
crate=$1
if [ "$crate" = "gstreamer" ]; then
echo "--features=ser_de,v1_20"
else
echo "--features=v1_20"
fi
}
for crate in gstreamer* gstreamer-gl/{egl,wayland,x11}; do
if [ -e $crate/Cargo.toml ]; then
FEATURES=$(get_features $crate)
echo "Running clippy on $crate with $FEATURES"
cargo clippy --locked --color=always --manifest-path $crate/Cargo.toml $FEATURES --all-targets -- $CLIPPY_LINTS
fi
done
# And also run over all the examples/tutorials
- |
cargo clippy --locked --color=always --manifest-path examples/Cargo.toml --all-targets --all-features -- $CLIPPY_LINTS
cargo clippy --locked --color=always --manifest-path tutorials/Cargo.toml --all-targets --all-features -- $CLIPPY_LINTS
deny:
extends: .img-stable
stage: 'extras'
needs:
- job: 'build-stable'
artifacts: false
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
only:
- schedules
script:
- cargo update --color=always
- cargo deny --color=always --workspace --all-features check all
- cargo deny --color=always check
gir-checks:
variables:
GIT_SUBMODULE_STRATEGY: recursive
extends: .img-stable
stage: 'extras'
tags: [ 'placeholder-job' ]
needs:
- job: 'build-stable'
artifacts: false
script:
- git submodule update --checkout
- python3 ci/gir-checks.py
- git diff --exit-code
- git clone --depth 1 https://github.com/gtk-rs/checker
- git diff --exit-code
- cd checker && echo '[workspace]' >> Cargo.toml
- cd .. && git diff --exit-code && cd checker
- cargo build --locked --color=always --release
- cd .. && git diff --exit-code && cd checker
- |
cargo run --color=always --release -- ../gstreamer* ../gstreamer-gl/{egl,wayland,x11}
- cd .. && git diff --exit-code && cd checker
# Check doc aliases
- |
for crate in ../gstreamer* ../gstreamer-gl/{egl,wayland,x11}; do
echo '--> Checking doc aliases in ' $crate
python3 doc_aliases.py $crate
done
- cd ..
# To ensure that there was no missing #[doc(alias = "...")]
- git diff --exit-code
- |
for crate in gstreamer* gstreamer-gl/{egl,wayland,x11}; do
echo '-->' $crate
./checker/check_init_asserts $crate
done
- git diff --exit-code
outdated:
extends: .img-stable
stage: 'extras'
needs:
- job: 'build-stable'
artifacts: false
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
only:
- schedules
script:
- cargo update --color=always
- cargo outdated --color=always --root-deps-only --exit-code 1 -v
coverage:
allow_failure: true
extends:
- '.cargo test'
- .img-stable
- .img-nightly
stage: 'extras'
needs:
- job: 'trigger'
artifacts: false
- job: 'build-stable'
artifacts: false
variables:
ALL_FEATURES: 'yes'
RUSTFLAGS: "-Cinstrument-coverage"
RUSTFLAGS: "-Zinstrument-coverage"
LLVM_PROFILE_FILE: "gstreamer-rs-%p-%m.profraw"
script:
- *cargo_test
@ -419,56 +437,31 @@ coverage:
paths:
- 'coverage'
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
cobertura: coverage.xml
doc-stripping:
variables:
GIT_SUBMODULE_STRATEGY: recursive
extends: .img-nightly
stage: 'extras'
needs:
- job: 'build-nightly'
artifacts: false
script:
- git submodule update --checkout
- PATH=~/.cargo/bin/:$PATH ./generator.py --gir-files-directories gir-files gst-gir-files --embed-docs
- PATH=~/.cargo/bin/:$PATH ./generator.py --gir-files-directories gir-files gst-gir-files --strip-docs
- PATH=~/.cargo/bin/:$PATH ./generator.py --strip-docs --no-fmt
- git diff --quiet || (echo 'Files changed after running `rustdoc-stripper -s`, make sure all documentation is protected with `// rustdoc-stripper-ignore-next`!'; git diff; false)
regen-check:
variables:
GIT_SUBMODULE_STRATEGY: recursive
extends: .img-nightly
stage: 'extras'
needs:
- job: 'build-nightly'
artifacts: false
script:
- git submodule update --checkout
- PATH=~/.cargo/bin/:$PATH ./generator.py --gir-files-directories gir-files gst-gir-files --yes
- git diff --quiet || (echo 'Files changed after running `generator.py`, make sure all submodules and generated files are in the correct version!'; git diff; false)
docs:
variables:
GIT_SUBMODULE_STRATEGY: recursive
extends: .img-nightly
stage: 'extras'
needs:
- job: 'build-nightly'
artifacts: false
script:
- git submodule update --checkout
- curl --proto '=https' --tlsv1.2 -sSf -o gir-rustdoc.py
https://gitlab.gnome.org/World/Rust/gir-rustdoc/-/raw/main/gir-rustdoc.py
- chmod +x gir-rustdoc.py
- PATH=~/.cargo/bin/:$PATH ./generator.py --gir-files-directories gir-files gst-gir-files --embed-docs --no-fmt
- PATH=~/.cargo/bin/:$PATH ./generator.py --embed-docs --no-fmt
- |
RUSTDOCFLAGS="$RUST_DOCS_FLAGS"
RUSTFLAGS="--cfg docsrs"
eval $(./gir-rustdoc.py pre-docs)
cargo +nightly doc --workspace --exclude examples --exclude tutorials --all-features --color=always --no-deps
cargo +nightly doc --workspace --exclude examples --exclude tutorials --color=always --features=dox --no-deps
- mv target/doc docs
artifacts:
paths:
@ -481,8 +474,8 @@ docs:
pages:
extends: .img-nightly
stage: 'deploy'
needs: [ 'docs' ]
interruptible: false
dependencies:
- docs
script:
- curl --proto '=https' --tlsv1.2 -sSf -o gir-rustdoc.py
https://gitlab.gnome.org/World/Rust/gir-rustdoc/-/raw/main/gir-rustdoc.py
@ -498,86 +491,5 @@ pages:
paths:
- 'public'
rules:
- if: ($CI_DEFAULT_BRANCH == $CI_COMMIT_BRANCH) && ($CI_PROJECT_NAMESPACE == $NAMESPACE)
- if: $CI_COMMIT_BRANCH == "master" || $CI_COMMIT_BRANCH == "main"
when: 'manual'
.windows rust docker build:
stage: 'container-final'
timeout: '2h'
needs: []
variables:
# Unlike the buildah/linux jobs, this file
# needs to be relative to windows-docker/ subdir
# as it makes life easier in the powershell script
#
# We also don't need a CONTEXT_DIR var as its also
# hardcoded to be windows-docker/
DOCKERFILE: 'ci/windows-docker/Dockerfile'
tags:
- 'windows'
- 'shell'
- '2022'
script:
# We need to pass an array and to resolve the env vars, so we can't use a variable:
- $DOCKER_BUILD_ARGS = @("--build-arg", "DEFAULT_BRANCH=$GST_UPSTREAM_BRANCH", "--build-arg", "RUST_VERSION=$RUST_VERSION")
- "& ci/windows-docker/container.ps1 $CI_REGISTRY $CI_REGISTRY_USER $CI_REGISTRY_PASSWORD $RUST_IMAGE $RUST_UPSTREAM_IMAGE $DOCKERFILE"
- |
if (!($?)) {
echo "Failed to build the image"
Exit 1
}
windows rust docker stable:
extends: '.windows rust docker build'
variables:
RUST_IMAGE: !reference [variables, "WINDOWS_RUST_STABLE_IMAGE"]
RUST_UPSTREAM_IMAGE: !reference [variables, "WINDOWS_RUST_STABLE_UPSTREAM_IMAGE"]
RUST_VERSION: !reference [variables, "GST_RS_STABLE"]
windows rust docker msrv:
extends: '.windows rust docker build'
when: 'manual'
variables:
RUST_IMAGE: !reference [variables, "WINDOWS_RUST_MINIMUM_IMAGE"]
RUST_UPSTREAM_IMAGE: !reference [variables, "WINDOWS_RUST_MINIMUM_UPSTREAM_IMAGE"]
RUST_VERSION: !reference [variables, "GST_RS_MSRV"]
.msvc2019 build:
stage: 'test'
tags:
- 'docker'
- 'windows'
- '2022'
script:
# Skip -sys tests as they don't work
# https://github.com/gtk-rs/gtk3-rs/issues/54
#
# We need to build each crate separately to avoid crates like -egl,-wayland etc on windows
- cmd.exe /C "C:\BuildTools\Common7\Tools\VsDevCmd.bat -host_arch=amd64 -arch=amd64 &&
powershell ./ci/run_windows_tests.ps1"
- |
if (!$?) {
Write-Host "Tests Failed!"
Exit 1
}
test windows msrv:
image: $WINDOWS_RUST_MINIMUM_IMAGE
needs:
- job: 'trigger'
artifacts: false
- job: 'windows rust docker msrv'
artifacts: false
extends: '.msvc2019 build'
test windows stable:
needs:
- job: 'trigger'
artifacts: false
- job: 'windows rust docker stable'
artifacts: false
image: "$WINDOWS_RUST_STABLE_IMAGE"
extends: '.msvc2019 build'

View file

@ -1,33 +0,0 @@
### Describe your issue
<!-- a clear and concise summary of the bug. -->
<!-- For any GStreamer usage question, please contact the community using the #gstreamer channel on IRC https://www.oftc.net/ or the mailing list on https://gstreamer.freedesktop.org/lists/ -->
#### Expected Behavior
<!-- What did you expect to happen -->
#### Observed Behavior
<!-- What actually happened -->
#### Setup
- **Operating System:**
- **Device:** Computer / Tablet / Mobile / Virtual Machine <!-- Delete as appropriate !-->
- **gstreamer-rs Version:**
- **GStreamer Version:**
- **Command line:**
### Steps to reproduce the bug
<!-- please fill in exact steps which reproduce the bug on your system, for example: -->
1. open terminal
2. type `command`
### How reproducible is the bug?
<!-- The reproducibility of the bug is Always/Intermittent/Only once after doing a very specific set of steps-->
### Screenshots if relevant
### Solutions you have tried
### Related non-duplicate issues
### Additional Information
<!-- Any other information such as logs. Make use of <details> for long output -->

3
.gitmodules vendored
View file

@ -1,12 +1,9 @@
[submodule "gir"]
path = gir
url = https://github.com/gtk-rs/gir
update = none
[submodule "gir-files"]
path = gir-files
url = https://github.com/gtk-rs/gir-files
update = none
[submodule "gst-gir-files"]
path = gst-gir-files
url = https://gitlab.freedesktop.org/gstreamer/gir-files-rs.git
update = none

2823
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,55 +1,7 @@
[workspace]
resolver = "2"
default-members = [
"gstreamer/sys",
"gstreamer-analytics/sys",
"gstreamer-app/sys",
"gstreamer-audio/sys",
"gstreamer-base/sys",
"gstreamer-check/sys",
"gstreamer-controller/sys",
"gstreamer-editing-services/sys",
"gstreamer-mpegts/sys",
"gstreamer-net/sys",
"gstreamer-pbutils/sys",
"gstreamer-play/sys",
"gstreamer-player/sys",
"gstreamer-rtp/sys",
"gstreamer-rtsp/sys",
"gstreamer-rtsp-server/sys",
"gstreamer-sdp/sys",
"gstreamer-tag/sys",
"gstreamer-video/sys",
"gstreamer-webrtc/sys",
"gstreamer",
"gstreamer-analytics",
"gstreamer-app",
"gstreamer-audio",
"gstreamer-base",
"gstreamer-check",
"gstreamer-controller",
"gstreamer-editing-services",
"gstreamer-mpegts",
"gstreamer-net",
"gstreamer-pbutils",
"gstreamer-play",
"gstreamer-player",
"gstreamer-rtp",
"gstreamer-rtsp",
"gstreamer-rtsp-server",
"gstreamer-sdp",
"gstreamer-tag",
"gstreamer-validate",
"gstreamer-video",
"gstreamer-webrtc",
"examples",
"tutorials",
]
members = [
"gstreamer/sys",
"gstreamer-analytics/sys",
"gstreamer-app/sys",
"gstreamer-audio/sys",
"gstreamer-base/sys",
@ -63,7 +15,6 @@ members = [
"gstreamer-mpegts/sys",
"gstreamer-net/sys",
"gstreamer-pbutils/sys",
"gstreamer-play/sys",
"gstreamer-player/sys",
"gstreamer-rtp/sys",
"gstreamer-rtsp/sys",
@ -72,9 +23,7 @@ members = [
"gstreamer-tag/sys",
"gstreamer-video/sys",
"gstreamer-webrtc/sys",
"gstreamer-allocators/sys",
"gstreamer",
"gstreamer-analytics",
"gstreamer-app",
"gstreamer-audio",
"gstreamer-base",
@ -85,68 +34,17 @@ members = [
"gstreamer-gl/egl",
"gstreamer-gl/wayland",
"gstreamer-gl/x11",
"gstreamer-mpegts",
"gstreamer-net",
"gstreamer-pbutils",
"gstreamer-play",
"gstreamer-player",
"gstreamer-rtp",
"gstreamer-rtsp",
"gstreamer-rtsp-server",
"gstreamer-sdp",
"gstreamer-tag",
"gstreamer-validate",
"gstreamer-video",
"gstreamer-webrtc",
"gstreamer-allocators",
"gstreamer-utils",
"examples",
"tutorials",
]
exclude = ["gir"]
[workspace.package]
version = "0.23.0"
categories = ["api-bindings", "multimedia"]
repository = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs"
homepage = "https://gstreamer.freedesktop.org"
edition = "2021"
rust-version = "1.70"
[workspace.dependencies]
gio = { git = "https://github.com/gtk-rs/gtk-rs-core", branch = "master" }
gio-sys = { git = "https://github.com/gtk-rs/gtk-rs-core", branch = "master" }
glib = { git = "https://github.com/gtk-rs/gtk-rs-core", branch = "master" }
glib-sys = { git = "https://github.com/gtk-rs/gtk-rs-core", branch = "master" }
gobject-sys = { git = "https://github.com/gtk-rs/gtk-rs-core", branch = "master" }
cairo-rs = { git = "https://github.com/gtk-rs/gtk-rs-core", branch = "master" }
pango = { git = "https://github.com/gtk-rs/gtk-rs-core", branch = "master" }
pangocairo = { git = "https://github.com/gtk-rs/gtk-rs-core", branch = "master" }
gstreamer-audio-sys = { path = "./gstreamer-audio/sys"}
gstreamer-base-sys = { path = "./gstreamer-base/sys"}
gstreamer-gl-sys = { path = "./gstreamer-gl/sys"}
gstreamer-net-sys = { path = "./gstreamer-net/sys"}
gstreamer-pbutils-sys = { path = "./gstreamer-pbutils/sys"}
gstreamer-rtsp-sys = { path = "./gstreamer-rtsp/sys"}
gstreamer-sdp-sys = { path = "./gstreamer-sdp/sys"}
gstreamer-sys = { path = "./gstreamer/sys"}
gstreamer-video-sys = { path = "./gstreamer-video/sys"}
ges = { package = "gstreamer-editing-services", path = "./gstreamer-editing-services" }
gst = { package = "gstreamer", path = "./gstreamer" }
gst-allocators = { package = "gstreamer-allocators", path = "./gstreamer-allocators" }
gst-app = { package = "gstreamer-app", path = "./gstreamer-app" }
gst-audio = { package = "gstreamer-audio", path = "./gstreamer-audio" }
gst-base = { package = "gstreamer-base", path = "./gstreamer-base" }
gst-check = { package = "gstreamer-check", path = "./gstreamer-check" }
gst-gl = { package = "gstreamer-gl", path = "./gstreamer-gl" }
gst-gl-egl = { package = "gstreamer-gl-egl", path = "./gstreamer-gl/egl" }
gst-gl-x11 = { package = "gstreamer-gl-x11", path = "./gstreamer-gl/x11" }
gst-net = { package = "gstreamer-net", path = "./gstreamer-net" }
gst-pbutils = { package = "gstreamer-pbutils", path = "./gstreamer-pbutils" }
gst-play = { package = "gstreamer-play", path = "./gstreamer-play" }
gst-player = { package = "gstreamer-player", path = "./gstreamer-player" }
gst-rtsp = { package = "gstreamer-rtsp", path = "./gstreamer-rtsp" }
gst-rtsp-server = { package = "gstreamer-rtsp-server", path = "./gstreamer-rtsp-server" }
gst-sdp = { package = "gstreamer-sdp", path = "./gstreamer-sdp" }
gst-video = { package = "gstreamer-video", path = "./gstreamer-video" }

111
README.md
View file

@ -1,4 +1,4 @@
# gstreamer-rs [![crates.io](https://img.shields.io/crates/v/gstreamer.svg)](https://crates.io/crates/gstreamer) [![pipeline status](https://gitlab.freedesktop.org/gstreamer/gstreamer-rs/badges/main/pipeline.svg)](https://gitlab.freedesktop.org/gstreamer/gstreamer-rs/commits/main)
# gstreamer-rs [![crates.io](https://img.shields.io/crates/v/gstreamer.svg)](https://crates.io/crates/gstreamer) [![pipeline status](https://gitlab.freedesktop.org/gstreamer/gstreamer-rs/badges/master/pipeline.svg)](https://gitlab.freedesktop.org/gstreamer/gstreamer-rs/commits/master)
[GStreamer](https://gstreamer.freedesktop.org/) bindings for Rust.
Documentation can be found [here](https://gstreamer.pages.freedesktop.org/gstreamer-rs/stable/latest/docs/gstreamer/).
@ -24,7 +24,7 @@ API metadata provided by the GStreamer project.
## Installation
To build the GStreamer bindings or anything depending on them, you need to
have at least GStreamer 1.14 and gst-plugins-base 1.14 installed. In addition,
have at least GStreamer 1.8 and gst-plugins-base 1.8 installed. In addition,
some of the examples/tutorials require various GStreamer plugins to be
available, which can be found in gst-plugins-base, gst-plugins-good,
gst-plugins-bad, gst-plugins-ugly and/or gst-libav.
@ -38,20 +38,23 @@ package manager, or build them from source.
On Debian/Ubuntu they can be installed with
```console
```
$ apt-get install libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \
gstreamer1.0-plugins-base gstreamer1.0-plugins-good \
gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly \
gstreamer1.0-libav libgstrtspserver-1.0-dev libges-1.0-dev
```
The minimum required version of the above libraries is >= 1.14. If you
The minimum required version of the above libraries is >= 1.8. If you
build the gstreamer-player sub-crate, or any of the examples that
depend on gstreamer-player, you must ensure that in addition to the above
packages, `libgstreamer-plugins-bad1.0-dev` is installed. See the `Cargo.toml`
files for the full details,
depend on gstreamer-player, you must ensure that in addition to the
above packages, `libgstreamer-plugins-bad1.0-dev` is installed and
that the version is >= 1.12. See the `Cargo.toml` files for the full
details,
```console
```
# Only if you wish to install gstreamer-player, make sure the version
# of this package is >= 1.12.
$ apt-get install libgstreamer-plugins-bad1.0-dev
```
@ -66,41 +69,31 @@ You can install GStreamer and the plugins via [Homebrew](https://brew.sh/) or
by installing the [binaries](https://gstreamer.freedesktop.org/data/pkg/osx/)
provided by the GStreamer project.
We recommend using the official GStreamer binaries over Homebrew, especially
as GStreamer in Homebrew is [currently broken](https://github.com/orgs/Homebrew/discussions/3740#discussioncomment-3804964).
#### Homebrew
```
$ brew install gstreamer gst-plugins-base gst-plugins-good \
gst-plugins-bad gst-plugins-ugly gst-libav gst-rtsp-server \
gst-editing-services
```
If you wish to install the gstreamer-player sub-crate, make sure the
version of these libraries is >= 1.12. Otherwise, a version >= 1.8 is
sufficient.
#### GStreamer Binaries
You need to download the *two* `.pkg` files from the GStreamer website and
install them, e.g. `gstreamer-1.0-1.20.4-universal.pkg` and
`gstreamer-1.0-devel-1.20.4-universal.pkg`.
install them, e.g. `gstreamer-1.0-1.12.3-x86_64.pkg` and
`gstreamer-1.0-devel-1.12.3-x86_64.pkg`.
After installation, you also need to set the `PATH` environment variable as
follows
After installation, you also need to install `pkg-config` (e.g. via Homebrew)
and set the `PKG_CONFIG_PATH` environment variable
```console
$ export PATH="/Library/Frameworks/GStreamer.framework/Versions/1.0/bin${PATH:+:$PATH}"
```
Also note that the `pkg-config` from GStreamer should be the first one in
the `PATH` as other versions have all kinds of quirks that will cause
problems.
#### Homebrew
Homebrew only installs various plugins if explicitly enabled, so some extra
`--with-*` flags may be required.
```console
$ brew install gstreamer gst-plugins-base gst-plugins-good \
gst-plugins-bad gst-plugins-ugly gst-libav gst-rtsp-server \
gst-editing-services --with-orc --with-libogg --with-opus \
--with-pango --with-theora --with-libvorbis --with-libvpx \
--enable-gtk3
$ export PKG_CONFIG_PATH="/Library/Frameworks/GStreamer.framework/Versions/Current/lib/pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}"
```
Make sure the version of these libraries is >= 1.14.
<a name="installation-windows"/>
### Windows
@ -110,35 +103,9 @@ with `pacman` or by installing the
[binaries](https://gstreamer.freedesktop.org/data/pkg/windows/) provided by
the GStreamer project.
We recommend using the official GStreamer binaries over MSYS2.
#### GStreamer Binaries
You need to download the *two* `.msi` files for your platform from the
GStreamer website and install them, e.g. `gstreamer-1.0-x86_64-1.20.4.msi` and
`gstreamer-1.0-devel-x86_64-1.20.4.msi`. Make sure to select the version that
matches your Rust toolchain, i.e. MinGW or MSVC.
After installation set the ``PATH` environment variable as follows:
```console
# For a UNIX-style shell:
$ export PATH="c:/gstreamer/1.0/msvc_x86_64/bin${PATH:+:$PATH}"
# For cmd.exe:
$ set PATH=C:\gstreamer\1.0\msvc_x86_64\bin;%PATH%
```
Make sure to update the path to where you have actually installed GStreamer
and for the corresponding toolchain.
Also note that the `pkg-config.exe` from GStreamer should be the first one in
the `PATH` as other versions have all kinds of quirks that will cause
problems.
#### MSYS2 / pacman
```console
```sh
$ pacman -S glib2-devel pkg-config \
mingw-w64-x86_64-gstreamer mingw-w64-x86_64-gst-plugins-base \
mingw-w64-x86_64-gst-plugins-good mingw-w64-x86_64-gst-plugins-bad \
@ -146,13 +113,29 @@ $ pacman -S glib2-devel pkg-config \
mingw-w64-x86_64-gst-rtsp-server
```
Make sure the version of these libraries is >= 1.14.
If you wish to install the gstreamer-player sub-crate, make sure the
version of these libraries is >= 1.12. Otherwise, a version >= 1.8 is
sufficient.
Note that the version of `pkg-config` included in `MSYS2` is
[known to have problems](https://github.com/rust-lang/pkg-config-rs/issues/51#issuecomment-346300858)
compiling GStreamer, so you may need to install another version. One option
would be [`pkg-config-lite`](https://sourceforge.net/projects/pkgconfiglite/).
#### GStreamer Binaries
You need to download the *two* `.msi` files for your platform from the
GStreamer website and install them, e.g. `gstreamer-1.0-x86_64-1.12.3.msi` and
`gstreamer-1.0-devel-x86_64-1.12.3.msi`.
After installation, you also need to install `pkg-config` (e.g. via MSYS2 or
from [here](https://sourceforge.net/projects/pkgconfiglite/))
and set the `PKG_CONFIG_PATH` environment variable
```sh
$ export PKG_CONFIG_PATH="c:\\gstreamer\\1.0\\x86_64\\lib\\pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}"
```
<a name="getting-started"/>
## Getting Started
@ -174,12 +157,12 @@ In addition there are
[tutorials](https://gstreamer.freedesktop.org/documentation/tutorials/) on the
GStreamer website. Many of them were ported to Rust already and the code can
be found in the
[tutorials](https://gitlab.freedesktop.org/gstreamer/gstreamer-rs/tree/main/tutorials)
[tutorials](https://gitlab.freedesktop.org/gstreamer/gstreamer-rs/tree/master/tutorials)
directory.
Some further examples for various aspects of GStreamer and how to use it from
Rust can be found in the
[examples](https://gitlab.freedesktop.org/gstreamer/gstreamer-rs/tree/main/examples)
[examples](https://gitlab.freedesktop.org/gstreamer/gstreamer-rs/tree/master/examples)
directory.
Various GStreamer plugins written in Rust can be found in the

View file

@ -1,42 +0,0 @@
from itertools import chain
import os
import sys
from pathlib import Path as P
from subprocess import check_call as exec
NATIVE_CRATES = ["gstreamer-utils"]
def git(*args):
exec(["git"] + list(args))
def check_no_git_diff():
git("diff", "--exit-code")
check_no_git_diff()
git("clone", "--depth", "1", "https://github.com/gtk-rs/checker")
check_no_git_diff()
rootdir = P(".")
checker_dir = P("checker")
with (checker_dir / "Cargo.toml").open("a") as f:
f.write("[workspace]\n")
check_no_git_diff()
exec(['cargo', 'build', '--locked', '--color=always', '--release'], cwd=checker_dir)
check_no_git_diff()
exec('cargo run --color=always --release -- ../gstreamer* ../gstreamer-gl/{egl,wayland,x11}', cwd=checker_dir, shell=True)
gl_dir = rootdir / 'gstreamer-gl'
for crate in chain(rootdir.glob('gstreamer*'), [gl_dir / 'egl', gl_dir / 'wayland', gl_dir / 'x11']):
# Ignore "native" crates
if crate.name in NATIVE_CRATES:
continue
print(f'--> Checking doc aliases in {crate.absolute()}')
exec(['python3', 'doc_aliases.py', crate.absolute()], cwd=checker_dir)
print(f'--> {crate.absolute()}')
exec(['./checker/check_init_asserts', crate.absolute()])
check_no_git_diff()

View file

@ -1,7 +1,4 @@
variables:
GST_RS_IMG_TAG: "2024-05-10.0"
GST_RS_STABLE: "1.78.0"
GST_RS_MSRV: "1.70.0"
# The branch we use to build GStreamer from in the docker images
# Ex. main, 1.24, my-test-branch
GST_UPSTREAM_BRANCH: 'main'
GST_RS_IMG_TAG: '2021-06-18.0'
GST_RS_STABLE: '1.53.0'
GST_RS_MSRV: '1.51.0'

View file

@ -1,11 +0,0 @@
set -e
RELEASE=1.4.1
git clone https://code.videolan.org/videolan/dav1d.git --branch $RELEASE
cd dav1d
meson build -D prefix=/usr/local
ninja -C build
ninja -C build install
cd ..
rm -rf dav1d

View file

@ -1,49 +1,10 @@
#! /bin/bash
set -e
DEFAULT_BRANCH="$GST_UPSTREAM_BRANCH"
pip3 install meson==0.58.1
pip3 install meson==1.1.1 --break-system-packages
git clone --depth 1 https://gitlab.freedesktop.org/gstreamer/gst-build.git --branch master
cd gst-build
# gstreamer-rs already has a 'gstreamer' directory so don't clone there
pushd .
cd ..
git clone https://gitlab.freedesktop.org/gstreamer/gstreamer.git \
--depth 1 \
--branch "$DEFAULT_BRANCH"
cd gstreamer
# plugins required by tests
PLUGINS="-D gst-plugins-base:ogg=enabled \
-D gst-plugins-base:vorbis=enabled \
-D gst-plugins-base:theora=enabled \
-D gst-plugins-good:matroska=enabled \
-D gst-plugins-good:vpx=enabled \
-D gst-plugins-bad:opus=enabled \
-D gst-plugins-ugly:x264=enabled"
echo "subproject('gtk')" >> meson.build
meson setup build \
-D prefix=/usr/local \
-D gpl=enabled \
-D ugly=enabled \
-D examples=disabled \
-D gtk_doc=disabled \
-D introspection=disabled \
-D libav=disabled \
-D python=disabled \
-D vaapi=disabled \
$PLUGINS
meson compile -C build
meson install -C build
ldconfig
cd ..
rm -rf gstreamer/
# Check what plugins we installed
gst-inspect-1.0
popd
meson build -D prefix=/usr/local -D devtools=disabled -D examples=disabled -D gtk_doc=disabled -D introspection=disabled -D libav=disabled -D libnice=disabled -D python=disabled -D ugly=disabled -D vaapi=disabled
ninja -C build
ninja -C build install

View file

@ -1,11 +1,9 @@
#! /bin/bash
source ./ci/env.sh
set -e
export CARGO_HOME='/usr/local/cargo'
RUSTUP_VERSION=1.27.1
RUSTUP_VERSION=1.24.3
RUST_VERSION=$1
RUST_IMAGE_FULL=$2
RUST_ARCH="x86_64-unknown-linux-gnu"
@ -25,27 +23,15 @@ rustc --version
if [ "$RUST_IMAGE_FULL" = "1" ]; then
rustup component add clippy-preview
rustup component add rustfmt
cargo install --force cargo-deny
cargo install --force cargo-outdated
fi
cargo install --locked --force cargo-deny
cargo install --locked --force cargo-outdated
cargo install --locked --force typos-cli --version "1.19.0"
if [ "$RUST_VERSION" = "nightly" ]; then
# Coverage tools
cargo install grcov
rustup component add llvm-tools-preview
cargo install --locked --force grcov
fi
if [ "$RUST_VERSION" = "nightly" ]; then
# FIXME: Don't build cargo-c with --locked for now because otherwise a
# version of ahash is used that doesn't build on nightly anymore
cargo install cargo-c --version 0.9.22+cargo-0.72
else
cargo install --locked cargo-c --version 0.9.22+cargo-0.72
fi
if [ "$RUST_VERSION" = "nightly" ]; then
rustup component add rustfmt --toolchain nightly
# Documentation tools
cargo install --locked --force rustdoc-stripper
cargo install --force rustdoc-stripper
fi

View file

@ -1,30 +0,0 @@
#! /bin/bash
set -ex
rustc --version
cargo --version
for crate in gstreamer* gstreamer-gl/{egl,wayland,x11}; do
if [ -e "$crate/Cargo.toml" ]; then
if [ -n "$ALL_FEATURES" ]; then
FEATURES="--all-features"
else
FEATURES=""
fi
echo "Building and testing $crate with $FEATURES"
cargo build --locked --color=always --manifest-path "$crate/Cargo.toml" $FEATURES
G_DEBUG=fatal_warnings cargo test --color=always --manifest-path "$crate/Cargo.toml" $FEATURES
fi
done
if [ -n "$EXAMPLES_TUTORIALS" ]; then
# Keep in sync with examples/Cargo.toml
# List all features except windows/win32
EXAMPLES_FEATURES="--features=rtsp-server,rtsp-server-record,pango-cairo,overlay-composition,gl,gst-gl-x11,gst-gl-egl,allocators,gst-play,gst-player,ges,image,cairo-rs,gst-video/v1_18"
cargo build --locked --color=always --manifest-path examples/Cargo.toml --bins --examples "$EXAMPLES_FEATURES"
cargo build --locked --color=always --manifest-path tutorials/Cargo.toml --bins --examples --all-features
fi

View file

@ -1,38 +0,0 @@
#! /bin/bash
set -ex
rustc --version
cargo --version
cargo clippy --version
# Keep features in sync with run-cargo-test.sh
get_features() {
crate=$1
case "$crate" in
gstreamer-audio|gstreamer-editing-services|gstreamer-gl|gstreamer-pbutils|gstreamer-rtp|gstreamer-rtsp|gstreamer-video|gstreamer)
echo "--features=serde,v1_26"
;;
*)
echo "--features=v1_26"
;;
esac
}
for crate in gstreamer* gstreamer-gl/{egl,wayland,x11}; do
if [ -e "$crate/Cargo.toml" ]; then
FEATURES=$(get_features "$crate")
echo "Running clippy on $crate with $FEATURES"
cargo clippy --locked --color=always --manifest-path "$crate/Cargo.toml" $FEATURES --all-targets -- $CLIPPY_LINTS
fi
done
# Keep in sync with examples/Cargo.toml
# List all features except windows/win32
EXAMPLES_FEATURES="--features=rtsp-server,rtsp-server-record,pango-cairo,overlay-composition,gl,gst-gl-x11,gst-gl-egl,allocators,gst-play,gst-player,ges,image,cairo-rs,gst-video/v1_18"
# And also run over all the examples/tutorials
cargo clippy --locked --color=always --manifest-path examples/Cargo.toml --all-targets "$EXAMPLES_FEATURES" -- $CLIPPY_LINTS
cargo clippy --locked --color=always --manifest-path tutorials/Cargo.toml --all-targets --all-features -- $CLIPPY_LINTS

View file

@ -1,43 +0,0 @@
#! /bin/bash
set -ex
rustc --version
cargo --version
for crate in gstreamer*/sys gstreamer-gl/*/sys; do
if [ -e "$crate/Cargo.toml" ]; then
echo "Building $crate with --all-features"
cargo build --locked --color=always --manifest-path "$crate/Cargo.toml" --all-features
fi
done
for crate in gstreamer/sys \
gstreamer-allocators/sys \
gstreamer-analytics/sys \
gstreamer-app/sys \
gstreamer-audio/sys \
gstreamer-base/sys \
gstreamer-check/sys \
gstreamer-controller/sys \
gstreamer-editing-services/sys \
gstreamer-gl/sys \
gstreamer-gl/egl/sys \
gstreamer-gl/wayland/sys \
gstreamer-gl/x11/sys \
gstreamer-mpegts/sys \
gstreamer-net/sys \
gstreamer-pbutils/sys \
gstreamer-play/sys \
gstreamer-player/sys \
gstreamer-rtp/sys \
gstreamer-rtsp-server/sys \
gstreamer-rtsp/sys \
gstreamer-sdp/sys \
gstreamer-tag/sys \
gstreamer-validate/sys \
gstreamer-video/sys \
gstreamer-webrtc/sys; do
echo "Testing $crate with --all-features)"
cargo test --locked --color=always --manifest-path $crate/Cargo.toml --all-features
done

View file

@ -1,86 +0,0 @@
# List of all the crates we want to build
# We need to do this manually to avoid trying
# to build egl,wayland,x11 etc, which can't
# work on windows
[string[]] $crates = @(
'gstreamer',
# Unix specific atm
# 'gstreamer-allocators'
'gstreamer-app',
'gstreamer-audio',
'gstreamer-base',
'gstreamer-check',
'gstreamer-controller',
'gstreamer-editing-services',
'gstreamer-gl',
# 'gstreamer-gl/egl',
# 'gstreamer-gl/wayland',
# 'gstreamer-gl/x11',
'gstreamer-mpegts',
'gstreamer-mpegts/sys',
'gstreamer-net',
'gstreamer-pbutils',
'gstreamer-player',
'gstreamer-rtp',
'gstreamer-rtsp',
'gstreamer-rtsp-server',
'gstreamer-sdp',
'gstreamer-tag',
'gstreamer-tag/sys',
'gstreamer-video',
'gstreamer-webrtc',
'tutorials',
'examples'
)
# "" is the default build, no flags appended
[string[]] $features_matrix = @(
# "--no-default-features",
# "--features=v1_18",
# "--features=v1_20",
"",
"--all-features"
)
foreach($features in $features_matrix) {
foreach($crate in $crates)
{
Write-Host "Building crate: $crate"
Write-Host "Features: $features"
$env:LocalFeatures = $features
# Don't append feature flags if the string is null/empty
# Or when we want to build without default features
if ($env:LocalFeatures -and ($env:LocalFeatures -ne '--no-default-features')) {
if ($crate -eq 'examples') {
# FIXME: We can do --all-features for examples once we have gtk3 installed in the image
$env:LocalFeatures = "--features=rtsp-server,rtsp-server-record,pango-cairo,overlay-composition,gst-play,gst-player,ges,image,cairo-rs,gst-video/v1_18,windows"
}
if ($crate -eq 'tutorials') {
$env:LocalFeatures = ''
}
}
Write-Host "with features: $env:LocalFeatures"
cargo build --color=always --manifest-path $crate/Cargo.toml --all-targets $env:LocalFeatures
if (!$?) {
Write-Host "Failed to build crate: $crate"
Exit 1
}
if (($crate -eq "gstreamer-tag/sys") -or ($crate -eq "gstreamer-mpegts/sys")) {
Write-Host "Skipping tests for $crate"
continue
}
$env:G_DEBUG="fatal_warnings"
cargo test --no-fail-fast --color=always --manifest-path $crate/Cargo.toml $env:LocalFeatures
if (!$?) {
Write-Host "Tests failed to for crate: $crate"
Exit 1
}
}
}

View file

@ -1,22 +0,0 @@
# escape=`
FROM "registry.freedesktop.org/gstreamer/gstreamer/amd64/windows:2023-07-17.0-main"
SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"]
ARG DEFAULT_BRANCH="1.24"
ARG RUST_VERSION="invalid"
RUN choco install -y pkgconfiglite nasm llvm openssl
# https://stackoverflow.com/a/50716450
RUN setx PATH '%PATH%;C:\Program Files\NASM;C:\gst-install\bin'
ENV PKG_CONFIG_PATH="C:\gst-install\lib\pkgconfig"
COPY install_gst.ps1 install_dav1d.ps1 C:\
RUN C:\install_gst.ps1
RUN C:\install_dav1d.ps1
RUN Invoke-WebRequest -Uri https://win.rustup.rs/x86_64 -OutFile C:\rustup-init.exe
RUN C:\rustup-init.exe -y --profile minimal --default-toolchain $env:RUST_VERSION
RUN cargo install --locked cargo-c --version 0.9.22+cargo-0.72

View file

@ -1,60 +0,0 @@
# Copied from mesa, big kudos
#
# https://gitlab.freedesktop.org/mesa/mesa/-/blob/master/.gitlab-ci/windows/mesa_container.ps1
# https://gitlab.freedesktop.org/mesa/mesa/-/blob/34e3e164936d1d3cef267da7780e87f062fedf39/.gitlab-ci/windows/mesa_container.ps1
# Implements the equivalent of ci-templates container-ifnot-exists, using
# Docker directly as we don't have buildah/podman/skopeo available under
# Windows, nor can we execute Docker-in-Docker
$registry_uri = $args[0]
$registry_username = $args[1]
$registry_password = $args[2]
$registry_user_image = $args[3]
$registry_central_image = $args[4]
$dockerfile = $args[5]
docker --config "windows-docker.conf" login -u "$registry_username" -p "$registry_password" "$registry_uri"
if (!$?) {
Write-Host "docker login failed to $registry_uri"
Exit 1
}
# if the image already exists, don't rebuild it
docker --config "windows-docker.conf" pull "$registry_user_image"
if ($?) {
Write-Host "User image $registry_user_image already exists; not rebuilding"
docker --config "windows-docker.conf" logout "$registry_uri"
Exit 0
}
# if the image already exists upstream, copy it
docker --config "windows-docker.conf" pull "$registry_central_image"
if ($?) {
Write-Host "Copying central image $registry_central_image to user image $registry_user_image"
docker --config "windows-docker.conf" tag "$registry_central_image" "$registry_user_image"
docker --config "windows-docker.conf" push "$registry_user_image"
$pushstatus = $?
docker --config "windows-docker.conf" logout "$registry_uri"
if (!$pushstatus) {
Write-Host "Pushing image to $registry_user_image failed"
Exit 1
}
Exit 0
}
Write-Host "No image found at $registry_user_image or $registry_central_image; rebuilding"
docker --config "windows-docker.conf" build $DOCKER_BUILD_ARGS --no-cache -t "$registry_user_image" -f "$dockerfile" "./ci/windows-docker"
if (!$?) {
Write-Host "Container build failed"
docker --config "windows-docker.conf" logout "$registry_uri"
Exit 1
}
Get-Date
docker --config "windows-docker.conf" push "$registry_user_image"
$pushstatus = $?
docker --config "windows-docker.conf" logout "$registry_uri"
if (!$pushstatus) {
Write-Host "Pushing image to $registry_user_image failed"
Exit 1
}

View file

@ -1,28 +0,0 @@
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12;
# Download gstreamer and all its subprojects
git clone -b 1.4.1 --depth 1 https://code.videolan.org/videolan/dav1d.git C:\dav1d
if (!$?) {
Write-Host "Failed to clone dav1d"
Exit 1
}
Set-Location C:\dav1d
# This is fine, we are not going to use the GtkMedia* apis
$env:MESON_ARGS = "--prefix=C:\gst-install\"
Write-Output "Building dav1d"
cmd.exe /C "C:\BuildTools\Common7\Tools\VsDevCmd.bat -host_arch=amd64 -arch=amd64 && meson _build $env:MESON_ARGS && meson compile -C _build && ninja -C _build install"
if (!$?) {
Write-Host "Failed to build and install dav1d"
Exit 1
}
cd C:\
cmd /c rmdir /s /q C:\dav1d
if (!$?) {
Write-Host "Failed to remove dav1d checkout"
Exit 1
}

View file

@ -1,71 +0,0 @@
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12;
# Download gstreamer and all its subprojects
git clone -b $env:DEFAULT_BRANCH --depth 1 https://gitlab.freedesktop.org/gstreamer/gstreamer.git C:\gstreamer
if (!$?) {
Write-Host "Failed to clone gstreamer"
Exit 1
}
Set-Location C:\gstreamer
# Copy the cache we already have in the image to avoid massive redownloads
Move-Item C:/subprojects/* C:\gstreamer\subprojects
# Update the subprojects cache
Write-Output "Running meson subproject reset"
meson subprojects update --reset
if (!$?) {
Write-Host "Failed to update gstreamer subprojects"
Exit 1
}
$MESON_ARGS = @(`
"--prefix=C:\gst-install", `
"-Dglib:installed_tests=false", `
"-Dlibnice:tests=disabled", `
"-Dlibnice:examples=disabled", `
"-Dffmpeg:tests=disabled", `
"-Dopenh264:tests=disabled", `
"-Dpygobject:tests=false", `
"-Dgpl=enabled", `
"-Dugly=enabled", `
"-Dbad=enabled", `
"-Dges=enabled", `
"-Drtsp_server=enabled", `
"-Ddevtools=enabled", `
"-Dsharp=disabled", `
"-Dpython=disabled", `
"-Dlibav=disabled", `
"-Dvaapi=disabled", `
"-Dgst-plugins-base:pango=enabled", `
"-Dgst-plugins-good:cairo=enabled", `
"-Dgst-plugins-good:lame=disabled"
)
$PSDefaultParameterValues['Out-File:Encoding'] = 'utf8'
echo "subproject('gtk')" >> meson.build
Write-Output "Building gstreamer"
meson setup --vsenv $MESON_ARGS _build
if (!$?) {
type "_build\meson-logs\meson-log.txt"
Write-Host "Failed to run meson setup, see log above"
Exit 1
}
Write-Output "Compiling gstreamer"
meson compile -C _build
if (!$?) {
Write-Host "Failed to run meson compile"
Exit 1
}
# meson install does a spurious rebuild sometimes that then fails
meson install --no-rebuild -C _build
if (!$?) {
Write-Host "Failed to run meson install"
Exit 1
}
cd c:\
Remove-Item -LiteralPath "C:\gstreamer" -Force -Recurse

View file

@ -1,8 +1,3 @@
exclude = [
"examples",
"tutorials",
]
[advisories]
db-path = "~/.cargo/advisory-db"
db-urls = ["https://github.com/rustsec/advisory-db"]
@ -13,7 +8,16 @@ ignore = []
[licenses]
unlicensed = "deny"
default = "deny"
allow = [
"Apache-2.0",
]
deny = [
"GPL-1.0",
"GPL-2.0",
"GPL-3.0",
"AGPL-1.0",
"AGPL-3.0",
]
copyleft = "deny"
allow-osi-fsf-free = "either"
confidence-threshold = 0.8
@ -23,12 +27,6 @@ multiple-versions = "deny"
wildcards = "allow"
highlight = "all"
# proc-macro-crate depends on an older version of toml_edit
# https://github.com/bkchr/proc-macro-crate/pull/50
[[bans.skip]]
name = "toml_edit"
version = "0.21"
[sources]
unknown-registry = "deny"
unknown-git = "deny"

View file

@ -1,73 +1,60 @@
[package]
name = "examples"
version.workspace = true
version = "0.17.4"
license = "MIT"
authors = ["Sebastian Dröge <sebastian@centricular.com>"]
edition.workspace = true
rust-version.workspace = true
edition = "2018"
[dependencies]
glib.workspace = true
gst.workspace = true
gst-gl = { workspace = true, optional = true }
gst-gl-egl = { workspace = true, optional = true }
gst-gl-x11 = { workspace = true, optional = true }
gst-app.workspace = true
gst-audio.workspace = true
gst-base.workspace = true
gst-video.workspace = true
gst-pbutils.workspace = true
gst-play = { workspace = true, optional = true }
gst-player = { workspace = true, optional = true }
ges = { workspace = true, optional = true }
gst-sdp = { workspace = true, optional = true }
gst-rtsp = { workspace = true, optional = true }
gst-rtsp-server = { workspace = true, optional = true }
gst-allocators = { workspace = true, optional = true }
gio = { workspace = true, optional = true }
glib = { version = "0.14" }
gst = { package = "gstreamer", version = "0.17", path = "../gstreamer" }
gst-gl = { package = "gstreamer-gl", version = "0.17", path = "../gstreamer-gl", optional = true }
gst-gl-egl = { package = "gstreamer-gl-egl", version = "0.17", path = "../gstreamer-gl/egl", optional = true }
gst-gl-wayland = { package = "gstreamer-gl-wayland", version = "0.17", path = "../gstreamer-gl/wayland", optional = true }
gst-gl-x11 = { package = "gstreamer-gl-x11", version = "0.17", path = "../gstreamer-gl/x11", optional = true }
gst-app = { package = "gstreamer-app", version = "0.17", path = "../gstreamer-app" }
gst-audio = { package = "gstreamer-audio", version = "0.17", path = "../gstreamer-audio" }
gst-base = { package = "gstreamer-base", version = "0.17", path = "../gstreamer-base" }
gst-video = { package = "gstreamer-video", version = "0.17", path = "../gstreamer-video" }
gst-pbutils = { package = "gstreamer-pbutils", version = "0.17", path = "../gstreamer-pbutils" }
gst-player = { package = "gstreamer-player", version = "0.17", path = "../gstreamer-player", optional = true }
ges = { package = "gstreamer-editing-services", version = "0.17", path = "../gstreamer-editing-services", optional = true }
gst-sdp = { package = "gstreamer-sdp", version = "0.17", path = "../gstreamer-sdp", optional = true }
gst-rtsp = { package = "gstreamer-rtsp", version = "0.17", path = "../gstreamer-rtsp", optional = true }
gst-rtsp-server = { package = "gstreamer-rtsp-server", version = "0.17", path = "../gstreamer-rtsp-server", optional = true }
gtk = { version = "0.14", optional = true }
gdk = { version = "0.14", optional = true }
gio = { version = "0.14", optional = true }
anyhow = "1.0"
byte-slice-cast = "1"
cairo-rs = { workspace = true, features=["use_glib"], optional = true }
derive_more = "0.99.5"
futures = "0.3"
glutin = { version = "0.31", optional = true, default-features = false }
glutin-winit = { version = "0.4", optional = true, default-features = false }
image = { version = "0.24", optional = true, default-features = false, features = ["png", "jpeg"] }
memfd = { version = "0.6", optional = true }
memmap2 = { version = "0.9", optional = true }
pango = { workspace = true, optional = true }
pangocairo = { workspace = true, optional = true }
raw-window-handle = { version = "0.5", optional = true }
uds = { version = "0.4", optional = true }
winit = { version = "0.29", optional = true, default-features = false, features = ["rwh_05"] }
atomic_refcell = "0.1"
data-encoding = "2.0"
once_cell = "1"
[target.'cfg(windows)'.dependencies]
windows = { version = "0.56", features=["Win32_Graphics_Direct3D11",
"Win32_Foundation", "Win32_Graphics_Direct3D", "Win32_Graphics_Dxgi",
"Win32_Graphics_Dxgi_Common", "Win32_Graphics_Direct2D",
"Win32_Graphics_Direct2D_Common", "Win32_Graphics_DirectWrite",
"Win32_Graphics_Imaging", "Win32_System_Com", "Foundation_Numerics"], optional = true }
byte-slice-cast = "1"
cairo-rs = { version = "0.14", features=["use_glib"], optional = true }
pango = { version = "0.14", optional = true }
pangocairo = { version = "0.14", optional = true }
glutin = { version = "0.27", optional = true }
once_cell = "1.0"
image = { version="0.23", optional = true }
[target.'cfg(target_os = "macos")'.dependencies]
cocoa = "0.25"
objc = "0.2.7"
cocoa = "0.24"
[build-dependencies]
gl_generator = { version = "0.14", optional = true }
[features]
default = []
gtksink = ["gtk", "gio"]
gtkvideooverlay = ["gtk", "gdk", "gio"]
gtkvideooverlay-x11 = ["gtkvideooverlay"]
gtkvideooverlay-quartz = ["gtkvideooverlay"]
rtsp-server = ["gst-rtsp-server", "gst-rtsp", "gst-sdp"]
rtsp-server-record = ["gst-rtsp-server", "gst-rtsp", "gio"]
v1_10 = ["gst/v1_10"]
v1_14 = ["gst/v1_14"]
pango-cairo = ["pango", "pangocairo", "cairo-rs"]
overlay-composition = ["pango", "pangocairo", "cairo-rs"]
gl = ["dep:gst-gl", "dep:gl_generator", "dep:glutin", "dep:glutin-winit", "dep:winit", "dep:raw-window-handle"]
gst-gl-x11 = ["dep:gst-gl-x11", "glutin-winit?/glx"] # glx turns on x11
gst-gl-egl = ["dep:gst-gl-egl", "glutin-winit?/egl", "glutin-winit?/x11", "glutin-winit?/wayland"] # Use X11 or Wayland via EGL
allocators = ["gst-allocators", "memmap2", "memfd", "uds"]
gl = ["gst-gl", "gl_generator", "glutin"]
[[bin]]
name = "appsink"
@ -77,6 +64,7 @@ name = "appsrc"
[[bin]]
name = "custom_events"
required-features = ["v1_10"]
[[bin]]
name = "custom_meta"
@ -86,6 +74,7 @@ name = "decodebin"
[[bin]]
name = "debug_ringbuffer"
required-features = ["v1_14"]
[[bin]]
name = "encodebin"
@ -93,6 +82,14 @@ name = "encodebin"
[[bin]]
name = "events"
[[bin]]
name = "gtksink"
required-features = ["gtksink"]
[[bin]]
name = "gtkvideooverlay"
required-features = ["gtkvideooverlay"]
[[bin]]
name = "iterator"
@ -108,10 +105,6 @@ name = "transmux"
[[bin]]
name = "pad_probes"
[[bin]]
name = "play"
required-features = ["gst-play"]
[[bin]]
name = "playbin"
@ -136,10 +129,6 @@ required-features = ["rtsp-server"]
name = "rtsp-server-subclass"
required-features = ["rtsp-server"]
[[bin]]
name = "rtsp-server-custom-auth"
required-features = ["rtsp-server", "gst-rtsp-server/v1_22"]
[[bin]]
name = "tagsetter"
@ -167,10 +156,6 @@ required-features = ["pango-cairo"]
name = "overlay-composition"
required-features = ["overlay-composition"]
[[bin]]
name = "overlay-composition-d2d"
required-features = ["windows"]
[[bin]]
name = "ges"
required-features = ["ges"]
@ -192,18 +177,3 @@ name = "video_converter"
[[bin]]
name = "thumbnail"
required-features = ["image"]
[[bin]]
name = "fd_allocator"
required-features = ["allocators"]
[[bin]]
name = "cairo_compositor"
required-features = ["cairo-rs", "gst-video/v1_18"]
[[bin]]
name = "d3d11videosink"
required-features = ["windows"]
[[bin]]
name = "audio_multichannel_interleave"

View file

@ -1,7 +1,7 @@
#[cfg(feature = "gl")]
fn generate_gl_bindings() {
let dest = std::path::PathBuf::from(&std::env::var("OUT_DIR").unwrap());
let mut file = std::fs::File::create(dest.join("test_gl_bindings.rs")).unwrap();
let mut file = std::fs::File::create(&dest.join("test_gl_bindings.rs")).unwrap();
gl_generator::Registry::new(
gl_generator::Api::Gles2,
(3, 0),

View file

@ -10,44 +10,61 @@
// This is the format we request:
// Audio / Signed 16bit / 1 channel / arbitrary sample rate
use gst::element_error;
use gst::prelude::*;
use byte_slice_cast::*;
use std::i16;
use std::i32;
use anyhow::Error;
use byte_slice_cast::*;
use derive_more::{Display, Error};
use gst::{element_error, prelude::*};
#[path = "../examples-common.rs"]
mod examples_common;
#[derive(Debug, Display, Error)]
#[display(fmt = "Received error from {src}: {error} (debug: {debug:?})")]
#[display(fmt = "Missing element {}", _0)]
struct MissingElement(#[error(not(source))] &'static str);
#[derive(Debug, Display, Error)]
#[display(fmt = "Received error from {}: {} (debug: {:?})", src, error, debug)]
struct ErrorMessage {
src: glib::GString,
error: glib::Error,
debug: Option<glib::GString>,
src: String,
error: String,
debug: Option<String>,
source: glib::Error,
}
fn create_pipeline() -> Result<gst::Pipeline, Error> {
gst::init()?;
let pipeline = gst::Pipeline::default();
let src = gst::ElementFactory::make("audiotestsrc").build()?;
let appsink = gst_app::AppSink::builder()
// Tell the appsink what format we want. It will then be the audiotestsrc's job to
// provide the format we request.
// This can be set after linking the two objects, because format negotiation between
// both elements will happen during pre-rolling of the pipeline.
.caps(
&gst_audio::AudioCapsBuilder::new_interleaved()
.format(gst_audio::AUDIO_FORMAT_S16)
.channels(1)
.build(),
)
.build();
let pipeline = gst::Pipeline::new(None);
let src = gst::ElementFactory::make("audiotestsrc", None)
.map_err(|_| MissingElement("audiotestsrc"))?;
let sink = gst::ElementFactory::make("appsink", None).map_err(|_| MissingElement("appsink"))?;
pipeline.add_many([&src, appsink.upcast_ref()])?;
src.link(&appsink)?;
pipeline.add_many(&[&src, &sink])?;
src.link(&sink)?;
let appsink = sink
.dynamic_cast::<gst_app::AppSink>()
.expect("Sink element is expected to be an appsink!");
// Tell the appsink what format we want. It will then be the audiotestsrc's job to
// provide the format we request.
// This can be set after linking the two objects, because format negotiation between
// both elements will happen during pre-rolling of the pipeline.
appsink.set_caps(Some(&gst::Caps::new_simple(
"audio/x-raw",
&[
("format", &gst_audio::AUDIO_FORMAT_S16.to_str()),
("layout", &"interleaved"),
("channels", &(1i32)),
("rate", &gst::IntRange::<i32>::new(1, i32::MAX)),
],
)));
// Getting data out of the appsink is done by setting callbacks on it.
// The appsink will then call those handlers, as soon as data is available.
@ -91,7 +108,7 @@ fn create_pipeline() -> Result<gst::Pipeline, Error> {
element_error!(
appsink,
gst::ResourceError::Failed,
("Failed to interpret buffer as S16 PCM")
("Failed to interprete buffer as S16 PCM")
);
gst::FlowError::Error
@ -107,7 +124,7 @@ fn create_pipeline() -> Result<gst::Pipeline, Error> {
})
.sum();
let rms = (sum / (samples.len() as f64)).sqrt();
println!("rms: {rms}");
println!("rms: {}", rms);
Ok(gst::FlowSuccess::Ok)
})
@ -134,10 +151,11 @@ fn main_loop(pipeline: gst::Pipeline) -> Result<(), Error> {
return Err(ErrorMessage {
src: msg
.src()
.map(|s| s.path_string())
.unwrap_or_else(|| glib::GString::from("UNKNOWN")),
error: err.error(),
.map(|s| String::from(s.path_string()))
.unwrap_or_else(|| String::from("None")),
error: err.error().to_string(),
debug: err.debug(),
source: err.error(),
}
.into());
}
@ -153,7 +171,7 @@ fn main_loop(pipeline: gst::Pipeline) -> Result<(), Error> {
fn example_main() {
match create_pipeline().and_then(main_loop) {
Ok(r) => r,
Err(e) => eprintln!("Error! {e}"),
Err(e) => eprintln!("Error! {}", e),
}
}

View file

@ -10,20 +10,25 @@
// The application provides data of the following format:
// Video / BGRx (4 bytes) / 2 fps
use gst::prelude::*;
use anyhow::Error;
use derive_more::{Display, Error};
use gst::prelude::*;
use gst_video::prelude::*;
#[path = "../examples-common.rs"]
mod examples_common;
#[derive(Debug, Display, Error)]
#[display(fmt = "Received error from {src}: {error} (debug: {debug:?})")]
#[display(fmt = "Missing element {}", _0)]
struct MissingElement(#[error(not(source))] &'static str);
#[derive(Debug, Display, Error)]
#[display(fmt = "Received error from {}: {} (debug: {:?})", src, error, debug)]
struct ErrorMessage {
src: glib::GString,
error: glib::Error,
debug: Option<glib::GString>,
src: String,
error: String,
debug: Option<String>,
source: glib::Error,
}
const WIDTH: usize = 320;
@ -32,7 +37,19 @@ const HEIGHT: usize = 240;
fn create_pipeline() -> Result<gst::Pipeline, Error> {
gst::init()?;
let pipeline = gst::Pipeline::default();
let pipeline = gst::Pipeline::new(None);
let src = gst::ElementFactory::make("appsrc", None).map_err(|_| MissingElement("appsrc"))?;
let videoconvert = gst::ElementFactory::make("videoconvert", None)
.map_err(|_| MissingElement("videoconvert"))?;
let sink = gst::ElementFactory::make("autovideosink", None)
.map_err(|_| MissingElement("autovideosink"))?;
pipeline.add_many(&[&src, &videoconvert, &sink])?;
gst::Element::link_many(&[&src, &videoconvert, &sink])?;
let appsrc = src
.dynamic_cast::<gst_app::AppSrc>()
.expect("Source element is expected to be an appsrc!");
// Specify the format we want to provide as application into the pipeline
// by creating a video info with the given format and creating caps from it for the appsrc element.
@ -42,16 +59,8 @@ fn create_pipeline() -> Result<gst::Pipeline, Error> {
.build()
.expect("Failed to create video info");
let appsrc = gst_app::AppSrc::builder()
.caps(&video_info.to_caps().unwrap())
.format(gst::Format::Time)
.build();
let videoconvert = gst::ElementFactory::make("videoconvert").build()?;
let sink = gst::ElementFactory::make("autovideosink").build()?;
pipeline.add_many([appsrc.upcast_ref(), &videoconvert, &sink])?;
gst::Element::link_many([appsrc.upcast_ref(), &videoconvert, &sink])?;
appsrc.set_caps(Some(&video_info.to_caps().unwrap()));
appsrc.set_format(gst::Format::Time);
// Our frame counter, that is stored in the mutable environment
// of the closure of the need-data callback
@ -76,7 +85,7 @@ fn create_pipeline() -> Result<gst::Pipeline, Error> {
return;
}
println!("Producing frame {i}");
println!("Producing frame {}", i);
let r = if i % 2 == 0 { 0 } else { 255 };
let g = if i % 3 == 0 { 0 } else { 255 };
@ -151,10 +160,11 @@ fn main_loop(pipeline: gst::Pipeline) -> Result<(), Error> {
return Err(ErrorMessage {
src: msg
.src()
.map(|s| s.path_string())
.unwrap_or_else(|| glib::GString::from("UNKNOWN")),
error: err.error(),
.map(|s| String::from(s.path_string()))
.unwrap_or_else(|| String::from("None")),
error: err.error().to_string(),
debug: err.debug(),
source: err.error(),
}
.into());
}
@ -170,7 +180,7 @@ fn main_loop(pipeline: gst::Pipeline) -> Result<(), Error> {
fn example_main() {
match create_pipeline().and_then(main_loop) {
Ok(r) => r,
Err(e) => eprintln!("Error! {e}"),
Err(e) => eprintln!("Error! {}", e),
}
}

View file

@ -1,153 +0,0 @@
// This example demonstrates how to mix multiple audio
// streams into a single output using the audiomixer element.
// In this case, we're mixing 4 stereo streams into a single 8 channel output.
use gst::prelude::*;
use std::env;
#[path = "../examples-common.rs"]
mod examples_common;
const TRACKS: i32 = 4;
fn create_source_and_link(pipeline: &gst::Pipeline, mixer: &gst::Element, track_number: i32) {
let freq = ((track_number + 1) * 1000) as f64;
let audiosrc = gst::ElementFactory::make("audiotestsrc")
.property("freq", freq)
.property("num-buffers", 2000)
.build()
.unwrap();
let caps = gst_audio::AudioCapsBuilder::new().channels(2).build();
let capsfilter = gst::ElementFactory::make("capsfilter")
.property("caps", &caps)
.build()
.unwrap();
pipeline.add_many([&audiosrc, &capsfilter]).unwrap();
gst::Element::link_many([&audiosrc, &capsfilter]).unwrap();
let src_pad = capsfilter.static_pad("src").unwrap();
let mixer_pad = mixer.request_pad_simple("sink_%u").unwrap();
// audiomixer expects a mix-matrix set on each input pad,
// indicating which output channels our input should appear in.
// Rows => input channels, columns => output channels.
// Here each input channel will appear in exactly one output channel.
let mut mix_matrix: Vec<Vec<f32>> = vec![];
for i in 0..TRACKS {
if i == track_number {
mix_matrix.push(vec![1.0, 0.0]);
mix_matrix.push(vec![0.0, 1.0]);
} else {
mix_matrix.push(vec![0.0, 0.0]);
mix_matrix.push(vec![0.0, 0.0]);
}
}
let mut audiomixer_config = gst_audio::AudioConverterConfig::new();
audiomixer_config.set_mix_matrix(&mix_matrix);
mixer_pad.set_property("converter-config", audiomixer_config);
src_pad.link(&mixer_pad).unwrap();
}
fn example_main() {
gst::init().unwrap();
let args: Vec<_> = env::args().collect();
let output_file = if args.len() == 2 {
&args[1]
} else {
println!("Usage: audiomixer <output file>");
std::process::exit(-1);
};
let pipeline = gst::Pipeline::new();
let audiomixer = gst::ElementFactory::make("audiomixer").build().unwrap();
// Using an arbitrary layout of 4 stereo pairs.
let positions = [
gst_audio::AudioChannelPosition::FrontLeft,
gst_audio::AudioChannelPosition::FrontRight,
gst_audio::AudioChannelPosition::RearLeft,
gst_audio::AudioChannelPosition::RearRight,
gst_audio::AudioChannelPosition::SideLeft,
gst_audio::AudioChannelPosition::SideRight,
gst_audio::AudioChannelPosition::TopFrontLeft,
gst_audio::AudioChannelPosition::TopFrontRight,
];
let mask = gst_audio::AudioChannelPosition::positions_to_mask(&positions, true).unwrap();
let caps = gst_audio::AudioCapsBuilder::new()
.channels(positions.len() as i32)
.channel_mask(mask)
.build();
let capsfilter = gst::ElementFactory::make("capsfilter")
.property("caps", &caps)
.build()
.unwrap();
let audioconvert = gst::ElementFactory::make("audioconvert").build().unwrap();
let audioresample = gst::ElementFactory::make("audioresample").build().unwrap();
let wavenc = gst::ElementFactory::make("wavenc").build().unwrap();
let sink = gst::ElementFactory::make("filesink")
.property("location", output_file)
.build()
.unwrap();
pipeline
.add_many([
&audiomixer,
&capsfilter,
&audioconvert,
&audioresample,
&wavenc,
&sink,
])
.unwrap();
gst::Element::link_many([
&audiomixer,
&capsfilter,
&audioconvert,
&audioresample,
&wavenc,
&sink,
])
.unwrap();
for i in 0..TRACKS {
create_source_and_link(&pipeline, &audiomixer, i);
}
let bus = pipeline.bus().expect("Pipeline without bus");
pipeline
.set_state(gst::State::Playing)
.expect("Unable to start pipeline");
for msg in bus.iter_timed(gst::ClockTime::NONE) {
use gst::MessageView;
match msg.view() {
MessageView::Eos(..) => break,
MessageView::Error(err) => {
eprintln!(
"Error from {:?}: {} ({:?})",
msg.src().map(|s| s.path_string()),
err.error(),
err.debug()
);
break;
}
_ => (),
}
}
pipeline
.set_state(gst::State::Null)
.expect("Unable to change pipeline state to NULL");
}
fn main() {
// tutorials_common::run is only required to set up the application environment on macOS
// (but not necessary in normal Cocoa applications where this is set up automatically)
examples_common::run(example_main);
}

View file

@ -1,719 +0,0 @@
// This example demonstrates how to implement a custom compositor based on cairo.
#![allow(clippy::non_send_fields_in_send_ty)]
use anyhow::{Context, Error};
use gst::prelude::*;
use gst_base::prelude::*;
#[path = "../examples-common.rs"]
mod examples_common;
// Our custom compositor element is defined in this module.
mod cairo_compositor {
use gst_base::subclass::prelude::*;
use gst_video::{prelude::*, subclass::prelude::*};
// In the imp submodule we include the actual implementation of the compositor.
mod imp {
use std::sync::Mutex;
use super::*;
// Settings of the compositor.
#[derive(Clone)]
struct Settings {
background_color: u32,
}
impl Default for Settings {
fn default() -> Self {
Self {
background_color: 0xff_00_00_00,
}
}
}
// This is the private data of our compositor.
#[derive(Default)]
pub struct CairoCompositor {
settings: Mutex<Settings>,
}
// This trait registers our type with the GObject object system and
// provides the entry points for creating a new instance and setting
// up the class data.
#[glib::object_subclass]
impl ObjectSubclass for CairoCompositor {
const NAME: &'static str = "CairoCompositor";
type Type = super::CairoCompositor;
type ParentType = gst_video::VideoAggregator;
type Interfaces = (gst::ChildProxy,);
}
// Implementation of glib::Object virtual methods.
impl ObjectImpl for CairoCompositor {
// Specification of the compositor properties.
// In this case a single property for configuring the background color of the
// composition.
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: std::sync::OnceLock<Vec<glib::ParamSpec>> =
std::sync::OnceLock::new();
PROPERTIES.get_or_init(|| {
vec![glib::ParamSpecUInt::builder("background-color")
.nick("Background Color")
.blurb("Background color as 0xRRGGBB")
.default_value(Settings::default().background_color)
.build()]
})
}
// Called by the application whenever the value of a property should be changed.
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
let mut settings = self.settings.lock().unwrap();
match pspec.name() {
"background-color" => {
settings.background_color = value.get().unwrap();
}
_ => unimplemented!(),
};
}
// Called by the application whenever the value of a property should be retrieved.
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
let settings = self.settings.lock().unwrap();
match pspec.name() {
"background-color" => settings.background_color.to_value(),
_ => unimplemented!(),
}
}
}
// Implementation of gst::Object virtual methods.
impl GstObjectImpl for CairoCompositor {}
// Implementation of gst::Element virtual methods.
impl ElementImpl for CairoCompositor {
// The element specific metadata. This information is what is visible from
// gst-inspect-1.0 and can also be programmatically retrieved from the gst::Registry
// after initial registration without having to load the plugin in memory.
fn metadata() -> Option<&'static gst::subclass::ElementMetadata> {
static ELEMENT_METADATA: std::sync::OnceLock<gst::subclass::ElementMetadata> =
std::sync::OnceLock::new();
Some(ELEMENT_METADATA.get_or_init(|| {
gst::subclass::ElementMetadata::new(
"Cairo Compositor",
"Compositor/Video",
"Cairo based compositor",
"Sebastian Dröge <sebastian@centricular.com>",
)
}))
}
fn pad_templates() -> &'static [gst::PadTemplate] {
static PAD_TEMPLATES: std::sync::OnceLock<Vec<gst::PadTemplate>> =
std::sync::OnceLock::new();
PAD_TEMPLATES.get_or_init(|| {
// Create pad templates for our sink and source pad. These are later used for
// actually creating the pads and beforehand already provide information to
// GStreamer about all possible pads that could exist for this type.
// On all pads we can only handle BGRx.
let caps = gst_video::VideoCapsBuilder::new()
.format(gst_video::VideoFormat::Bgrx)
.pixel_aspect_ratio((1, 1).into())
.build();
vec![
// The src pad template must be named "src" for aggregator
// and always be there.
gst::PadTemplate::new(
"src",
gst::PadDirection::Src,
gst::PadPresence::Always,
&caps,
)
.unwrap(),
// The sink pad template must be named "sink_%u" by default for aggregator
// and be requested by the application.
//
// Also declare here that it should be a pad with our custom compositor pad
// type that is defined further below.
gst::PadTemplate::with_gtype(
"sink_%u",
gst::PadDirection::Sink,
gst::PadPresence::Request,
&caps,
super::CairoCompositorPad::static_type(),
)
.unwrap(),
]
})
}
// Notify via the child proxy interface whenever a new pad is added or removed.
fn request_new_pad(
&self,
templ: &gst::PadTemplate,
name: Option<&str>,
caps: Option<&gst::Caps>,
) -> Option<gst::Pad> {
let element = self.obj();
let pad = self.parent_request_new_pad(templ, name, caps)?;
element.child_added(&pad, &pad.name());
Some(pad)
}
fn release_pad(&self, pad: &gst::Pad) {
let element = self.obj();
element.child_removed(pad, &pad.name());
self.parent_release_pad(pad);
}
}
// Implementation of gst_base::Aggregator virtual methods.
impl AggregatorImpl for CairoCompositor {
// Called whenever a query arrives at the given sink pad of the compositor.
fn sink_query(
&self,
aggregator_pad: &gst_base::AggregatorPad,
query: &mut gst::QueryRef,
) -> bool {
use gst::QueryViewMut;
// We can accept any input caps that match the pad template. By default
// videoaggregator only allows caps that have the same format as the output.
match query.view_mut() {
QueryViewMut::Caps(q) => {
let caps = aggregator_pad.pad_template_caps();
let filter = q.filter();
let caps = if let Some(filter) = filter {
filter.intersect_with_mode(&caps, gst::CapsIntersectMode::First)
} else {
caps
};
q.set_result(&caps);
true
}
QueryViewMut::AcceptCaps(q) => {
let caps = q.caps();
let template_caps = aggregator_pad.pad_template_caps();
let res = caps.is_subset(&template_caps);
q.set_result(res);
true
}
_ => self.parent_sink_query(aggregator_pad, query),
}
}
}
// Implementation of gst_video::VideoAggregator virtual methods.
impl VideoAggregatorImpl for CairoCompositor {
// Called by videoaggregator whenever the output format should be determined.
fn find_best_format(
&self,
_downstream_caps: &gst::Caps,
) -> Option<(gst_video::VideoInfo, bool)> {
// Let videoaggregator select whatever format downstream wants.
//
// By default videoaggregator doesn't allow a different format than the input
// format.
None
}
// Called whenever a new output frame should be produced. At this point, each pad has
// either no frame queued up at all or the frame that should be used for this output
// time.
fn aggregate_frames(
&self,
token: &gst_video::subclass::AggregateFramesToken,
outbuf: &mut gst::BufferRef,
) -> Result<gst::FlowSuccess, gst::FlowError> {
let element = self.obj();
let pads = element.sink_pads();
// Map the output frame writable.
let out_info = element.video_info().unwrap();
let mut out_frame =
gst_video::VideoFrameRef::from_buffer_ref_writable(outbuf, &out_info).unwrap();
// And then create a cairo context for drawing on the output frame.
with_frame(&mut out_frame, |ctx| {
let settings = self.settings.lock().unwrap().clone();
// First of all, clear the background.
let bg = (
((settings.background_color >> 16) & 0xff) as f64 / 255.0,
((settings.background_color >> 8) & 0xff) as f64 / 255.0,
(settings.background_color & 0xff) as f64 / 255.0,
);
ctx.set_operator(cairo::Operator::Source);
ctx.set_source_rgb(bg.0, bg.1, bg.2);
ctx.paint().unwrap();
ctx.set_operator(cairo::Operator::Over);
// Then for each pad (in zorder), draw it according to the current settings.
for pad in pads {
let pad = pad.downcast_ref::<CairoCompositorPad>().unwrap();
let settings = pad.imp().settings.lock().unwrap().clone();
if settings.alpha <= 0.0 || settings.scale <= 0.0 {
continue;
}
let frame = match pad.prepared_frame(token) {
Some(frame) => frame,
None => continue,
};
ctx.save().unwrap();
ctx.translate(settings.xpos, settings.ypos);
ctx.scale(settings.scale, settings.scale);
ctx.translate(frame.width() as f64 / 2.0, frame.height() as f64 / 2.0);
ctx.rotate(settings.rotate / 360.0 * 2.0 * std::f64::consts::PI);
ctx.translate(
-(frame.width() as f64 / 2.0),
-(frame.height() as f64 / 2.0),
);
paint_frame(ctx, &frame, settings.alpha);
ctx.restore().unwrap();
}
});
Ok(gst::FlowSuccess::Ok)
}
}
// Implementation of gst::ChildProxy virtual methods.
//
// This allows accessing the pads and their properties from e.g. gst-launch.
impl ChildProxyImpl for CairoCompositor {
fn children_count(&self) -> u32 {
let object = self.obj();
object.num_pads() as u32
}
fn child_by_name(&self, name: &str) -> Option<glib::Object> {
let object = self.obj();
object
.pads()
.into_iter()
.find(|p| p.name() == name)
.map(|p| p.upcast())
}
fn child_by_index(&self, index: u32) -> Option<glib::Object> {
let object = self.obj();
object
.pads()
.into_iter()
.nth(index as usize)
.map(|p| p.upcast())
}
}
}
// Creates a cairo context around the given video frame and then calls the closure to operate
// on the cairo context. Ensures that no references to the video frame stay inside cairo.
fn with_frame<F: FnOnce(&cairo::Context)>(
frame: &mut gst_video::VideoFrameRef<&mut gst::BufferRef>,
func: F,
) {
// SAFETY: This is the one and only surface reference and it is dropped at the end, meaning
// nothing from cairo is referencing the frame data anymore.
unsafe {
use glib::translate::*;
let surface = cairo::ImageSurface::create_for_data_unsafe(
frame.plane_data_mut(0).unwrap().as_mut_ptr(),
cairo::Format::Rgb24,
frame.width() as i32,
frame.height() as i32,
frame.plane_stride()[0],
)
.unwrap();
let ctx = cairo::Context::new(&surface).unwrap();
func(&ctx);
drop(ctx);
surface.finish();
assert_eq!(
cairo::ffi::cairo_surface_get_reference_count(surface.to_glib_none().0),
1,
);
}
}
// Paints the frame with the given alpha on the cairo context at the current origin.
// Ensures that no references to the video frame stay inside cairo.
fn paint_frame(
ctx: &cairo::Context,
frame: &gst_video::VideoFrameRef<&gst::BufferRef>,
alpha: f64,
) {
// SAFETY: This is the one and only surface reference and it is dropped at the end, meaning
// nothing from cairo is referencing the frame data anymore.
//
// Also nothing is ever writing to the surface from here.
unsafe {
use glib::translate::*;
let surface = cairo::ImageSurface::create_for_data_unsafe(
frame.plane_data(0).unwrap().as_ptr() as *mut u8,
cairo::Format::Rgb24,
frame.width() as i32,
frame.height() as i32,
frame.plane_stride()[0],
)
.unwrap();
ctx.set_source_surface(&surface, 0.0, 0.0).unwrap();
ctx.paint_with_alpha(alpha).unwrap();
ctx.set_source_rgb(0.0, 0.0, 0.0);
assert_eq!(
cairo::ffi::cairo_surface_get_reference_count(surface.to_glib_none().0),
1,
);
}
}
// This here defines the public interface of our element and implements
// the corresponding traits so that it behaves like any other gst::Element.
glib::wrapper! {
pub struct CairoCompositor(ObjectSubclass<imp::CairoCompositor>) @extends gst_video::VideoAggregator, gst_base::Aggregator, gst::Element, gst::Object, @implements gst::ChildProxy;
}
impl CairoCompositor {
// Creates a new instance of our compositor with the given name.
pub fn new(name: Option<&str>) -> Self {
glib::Object::builder().property("name", name).build()
}
}
// In the imp submodule we include the implementation of the pad subclass.
//
// This doesn't implement any additional logic but only provides properties for configuring the
// appearance of the stream corresponding to this pad and the storage of the property values.
mod imp_pad {
use std::sync::Mutex;
use super::*;
// Settings of our pad.
#[derive(Clone)]
pub(super) struct Settings {
pub(super) alpha: f64,
pub(super) scale: f64,
pub(super) rotate: f64,
pub(super) xpos: f64,
pub(super) ypos: f64,
}
impl Default for Settings {
fn default() -> Self {
Self {
alpha: 1.0,
scale: 1.0,
rotate: 0.0,
xpos: 0.0,
ypos: 0.0,
}
}
}
// This is the private data of our pad.
#[derive(Default)]
pub struct CairoCompositorPad {
pub(super) settings: Mutex<Settings>,
}
// This trait registers our type with the GObject object system and
// provides the entry points for creating a new instance and setting
// up the class data.
#[glib::object_subclass]
impl ObjectSubclass for CairoCompositorPad {
const NAME: &'static str = "CairoCompositorPad";
type Type = super::CairoCompositorPad;
type ParentType = gst_video::VideoAggregatorPad;
}
// Implementation of glib::Object virtual methods.
impl ObjectImpl for CairoCompositorPad {
// Specification of the compositor pad properties.
// In this case there are various properties for defining the position and otherwise
// the appearance of the stream corresponding to this pad.
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: std::sync::OnceLock<Vec<glib::ParamSpec>> =
std::sync::OnceLock::new();
PROPERTIES.get_or_init(|| {
vec![
glib::ParamSpecDouble::builder("alpha")
.nick("Alpha")
.blurb("Alpha value of the input")
.minimum(0.0)
.maximum(1.0)
.default_value(Settings::default().alpha)
.build(),
glib::ParamSpecDouble::builder("scale")
.nick("Scale")
.blurb("Scale factor of the input")
.minimum(0.0)
.maximum(f64::MAX)
.default_value(Settings::default().scale)
.build(),
glib::ParamSpecDouble::builder("rotate")
.nick("Rotate")
.blurb("Rotation of the input")
.minimum(0.0)
.maximum(360.0)
.default_value(Settings::default().rotate)
.build(),
glib::ParamSpecDouble::builder("xpos")
.nick("X Position")
.blurb("Horizontal position of the input")
.minimum(0.0)
.maximum(f64::MAX)
.default_value(Settings::default().xpos)
.build(),
glib::ParamSpecDouble::builder("ypos")
.nick("Y Position")
.blurb("Vertical position of the input")
.minimum(0.0)
.maximum(f64::MAX)
.default_value(Settings::default().ypos)
.build(),
]
})
}
// Called by the application whenever the value of a property should be changed.
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
let mut settings = self.settings.lock().unwrap();
match pspec.name() {
"alpha" => {
settings.alpha = value.get().unwrap();
}
"scale" => {
settings.scale = value.get().unwrap();
}
"rotate" => {
settings.rotate = value.get().unwrap();
}
"xpos" => {
settings.xpos = value.get().unwrap();
}
"ypos" => {
settings.ypos = value.get().unwrap();
}
_ => unimplemented!(),
};
}
// Called by the application whenever the value of a property should be retrieved.
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
let settings = self.settings.lock().unwrap();
match pspec.name() {
"alpha" => settings.alpha.to_value(),
"scale" => settings.scale.to_value(),
"rotate" => settings.rotate.to_value(),
"xpos" => settings.xpos.to_value(),
"ypos" => settings.ypos.to_value(),
_ => unimplemented!(),
}
}
}
// Implementation of gst::Object virtual methods.
impl GstObjectImpl for CairoCompositorPad {}
// Implementation of gst::Pad virtual methods.
impl PadImpl for CairoCompositorPad {}
// Implementation of gst_base::AggregatorPad virtual methods.
impl AggregatorPadImpl for CairoCompositorPad {}
// Implementation of gst_video::VideoAggregatorPad virtual methods.
impl VideoAggregatorPadImpl for CairoCompositorPad {}
}
// This here defines the public interface of our element and implements
// the corresponding traits so that it behaves like any other gst::Pad.
glib::wrapper! {
pub struct CairoCompositorPad(ObjectSubclass<imp_pad::CairoCompositorPad>) @extends gst_video::VideoAggregatorPad, gst_base::AggregatorPad, gst::Pad, gst::Object;
}
}
fn create_pipeline() -> Result<gst::Pipeline, Error> {
gst::init()?;
// Create our pipeline with the compositor and two input streams.
let pipeline = gst::Pipeline::default();
let src1 = gst::ElementFactory::make("videotestsrc")
.property_from_str("pattern", "ball")
.build()?;
let src2 = gst::ElementFactory::make("videotestsrc")
.property_from_str("pattern", "smpte")
.build()?;
let comp = cairo_compositor::CairoCompositor::new(None);
let conv = gst::ElementFactory::make("videoconvert").build()?;
let sink = gst::ElementFactory::make("autovideosink").build()?;
comp.set_property("background-color", 0xff_33_33_33u32);
pipeline.add_many([&src1, &src2, comp.upcast_ref(), &conv, &sink])?;
// Link everything together.
src1.link_filtered(
&comp,
&gst::Caps::builder("video/x-raw")
.field("width", 320i32)
.field("height", 240i32)
.build(),
)
.context("Linking source 1")?;
src2.link_filtered(
&comp,
&gst::Caps::builder("video/x-raw")
.field("width", 320i32)
.field("height", 240i32)
.build(),
)
.context("Linking source 2")?;
comp.link_filtered(
&conv,
&gst::Caps::builder("video/x-raw")
.field("width", 1280i32)
.field("height", 720i32)
.build(),
)
.context("Linking converter")?;
conv.link(&sink).context("Linking sink")?;
// Change positions etc of both inputs based on a timer
let xmax = 1280.0 - 320.0f64;
let ymax = 720.0 - 240.0f64;
let sink_0 = comp.static_pad("sink_0").unwrap();
sink_0.set_property("xpos", 0.0f64);
sink_0.set_property("ypos", 0.0f64);
let sink_1 = comp.static_pad("sink_1").unwrap();
sink_1.set_property("xpos", xmax);
sink_1.set_property("ypos", ymax);
comp.set_emit_signals(true);
comp.connect_samples_selected(move |_agg, _seg, pts, _dts, _dur, _info| {
// Position and rotation period is 10s.
let pos = (pts.unwrap().nseconds() % gst::ClockTime::from_seconds(10).nseconds()) as f64
/ gst::ClockTime::from_seconds(10).nseconds() as f64;
let xpos = (1.0 + f64::sin(2.0 * std::f64::consts::PI * pos)) * xmax / 2.0;
let ypos = (1.0 + f64::cos(2.0 * std::f64::consts::PI * pos)) * ymax / 2.0;
sink_0.set_property("xpos", xpos);
sink_0.set_property("ypos", ypos);
let xpos = (1.0 + f64::cos(2.0 * std::f64::consts::PI * pos)) * xmax / 2.0;
let ypos = (1.0 + f64::sin(2.0 * std::f64::consts::PI * pos)) * ymax / 2.0;
sink_1.set_property("xpos", xpos);
sink_1.set_property("ypos", ypos);
sink_0.set_property("rotate", pos * 360.0);
sink_1.set_property("rotate", 360.0 - pos * 360.0);
// Alpha period is 2s.
let pos = (pts.unwrap().nseconds() % gst::ClockTime::from_seconds(2).nseconds()) as f64
/ gst::ClockTime::from_seconds(2).nseconds() as f64;
sink_0.set_property(
"alpha",
(1.0 + f64::sin(2.0 * std::f64::consts::PI * pos)) / 2.0,
);
sink_1.set_property(
"alpha",
(1.0 + f64::cos(2.0 * std::f64::consts::PI * pos)) / 2.0,
);
// Scale period is 20s.
let pos = (pts.unwrap().nseconds() % gst::ClockTime::from_seconds(20).nseconds()) as f64
/ gst::ClockTime::from_seconds(20).nseconds() as f64;
sink_0.set_property("scale", pos);
sink_1.set_property("scale", 1.0 - pos);
});
Ok(pipeline)
}
// Start the pipeline and collect messages from the bus until an error or EOS.
fn main_loop(pipeline: gst::Pipeline) -> Result<(), Error> {
pipeline.set_state(gst::State::Playing)?;
let bus = pipeline
.bus()
.expect("Pipeline without bus. Shouldn't happen!");
let mut bus_stream = bus.stream();
let main_context = glib::MainContext::default();
// Storage for any error so we can report it later.
let mut error = None;
main_context.block_on(async {
use futures::prelude::*;
while let Some(msg) = bus_stream.next().await {
use gst::MessageView;
match msg.view() {
MessageView::Eos(..) => break,
MessageView::Error(err) => {
error = Some(anyhow::anyhow!(
"Error from {:?}: {} ({:?})",
err.src().map(|s| s.path_string()),
err.error(),
err.debug()
));
break;
}
_ => (),
}
}
});
// In case of error, report to the caller.
if let Some(error) = error {
let _ = pipeline.set_state(gst::State::Null);
return Err(error);
}
pipeline.set_state(gst::State::Null)?;
Ok(())
}
fn example_main() -> Result<(), Error> {
create_pipeline().and_then(main_loop)
}
fn main() -> Result<(), Error> {
// tutorials_common::run is only required to set up the application environment on macOS
// (but not necessary in normal Cocoa applications where this is set up automatically).
examples_common::run(example_main)
}

View file

@ -21,7 +21,7 @@ impl ExampleCustomEvent {
#[allow(clippy::new_ret_no_self)]
pub fn new(send_eos: bool) -> gst::Event {
let s = gst::Structure::builder(Self::EVENT_NAME)
.field("send_eos", send_eos)
.field("send_eos", &send_eos)
.build();
gst::event::CustomDownstream::new(s)
}
@ -48,7 +48,7 @@ fn example_main() {
let main_loop = glib::MainLoop::new(None, false);
// This creates a pipeline by parsing the gst-launch pipeline syntax.
let pipeline = gst::parse::launch(
let pipeline = gst::parse_launch(
"audiotestsrc name=src ! queue max-size-time=2000000000 ! fakesink name=sink sync=true",
)
.unwrap();
@ -75,33 +75,31 @@ fn example_main() {
// Add a pad probe on the sink pad and catch the custom event we sent, then send
// an EOS event on the pipeline.
sinkpad.add_probe(gst::PadProbeType::EVENT_DOWNSTREAM, move |_, probe_info| {
let Some(event) = probe_info.event() else {
return gst::PadProbeReturn::Ok;
};
let Some(custom_event) = ExampleCustomEvent::parse(event) else {
return gst::PadProbeReturn::Ok;
};
let Some(pipeline) = pipeline_weak.upgrade() else {
return gst::PadProbeReturn::Ok;
};
if custom_event.send_eos {
/* Send EOS event to shut down the pipeline, but from an async callback, as we're
* in a pad probe blocking the stream thread here... */
println!("Got custom event with send_eos=true. Sending EOS");
let ev = gst::event::Eos::new();
let pipeline_weak = pipeline_weak.clone();
pipeline.call_async(move |_| {
if let Some(pipeline) = pipeline_weak.upgrade() {
pipeline.send_event(ev);
match probe_info.data {
Some(gst::PadProbeData::Event(ref ev))
if ev.type_() == gst::EventType::CustomDownstream =>
{
if let Some(custom_event) = ExampleCustomEvent::parse(ev) {
if let Some(pipeline) = pipeline_weak.upgrade() {
if custom_event.send_eos {
/* Send EOS event to shut down the pipeline, but from an async callback, as we're
* in a pad probe blocking the stream thread here... */
println!("Got custom event with send_eos=true. Sending EOS");
let ev = gst::event::Eos::new();
let pipeline_weak = pipeline_weak.clone();
pipeline.call_async(move |_| {
if let Some(pipeline) = pipeline_weak.upgrade() {
pipeline.send_event(ev);
}
});
} else {
println!("Got custom event, with send_eos=false. Ignoring");
}
}
}
});
} else {
println!("Got custom event, with send_eos=false. Ignoring");
}
_ => (),
}
gst::PadProbeReturn::Ok
});
@ -115,53 +113,56 @@ fn example_main() {
glib::timeout_add_seconds(2 + i as u32, move || {
// Here we temporarily retrieve a strong reference on the pipeline from the weak one
// we moved into this callback.
let Some(pipeline) = pipeline_weak.upgrade() else {
return glib::ControlFlow::Break;
let pipeline = match pipeline_weak.upgrade() {
Some(pipeline) => pipeline,
None => return glib::Continue(false),
};
println!("Sending custom event to the pipeline with send_eos={send_eos}");
println!(
"Sending custom event to the pipeline with send_eos={}",
send_eos
);
let ev = ExampleCustomEvent::new(*send_eos);
if !pipeline.send_event(ev) {
println!("Warning: Failed to send custom event");
}
// Remove this handler, the pipeline will shutdown once our pad probe catches the custom
// event and sends EOS
glib::ControlFlow::Break
glib::Continue(false)
});
}
let main_loop_clone = main_loop.clone();
// This sets the bus's signal handler (don't be mislead by the "add", there can only be one).
// Every message from the bus is passed through this function. Its returnvalue determines
// whether the handler wants to be called again. If glib::ControlFlow::Break is returned, the
// whether the handler wants to be called again. If glib::Continue(false) is returned, the
// handler is removed and will never be called again. The mainloop still runs though.
let _bus_watch = bus
.add_watch(move |_, msg| {
use gst::MessageView;
bus.add_watch(move |_, msg| {
use gst::MessageView;
let main_loop = &main_loop_clone;
match msg.view() {
MessageView::Eos(..) => {
println!("received eos");
// An EndOfStream event was sent to the pipeline, so we tell our main loop
// to stop execution here.
main_loop.quit()
}
MessageView::Error(err) => {
println!(
"Error from {:?}: {} ({:?})",
err.src().map(|s| s.path_string()),
err.error(),
err.debug()
);
main_loop.quit();
}
_ => (),
};
let main_loop = &main_loop_clone;
match msg.view() {
MessageView::Eos(..) => {
println!("received eos");
// An EndOfStream event was sent to the pipeline, so we tell our main loop
// to stop execution here.
main_loop.quit()
}
MessageView::Error(err) => {
println!(
"Error from {:?}: {} ({:?})",
err.src().map(|s| s.path_string()),
err.error(),
err.debug()
);
main_loop.quit();
}
_ => (),
};
// Tell the mainloop to continue executing this callback.
glib::ControlFlow::Continue
})
.expect("Failed to add bus watch");
// Tell the mainloop to continue executing this callback.
glib::Continue(true)
})
.expect("Failed to add bus watch");
// Operate GStreamer's bus, facilitating GLib's mainloop here.
// This function call will block until you tell the mainloop to quit
@ -171,6 +172,11 @@ fn example_main() {
pipeline
.set_state(gst::State::Null)
.expect("Unable to set the pipeline to the `Null` state");
// Remove the watch function from the bus.
// Again: There can always only be one watch function.
// Thus we don't have to tell him which function to remove.
bus.remove_watch().unwrap();
}
fn main() {

View file

@ -2,17 +2,17 @@
//
// It simply attaches a GstMeta with a Rust String to buffers that are passed into
// an appsrc and retrieves them again from an appsink.
#![allow(clippy::non_send_fields_in_send_ty)]
use gst::{element_error, prelude::*};
use gst::element_error;
use gst::prelude::*;
#[path = "../examples-common.rs"]
mod examples_common;
mod custom_meta {
use std::{fmt, mem};
use gst::prelude::*;
use std::fmt;
use std::mem;
// Public Rust type for the custom meta.
#[repr(transparent)]
@ -45,6 +45,7 @@ mod custom_meta {
}
// Retrieve the stored label.
#[doc(alias = "get_label")]
pub fn label(&self) -> &str {
self.0.label.as_str()
}
@ -69,9 +70,10 @@ mod custom_meta {
// Actual unsafe implementation of the meta.
mod imp {
use std::{mem, ptr};
use glib::translate::*;
use once_cell::sync::Lazy;
use std::mem;
use std::ptr;
pub(super) struct CustomMetaParams {
pub label: String,
@ -86,10 +88,8 @@ mod custom_meta {
// Function to register the meta API and get a type back.
pub(super) fn custom_meta_api_get_type() -> glib::Type {
static TYPE: std::sync::OnceLock<glib::Type> = std::sync::OnceLock::new();
*TYPE.get_or_init(|| unsafe {
let t = glib::Type::from_glib(gst::ffi::gst_meta_api_type_register(
static TYPE: Lazy<glib::Type> = Lazy::new(|| unsafe {
let t = from_glib(gst::ffi::gst_meta_api_type_register(
b"MyCustomMetaAPI\0".as_ptr() as *const _,
// We provide no tags here as our meta is just a label and does
// not refer to any specific aspect of the buffer.
@ -99,7 +99,9 @@ mod custom_meta {
assert_ne!(t, glib::Type::INVALID);
t
})
});
*TYPE
}
// Initialization function for our meta. This needs to ensure all fields are correctly
@ -156,24 +158,21 @@ mod custom_meta {
unsafe impl Send for MetaInfo {}
unsafe impl Sync for MetaInfo {}
static META_INFO: std::sync::OnceLock<MetaInfo> = std::sync::OnceLock::new();
static META_INFO: Lazy<MetaInfo> = Lazy::new(|| unsafe {
MetaInfo(
ptr::NonNull::new(gst::ffi::gst_meta_register(
custom_meta_api_get_type().into_glib(),
b"MyCustomMeta\0".as_ptr() as *const _,
mem::size_of::<CustomMeta>(),
Some(custom_meta_init),
Some(custom_meta_free),
Some(custom_meta_transform),
) as *mut gst::ffi::GstMetaInfo)
.expect("Failed to register meta API"),
)
});
META_INFO
.get_or_init(|| unsafe {
MetaInfo(
ptr::NonNull::new(gst::ffi::gst_meta_register(
custom_meta_api_get_type().into_glib(),
b"MyCustomMeta\0".as_ptr() as *const _,
mem::size_of::<CustomMeta>(),
Some(custom_meta_init),
Some(custom_meta_free),
Some(custom_meta_transform),
) as *mut gst::ffi::GstMetaInfo)
.expect("Failed to register meta API"),
)
})
.0
.as_ptr()
META_INFO.0.as_ptr()
}
}
}
@ -182,9 +181,15 @@ fn example_main() {
gst::init().unwrap();
// This creates a pipeline with appsrc and appsink.
let pipeline = gst::Pipeline::default();
let appsrc = gst_app::AppSrc::builder().build();
let appsink = gst_app::AppSink::builder().build();
let pipeline = gst::Pipeline::new(None);
let appsrc = gst::ElementFactory::make("appsrc", None)
.unwrap()
.downcast::<gst_app::AppSrc>()
.unwrap();
let appsink = gst::ElementFactory::make("appsink", None)
.unwrap()
.downcast::<gst_app::AppSink>()
.unwrap();
pipeline.add(&appsrc).unwrap();
pipeline.add(&appsink).unwrap();
@ -202,13 +207,13 @@ fn example_main() {
return;
}
println!("Producing buffer {i}");
println!("Producing buffer {}", i);
// Add a custom meta with a label to this buffer.
let mut buffer = gst::Buffer::new();
{
let buffer = buffer.get_mut().unwrap();
custom_meta::CustomMeta::add(buffer, format!("This is buffer {i}"));
custom_meta::CustomMeta::add(buffer, format!("This is buffer {}", i));
}
i += 1;

View file

@ -1,364 +0,0 @@
// This example demonstrates the use of the d3d11videosink's "present"
// signal and the use of Direct2D/DirectWrite APIs in Rust.
//
// Application can perform various hardware-accelerated 2D graphics operation
// (e.g., like cairo can support) and text rendering via the Windows APIs.
// In this example, 2D graphics operation and text rendering will happen
// directly to the on the DXGI swapchain's backbuffer via Windows API in
// strictly zero-copy manner
use std::{
collections::VecDeque,
sync::{Arc, Mutex},
time::SystemTime,
};
use gst::{glib, prelude::*};
use windows::{
core::*,
Win32::Graphics::{
Direct2D::{Common::*, *},
Direct3D11::*,
DirectWrite::*,
Dxgi::{Common::*, *},
},
};
struct OverlayContext {
d2d_factory: ID2D1Factory,
dwrite_factory: IDWriteFactory,
text_format: IDWriteTextFormat,
texture_desc: D3D11_TEXTURE2D_DESC,
text_layout: Option<IDWriteTextLayout>,
timestamp_queue: VecDeque<SystemTime>,
avg_fps: f32,
display_fps: f32,
font_size: f32,
}
fn create_overlay_context() -> Arc<Mutex<OverlayContext>> {
// Lots of DirectX APIs are marked as unsafe but the below operations
// are not expected to be failed unless GPU hang or device remove condition
// happens
let d2d_factory = unsafe {
D2D1CreateFactory::<ID2D1Factory>(D2D1_FACTORY_TYPE_MULTI_THREADED, None).unwrap()
};
let dwrite_factory =
unsafe { DWriteCreateFactory::<IDWriteFactory>(DWRITE_FACTORY_TYPE_SHARED).unwrap() };
// Font size can be updated later
let text_format = unsafe {
dwrite_factory
.CreateTextFormat(
w!("Consolas"),
None,
DWRITE_FONT_WEIGHT_REGULAR,
DWRITE_FONT_STYLE_NORMAL,
DWRITE_FONT_STRETCH_NORMAL,
12f32,
w!("en-us"),
)
.unwrap()
};
Arc::new(Mutex::new(OverlayContext {
d2d_factory,
dwrite_factory,
text_format,
texture_desc: D3D11_TEXTURE2D_DESC::default(),
text_layout: None,
timestamp_queue: VecDeque::with_capacity(10),
avg_fps: 0f32,
display_fps: 0f32,
font_size: 12f32,
}))
}
fn main() -> Result<()> {
gst::init().unwrap();
let args: Vec<String> = std::env::args().collect();
if args.len() != 2 {
println!("URI must be specified");
return Ok(());
}
let main_loop = glib::MainLoop::new(None, false);
let overlay_context = create_overlay_context();
let overlay_context_weak = Arc::downgrade(&overlay_context);
// Needs BGRA or RGBA swapchain for D2D interop,
// and "present" signal must be explicitly enabled
let videosink = gst::ElementFactory::make("d3d11videosink")
.property("emit-present", true)
.property_from_str("display-format", "DXGI_FORMAT_B8G8R8A8_UNORM")
.build()
.unwrap();
// Listen "present" signal and draw overlay from the callback
// Required operations here:
// 1) Gets IDXGISurface and ID3D11Texture2D interface from
// given ID3D11RenderTargetView COM object
// - ID3D11Texture2D: To get texture resolution
// - IDXGISurface: To create Direct2D render target
// 2) Creates or reuses IDWriteTextLayout interface
// - This object represents text layout we want to draw on render target
// 3) Draw rectangle (overlay background) and text on render target
//
// NOTE: ID2D1Factory, IDWriteFactory, IDWriteTextFormat, and
// IDWriteTextLayout objects are device-independent. Which can be created
// earlier instead of creating them in the callback.
// But ID2D1RenderTarget is a device-dependent resource.
// The client should not hold the d2d render target object outside of
// this callback scope because the resource must be cleared before
// releasing/resizing DXGI swapchain.
videosink.connect_closure(
"present",
false,
glib::closure!(move |_sink: &gst::Element,
_device: &gst::Object,
rtv_raw: glib::Pointer| {
let overlay_context = overlay_context_weak.upgrade().unwrap();
let mut context = overlay_context.lock().unwrap();
let dwrite_factory = context.dwrite_factory.clone();
let d2d_factory = context.d2d_factory.clone();
// SAFETY: transmute() below is clearly unsafe operation here.
// Regarding the other part of the below block, all DirectX
// APIs are marked as unsafe, except for cast.
//
// In theory, all the Direct3D/Direct2D APIs could fail for
// some reasons (it's hardware!), but in practice, it's very unexpected
// situation and any of failure below would mean we are doing
// something in wrong way or driver bug or so.
unsafe {
let rtv = ID3D11RenderTargetView::from_raw_borrowed(&rtv_raw).unwrap();
let resource = rtv.GetResource().unwrap();
let texture = resource.cast::<ID3D11Texture2D>().unwrap();
let desc = {
let mut desc = D3D11_TEXTURE2D_DESC::default();
texture.GetDesc(&mut desc);
desc
};
// Window size was updated, creates new text layout
let calculate_font_size = if desc != context.texture_desc {
context.texture_desc = desc;
context.text_layout = None;
true
} else {
false
};
// New fps, creates new layout
if context.avg_fps != context.display_fps {
context.display_fps = context.avg_fps;
context.text_layout = None;
}
if context.text_layout.is_none() {
let overlay_string = format!("TextOverlay, Fps {:.1}", context.display_fps);
let overlay_wstring = overlay_string.encode_utf16().collect::<Vec<_>>();
let layout = dwrite_factory
.CreateTextLayout(
&overlay_wstring,
&context.text_format,
desc.Width as f32,
desc.Height as f32 / 5f32,
)
.unwrap();
// Adjust alignment
layout
.SetTextAlignment(DWRITE_TEXT_ALIGNMENT_CENTER)
.unwrap();
layout
.SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_CENTER)
.unwrap();
// XXX: This is not an efficient approach.
// The font size can be pre-calculated for a pre-defined
// window size and string length
let mut range = DWRITE_TEXT_RANGE {
startPosition: 0u32,
length: overlay_wstring.len() as u32,
};
if calculate_font_size {
let mut font_size = 12f32;
let mut was_decreased = false;
loop {
let mut metrics = DWRITE_TEXT_METRICS::default();
layout.GetMetrics(&mut metrics).unwrap();
layout
.GetFontSize(0, &mut font_size, Some(&mut range))
.unwrap();
if metrics.widthIncludingTrailingWhitespace >= desc.Width as f32 {
if font_size > 1f32 {
font_size -= 0.5f32;
was_decreased = true;
layout.SetFontSize(font_size, range).unwrap();
continue;
}
break;
}
if was_decreased {
break;
}
if metrics.widthIncludingTrailingWhitespace < desc.Width as f32 {
if metrics.widthIncludingTrailingWhitespace
>= desc.Width as f32 * 0.7f32
{
break;
}
font_size += 0.5f32;
layout.SetFontSize(font_size, range).unwrap();
}
}
context.font_size = font_size;
} else {
layout.SetFontSize(context.font_size, range).unwrap();
}
context.text_layout = Some(layout);
};
let dxgi_surf = resource.cast::<IDXGISurface>().unwrap();
let render_target = d2d_factory
.CreateDxgiSurfaceRenderTarget(
&dxgi_surf,
&D2D1_RENDER_TARGET_PROPERTIES {
r#type: D2D1_RENDER_TARGET_TYPE_DEFAULT,
pixelFormat: D2D1_PIXEL_FORMAT {
format: DXGI_FORMAT_B8G8R8A8_UNORM,
alphaMode: D2D1_ALPHA_MODE_PREMULTIPLIED,
},
// zero means default DPI
dpiX: 0f32,
dpiY: 0f32,
usage: D2D1_RENDER_TARGET_USAGE_NONE,
minLevel: D2D1_FEATURE_LEVEL_DEFAULT,
},
)
.unwrap();
let text_brush = render_target
.CreateSolidColorBrush(
&D2D1_COLOR_F {
r: 0f32,
g: 0f32,
b: 0f32,
a: 1f32,
},
None,
)
.unwrap();
let overlay_brush = render_target
.CreateSolidColorBrush(
&D2D1_COLOR_F {
r: 0f32,
g: 0.5f32,
b: 0.5f32,
a: 0.3f32,
},
None,
)
.unwrap();
render_target.BeginDraw();
// Draws overlay background. It will blend overlay's background
// color with already rendred video frame
render_target.FillRectangle(
&D2D_RECT_F {
left: 0f32,
top: 0f32,
right: desc.Width as f32,
bottom: desc.Height as f32 / 5f32,
},
&overlay_brush,
);
// Then, renders text
render_target.DrawTextLayout(
D2D_POINT_2F { x: 0f32, y: 0f32 },
context.text_layout.as_ref(),
&text_brush,
D2D1_DRAW_TEXT_OPTIONS_NONE,
);
// EndDraw may not be successful for some reasons.
// Ignores any error in this example
let _ = render_target.EndDraw(None, None);
}
}),
);
// Add pad probe to calculate framerate
let sinkpad = videosink.static_pad("sink").unwrap();
let overlay_context_weak = Arc::downgrade(&overlay_context);
sinkpad.add_probe(gst::PadProbeType::BUFFER, move |_, probe_info| {
let overlay_context = overlay_context_weak.upgrade().unwrap();
let mut context = overlay_context.lock().unwrap();
context.timestamp_queue.push_back(SystemTime::now());
// Updates framerate per 10 frames
if context.timestamp_queue.len() >= 10 {
let now = context.timestamp_queue.back().unwrap();
let front = context.timestamp_queue.front().unwrap();
let duration = now.duration_since(*front).unwrap().as_millis() as f32;
context.avg_fps = 1000f32 * (context.timestamp_queue.len() - 1) as f32 / duration;
context.timestamp_queue.clear();
}
gst::PadProbeReturn::Ok
});
let playbin = gst::ElementFactory::make("playbin")
.property("uri", &args[1])
.property("video-sink", &videosink)
.build()
.unwrap();
let main_loop_clone = main_loop.clone();
let bus = playbin.bus().unwrap();
let _bus_watch = bus
.add_watch(move |_, msg| {
use gst::MessageView;
let main_loop = &main_loop_clone;
match msg.view() {
MessageView::Eos(..) => {
println!("received eos");
main_loop.quit()
}
MessageView::Error(err) => {
println!(
"Error from {:?}: {} ({:?})",
err.src().map(|s| s.path_string()),
err.error(),
err.debug()
);
main_loop.quit();
}
_ => (),
};
glib::ControlFlow::Continue
})
.unwrap();
playbin.set_state(gst::State::Playing).unwrap();
main_loop.run();
playbin.set_state(gst::State::Null).unwrap();
Ok(())
}

View file

@ -5,10 +5,11 @@
//
// It's possible to dump the logs at any time in an application,
// not just on exit like is done here.
use std::process;
use gst::gst_error;
use gst::prelude::*;
use std::process;
#[path = "../examples-common.rs"]
mod examples_common;
@ -19,21 +20,21 @@ fn example_main() {
/* Disable stdout debug, then configure the debug ringbuffer and enable
* all debug */
gst::log::remove_default_log_function();
gst::debug_remove_default_log_function();
/* Keep 1KB of logs per thread, removing old threads after 10 seconds */
gst::log::add_ring_buffer_logger(1024, 10);
gst::debug_add_ring_buffer_logger(1024, 10);
/* Enable all debug categories */
gst::log::set_default_threshold(gst::DebugLevel::Log);
gst::debug_set_default_threshold(gst::DebugLevel::Log);
let mut context = gst::ParseContext::new();
let pipeline =
match gst::parse::launch_full(pipeline_str, Some(&mut context), gst::ParseFlags::empty()) {
match gst::parse_launch_full(pipeline_str, Some(&mut context), gst::ParseFlags::empty()) {
Ok(pipeline) => pipeline,
Err(err) => {
if let Some(gst::ParseError::NoSuchElement) = err.kind::<gst::ParseError>() {
println!("Missing element(s): {:?}", context.missing_elements());
} else {
println!("Failed to parse pipeline: {err}");
println!("Failed to parse pipeline: {}", err);
}
process::exit(-1)
@ -70,11 +71,11 @@ fn example_main() {
.expect("Unable to set the pipeline to the `Null` state");
/* Insert a message into the debug log */
gst::error!(gst::CAT_DEFAULT, "Hi from the debug log ringbuffer example");
gst_error!(gst::CAT_DEFAULT, "Hi from the debug log ringbuffer example");
println!("Dumping debug logs\n");
for s in gst::log::ring_buffer_logger_get_logs().iter() {
println!("{s}\n------------------");
for s in gst::debug_ring_buffer_logger_get_logs().iter() {
println!("{}\n------------------", s);
}
}

View file

@ -29,28 +29,39 @@
// Especially Windows APIs tend to be quite picky about samplerate and sample-format.
// The same applies to videostreams.
use std::{
env,
sync::{Arc, Mutex},
};
use gst::element_error;
use gst::element_warning;
use gst::prelude::*;
#[cfg(feature = "v1_10")]
use glib::GBoxed;
use std::env;
#[cfg(feature = "v1_10")]
use std::sync::{Arc, Mutex};
use anyhow::Error;
use derive_more::{Display, Error};
use gst::{element_error, element_warning, prelude::*};
#[path = "../examples-common.rs"]
mod examples_common;
#[derive(Debug, Display, Error)]
#[display(fmt = "Received error from {src}: {error} (debug: {debug:?})")]
#[display(fmt = "Missing element {}", _0)]
struct MissingElement(#[error(not(source))] &'static str);
#[derive(Debug, Display, Error)]
#[display(fmt = "Received error from {}: {} (debug: {:?})", src, error, debug)]
struct ErrorMessage {
src: glib::GString,
error: glib::Error,
debug: Option<glib::GString>,
src: String,
error: String,
debug: Option<String>,
source: glib::Error,
}
#[derive(Clone, Debug, glib::Boxed)]
#[boxed_type(name = "ErrorValue")]
#[cfg(feature = "v1_10")]
#[derive(Clone, Debug, GBoxed)]
#[gboxed(type_name = "ErrorValue")]
struct ErrorValue(Arc<Mutex<Option<Error>>>);
fn example_main() -> Result<(), Error> {
@ -64,14 +75,16 @@ fn example_main() -> Result<(), Error> {
std::process::exit(-1)
};
let pipeline = gst::Pipeline::default();
let src = gst::ElementFactory::make("filesrc")
.property("location", uri)
.build()?;
let decodebin = gst::ElementFactory::make("decodebin").build()?;
let pipeline = gst::Pipeline::new(None);
let src = gst::ElementFactory::make("filesrc", None).map_err(|_| MissingElement("filesrc"))?;
let decodebin =
gst::ElementFactory::make("decodebin", None).map_err(|_| MissingElement("decodebin"))?;
pipeline.add_many([&src, &decodebin])?;
gst::Element::link_many([&src, &decodebin])?;
// Tell the filesrc what file to load
src.set_property("location", &uri)?;
pipeline.add_many(&[&src, &decodebin])?;
gst::Element::link_many(&[&src, &decodebin])?;
// Need to move a new reference into the closure.
// !!ATTENTION!!:
@ -90,8 +103,9 @@ fn example_main() -> Result<(), Error> {
decodebin.connect_pad_added(move |dbin, src_pad| {
// Here we temporarily retrieve a strong reference on the pipeline from the weak one
// we moved into this callback.
let Some(pipeline) = pipeline_weak.upgrade() else {
return;
let pipeline = match pipeline_weak.upgrade() {
Some(pipeline) => pipeline,
None => return,
};
// Try to detect whether the raw stream decodebin provided us with
@ -126,10 +140,14 @@ fn example_main() -> Result<(), Error> {
if is_audio {
// decodebin found a raw audiostream, so we build the follow-up pipeline to
// play it on the default audio playback device (using autoaudiosink).
let queue = gst::ElementFactory::make("queue").build()?;
let convert = gst::ElementFactory::make("audioconvert").build()?;
let resample = gst::ElementFactory::make("audioresample").build()?;
let sink = gst::ElementFactory::make("autoaudiosink").build()?;
let queue = gst::ElementFactory::make("queue", None)
.map_err(|_| MissingElement("queue"))?;
let convert = gst::ElementFactory::make("audioconvert", None)
.map_err(|_| MissingElement("audioconvert"))?;
let resample = gst::ElementFactory::make("audioresample", None)
.map_err(|_| MissingElement("audioresample"))?;
let sink = gst::ElementFactory::make("autoaudiosink", None)
.map_err(|_| MissingElement("autoaudiosink"))?;
let elements = &[&queue, &convert, &resample, &sink];
pipeline.add_many(elements)?;
@ -150,10 +168,14 @@ fn example_main() -> Result<(), Error> {
} else if is_video {
// decodebin found a raw videostream, so we build the follow-up pipeline to
// display it using the autovideosink.
let queue = gst::ElementFactory::make("queue").build()?;
let convert = gst::ElementFactory::make("videoconvert").build()?;
let scale = gst::ElementFactory::make("videoscale").build()?;
let sink = gst::ElementFactory::make("autovideosink").build()?;
let queue = gst::ElementFactory::make("queue", None)
.map_err(|_| MissingElement("queue"))?;
let convert = gst::ElementFactory::make("videoconvert", None)
.map_err(|_| MissingElement("videoconvert"))?;
let scale = gst::ElementFactory::make("videoscale", None)
.map_err(|_| MissingElement("videoscale"))?;
let sink = gst::ElementFactory::make("autovideosink", None)
.map_err(|_| MissingElement("autovideosink"))?;
let elements = &[&queue, &convert, &scale, &sink];
pipeline.add_many(elements)?;
@ -183,6 +205,7 @@ fn example_main() -> Result<(), Error> {
if let Err(err) = insert_sink(is_audio, is_video) {
// The following sends a message of type Error on the bus, containing our detailed
// error information.
#[cfg(feature = "v1_10")]
element_error!(
dbin,
gst::LibraryError::Failed,
@ -192,6 +215,14 @@ fn example_main() -> Result<(), Error> {
&ErrorValue(Arc::new(Mutex::new(Some(err)))))
.build()
);
#[cfg(not(feature = "v1_10"))]
element_error!(
dbin,
gst::LibraryError::Failed,
("Failed to insert sink"),
["{}", err]
);
}
});
@ -213,32 +244,49 @@ fn example_main() -> Result<(), Error> {
MessageView::Error(err) => {
pipeline.set_state(gst::State::Null)?;
match err.details() {
// This bus-message of type error contained our custom error-details struct
// that we sent in the pad-added callback above. So we unpack it and log
// the detailed error information here. details contains a glib::SendValue.
// The unpacked error is the converted to a Result::Err, stopping the
// application's execution.
Some(details) if details.name() == "error-details" => details
.get::<&ErrorValue>("error")
.unwrap()
.clone()
.0
.lock()
.unwrap()
.take()
.map(Result::Err)
.expect("error-details message without actual error"),
_ => Err(ErrorMessage {
#[cfg(feature = "v1_10")]
{
match err.details() {
// This bus-message of type error contained our custom error-details struct
// that we sent in the pad-added callback above. So we unpack it and log
// the detailed error information here. details contains a glib::SendValue.
// The unpacked error is the converted to a Result::Err, stopping the
// application's execution.
Some(details) if details.name() == "error-details" => details
.get::<&ErrorValue>("error")
.unwrap()
.clone()
.0
.lock()
.unwrap()
.take()
.map(Result::Err)
.expect("error-details message without actual error"),
_ => Err(ErrorMessage {
src: msg
.src()
.map(|s| String::from(s.path_string()))
.unwrap_or_else(|| String::from("None")),
error: err.error().to_string(),
debug: err.debug(),
source: err.error(),
}
.into()),
}?;
}
#[cfg(not(feature = "v1_10"))]
{
return Err(ErrorMessage {
src: msg
.src()
.map(|s| s.path_string())
.unwrap_or_else(|| glib::GString::from("UNKNOWN")),
error: err.error(),
.map(|s| String::from(s.path_string()))
.unwrap_or_else(|| String::from("None")),
error: err.error().to_string(),
debug: err.debug(),
source: err.error(),
}
.into()),
}?;
.into());
}
}
MessageView::StateChanged(s) => {
println!(
@ -263,6 +311,6 @@ fn main() {
// (but not necessary in normal Cocoa applications where this is set up automatically)
match examples_common::run(example_main) {
Ok(r) => r,
Err(e) => eprintln!("Error! {e}"),
Err(e) => eprintln!("Error! {}", e),
}
}

View file

@ -8,17 +8,21 @@
// Discovered information could for example contain the stream's duration or whether it is
// seekable (filesystem) or not (some http servers).
use std::env;
use gst_pbutils::prelude::*;
use gst_pbutils::DiscovererInfo;
use gst_pbutils::DiscovererStreamInfo;
use anyhow::Error;
use derive_more::{Display, Error};
use gst_pbutils::{prelude::*, DiscovererInfo, DiscovererStreamInfo};
use std::env;
#[path = "../examples-common.rs"]
mod examples_common;
#[derive(Debug, Display, Error)]
#[display(fmt = "Discoverer error {_0}")]
#[display(fmt = "Discoverer error {}", _0)]
struct DiscovererError(#[error(not(source))] &'static str);
fn print_tags(info: &DiscovererInfo) {
@ -27,7 +31,7 @@ fn print_tags(info: &DiscovererInfo) {
let tags = info.tags();
match tags {
Some(taglist) => {
println!(" {taglist}"); // FIXME use an iterator
println!(" {}", taglist.to_string()); // FIXME use an iterator
}
None => {
println!(" no tags");
@ -37,20 +41,21 @@ fn print_tags(info: &DiscovererInfo) {
fn print_stream_info(stream: &DiscovererStreamInfo) {
println!("Stream: ");
if let Some(stream_id) = stream.stream_id() {
println!(" Stream id: {}", stream_id);
if let Some(id) = stream.stream_id() {
println!(" Stream id: {}", id);
}
let caps_str = match stream.caps() {
Some(caps) => caps.to_string(),
None => String::from("--"),
};
println!(" Format: {caps_str}");
println!(" Format: {}", caps_str);
}
fn print_discoverer_info(info: &DiscovererInfo) -> Result<(), Error> {
println!("URI: {}", info.uri());
let uri = info
.uri()
.ok_or(DiscovererError("URI should not be null"))?;
println!("URI: {}", uri);
println!("Duration: {}", info.duration().display());
print_tags(info);
print_stream_info(
@ -89,7 +94,7 @@ fn run_discoverer() -> Result<(), Error> {
fn example_main() {
match run_discoverer() {
Ok(_) => (),
Err(e) => eprintln!("Error: {e}"),
Err(e) => eprintln!("Error: {}", e),
}
}

View file

@ -12,60 +12,74 @@
// {uridecodebin} -| {encodebin}-{filesink}
// \-{queue}-{videoconvert}-{videoscale}----/
use std::{
env,
sync::{Arc, Mutex},
};
use gst::element_error;
use gst::element_warning;
use gst_pbutils::prelude::*;
#[cfg(feature = "v1_10")]
use glib::GBoxed;
use std::env;
#[cfg(feature = "v1_10")]
use std::sync::{Arc, Mutex};
use anyhow::Error;
use derive_more::{Display, Error};
use gst::{element_error, element_warning};
use gst_pbutils::prelude::*;
#[path = "../examples-common.rs"]
mod examples_common;
#[derive(Debug, Display, Error)]
#[display(fmt = "Received error from {src}: {error} (debug: {debug:?})")]
#[display(fmt = "Missing element {}", _0)]
struct MissingElement(#[error(not(source))] &'static str);
#[derive(Debug, Display, Error)]
#[display(fmt = "Received error from {}: {} (debug: {:?})", src, error, debug)]
struct ErrorMessage {
src: glib::GString,
error: glib::Error,
debug: Option<glib::GString>,
src: String,
error: String,
debug: Option<String>,
source: glib::Error,
}
#[derive(Clone, Debug, glib::Boxed)]
#[boxed_type(name = "ErrorValue")]
#[cfg(feature = "v1_10")]
#[derive(Clone, Debug, GBoxed)]
#[gboxed(type_name = "ErrorValue")]
struct ErrorValue(Arc<Mutex<Option<Error>>>);
fn configure_encodebin(encodebin: &gst::Element) {
fn configure_encodebin(encodebin: &gst::Element) -> Result<(), Error> {
// To tell the encodebin what we want it to produce, we create an EncodingProfile
// https://gstreamer.freedesktop.org/data/doc/gstreamer/head/gst-plugins-base-libs/html/GstEncodingProfile.html
// This profile consists of information about the contained audio and video formats
// as well as the container format we want everything to be combined into.
// Every audiostream piped into the encodebin should be encoded using vorbis.
let audio_profile =
gst_pbutils::EncodingAudioProfile::builder(&gst::Caps::builder("audio/x-vorbis").build())
.presence(0)
.build();
let audio_profile = gst_pbutils::EncodingAudioProfileBuilder::new()
.format(&gst::Caps::new_simple("audio/x-vorbis", &[]))
.presence(0)
.build()?;
// Every videostream piped into the encodebin should be encoded using theora.
let video_profile =
gst_pbutils::EncodingVideoProfile::builder(&gst::Caps::builder("video/x-theora").build())
.presence(0)
.build();
let video_profile = gst_pbutils::EncodingVideoProfileBuilder::new()
.format(&gst::Caps::new_simple("video/x-theora", &[]))
.presence(0)
.build()?;
// All streams are then finally combined into a matroska container.
let container_profile = gst_pbutils::EncodingContainerProfile::builder(
&gst::Caps::builder("video/x-matroska").build(),
)
.name("container")
.add_profile(video_profile)
.add_profile(audio_profile)
.build();
let container_profile = gst_pbutils::EncodingContainerProfileBuilder::new()
.name("container")
.format(&gst::Caps::new_simple("video/x-matroska", &[]))
.add_profile(&(video_profile))
.add_profile(&(audio_profile))
.build()?;
// Finally, apply the EncodingProfile onto our encodebin element.
encodebin.set_property("profile", &container_profile);
encodebin
.set_property("profile", &container_profile)
.expect("set profile property failed");
Ok(())
}
fn example_main() -> Result<(), Error> {
@ -83,27 +97,31 @@ fn example_main() -> Result<(), Error> {
std::process::exit(-1)
};
let pipeline = gst::Pipeline::default();
let src = gst::ElementFactory::make("uridecodebin")
.property("uri", uri)
.build()?;
let encodebin = gst::ElementFactory::make("encodebin").build()?;
let sink = gst::ElementFactory::make("filesink")
.property("location", output_file)
.build()?;
let pipeline = gst::Pipeline::new(None);
let src = gst::ElementFactory::make("uridecodebin", None)
.map_err(|_| MissingElement("uridecodebin"))?;
let encodebin =
gst::ElementFactory::make("encodebin", None).map_err(|_| MissingElement("encodebin"))?;
let sink =
gst::ElementFactory::make("filesink", None).map_err(|_| MissingElement("filesink"))?;
src.set_property("uri", &uri)
.expect("setting URI Property failed");
sink.set_property("location", &output_file)
.expect("setting location property failed");
// Configure the encodebin.
// Here we tell the bin what format we expect it to create at its output.
configure_encodebin(&encodebin);
configure_encodebin(&encodebin)?;
pipeline
.add_many([&src, &encodebin, &sink])
.add_many(&[&src, &encodebin, &sink])
.expect("failed to add elements to pipeline");
// It is clear from the start, that encodebin has only one src pad, so we can
// directly link it to our filesink without problems.
// The caps of encodebin's src-pad are set after we configured the encoding-profile.
// (But filesink doesn't really care about the caps at its input anyway)
gst::Element::link_many([&encodebin, &sink])?;
gst::Element::link_many(&[&encodebin, &sink])?;
// Need to move a new reference into the closure.
// !!ATTENTION!!:
@ -120,8 +138,9 @@ fn example_main() -> Result<(), Error> {
src.connect_pad_added(move |dbin, dbin_src_pad| {
// Here we temporarily retrieve a strong reference on the pipeline from the weak one
// we moved into this callback.
let Some(pipeline) = pipeline_weak.upgrade() else {
return;
let pipeline = match pipeline_weak.upgrade() {
Some(pipeline) => pipeline,
None => return,
};
let (is_audio, is_video) = {
@ -148,9 +167,12 @@ fn example_main() -> Result<(), Error> {
let link_to_encodebin = |is_audio, is_video| -> Result<(), Error> {
if is_audio {
let queue = gst::ElementFactory::make("queue").build()?;
let convert = gst::ElementFactory::make("audioconvert").build()?;
let resample = gst::ElementFactory::make("audioresample").build()?;
let queue = gst::ElementFactory::make("queue", None)
.map_err(|_| MissingElement("queue"))?;
let convert = gst::ElementFactory::make("audioconvert", None)
.map_err(|_| MissingElement("audioconvert"))?;
let resample = gst::ElementFactory::make("audioresample", None)
.map_err(|_| MissingElement("audioresample"))?;
let elements = &[&queue, &convert, &resample];
pipeline
@ -176,9 +198,12 @@ fn example_main() -> Result<(), Error> {
let sink_pad = queue.static_pad("sink").expect("queue has no sinkpad");
dbin_src_pad.link(&sink_pad)?;
} else if is_video {
let queue = gst::ElementFactory::make("queue").build()?;
let convert = gst::ElementFactory::make("videoconvert").build()?;
let scale = gst::ElementFactory::make("videoscale").build()?;
let queue = gst::ElementFactory::make("queue", None)
.map_err(|_| MissingElement("queue"))?;
let convert = gst::ElementFactory::make("videoconvert", None)
.map_err(|_| MissingElement("videoconvert"))?;
let scale = gst::ElementFactory::make("videoscale", None)
.map_err(|_| MissingElement("videoscale"))?;
let elements = &[&queue, &convert, &scale];
pipeline
@ -188,7 +213,7 @@ fn example_main() -> Result<(), Error> {
// Request a sink pad from our encodebin, that can handle a raw videostream.
// The encodebin will then automatically create an internal pipeline, that encodes
// the video stream in the format we specified in the EncodingProfile.
// the audio stream in the format we specified in the EncodingProfile.
let enc_sink_pad = encodebin
.request_pad_simple("video_%u")
.expect("Could not get video pad from encodebin");
@ -209,6 +234,7 @@ fn example_main() -> Result<(), Error> {
};
if let Err(err) = link_to_encodebin(is_audio, is_video) {
#[cfg(feature = "v1_10")]
element_error!(
dbin,
gst::LibraryError::Failed,
@ -218,6 +244,14 @@ fn example_main() -> Result<(), Error> {
&ErrorValue(Arc::new(Mutex::new(Some(err)))))
.build()
);
#[cfg(not(feature = "v1_10"))]
element_error!(
dbin,
gst::LibraryError::Failed,
("Failed to insert sink"),
["{}", err]
);
}
});
@ -235,27 +269,44 @@ fn example_main() -> Result<(), Error> {
MessageView::Error(err) => {
pipeline.set_state(gst::State::Null)?;
match err.details() {
Some(details) if details.name() == "error-details" => details
.get::<&ErrorValue>("error")
.unwrap()
.clone()
.0
.lock()
.unwrap()
.take()
.map(Result::Err)
.expect("error-details message without actual error"),
_ => Err(ErrorMessage {
#[cfg(feature = "v1_10")]
{
match err.details() {
Some(details) if details.name() == "error-details" => details
.get::<&ErrorValue>("error")
.unwrap()
.clone()
.0
.lock()
.unwrap()
.take()
.map(Result::Err)
.expect("error-details message without actual error"),
_ => Err(ErrorMessage {
src: msg
.src()
.map(|s| String::from(s.path_string()))
.unwrap_or_else(|| String::from("None")),
error: err.error().to_string(),
debug: err.debug(),
source: err.error(),
}
.into()),
}?;
}
#[cfg(not(feature = "v1_10"))]
{
return Err(ErrorMessage {
src: msg
.src()
.map(|s| s.path_string())
.unwrap_or_else(|| glib::GString::from("UNKNOWN")),
error: err.error(),
.map(|s| String::from(s.path_string()))
.unwrap_or_else(|| String::from("None")),
error: err.error().to_string(),
debug: err.debug(),
source: err.error(),
}
.into()),
}?;
.into());
}
}
MessageView::StateChanged(s) => {
println!(
@ -280,6 +331,6 @@ fn main() {
// (but not necessary in normal Cocoa applications where this is set up automatically)
match examples_common::run(example_main) {
Ok(r) => r,
Err(e) => eprintln!("Error! {e}"),
Err(e) => eprintln!("Error! {}", e),
}
}

View file

@ -30,7 +30,7 @@ fn example_main() {
let main_loop = glib::MainLoop::new(None, false);
// This creates a pipeline by parsing the gst-launch pipeline syntax.
let pipeline = gst::parse::launch("audiotestsrc ! fakesink").unwrap();
let pipeline = gst::parse_launch("audiotestsrc ! fakesink").unwrap();
let bus = pipeline.bus().unwrap();
pipeline
@ -50,13 +50,14 @@ fn example_main() {
// Add a timeout to the main loop. This closure will be executed
// in an interval of 5 seconds. The return value of the handler function
// determines whether the handler still wants to be called:
// - glib::ControlFlow::Break - stop calling this handler, remove timeout
// - glib::ControlFlow::Continue- continue calling this handler
// - glib::Continue(false) - stop calling this handler, remove timeout
// - glib::Continue(true) - continue calling this handler
glib::timeout_add_seconds(5, move || {
// Here we temporarily retrieve a strong reference on the pipeline from the weak one
// we moved into this callback.
let Some(pipeline) = pipeline_weak.upgrade() else {
return glib::ControlFlow::Break;
let pipeline = match pipeline_weak.upgrade() {
Some(pipeline) => pipeline,
None => return glib::Continue(false),
};
println!("sending eos");
@ -76,7 +77,7 @@ fn example_main() {
// Remove this handler, the pipeline will shutdown anyway, now that we
// sent the EOS event.
glib::ControlFlow::Break
glib::Continue(false)
});
//bus.add_signal_watch();
@ -84,36 +85,35 @@ fn example_main() {
let main_loop_clone = main_loop.clone();
// This sets the bus's signal handler (don't be mislead by the "add", there can only be one).
// Every message from the bus is passed through this function. Its returnvalue determines
// whether the handler wants to be called again. If glib::ControlFlow::Break is returned, the
// whether the handler wants to be called again. If glib::Continue(false) is returned, the
// handler is removed and will never be called again. The mainloop still runs though.
let _bus_watch = bus
.add_watch(move |_, msg| {
use gst::MessageView;
bus.add_watch(move |_, msg| {
use gst::MessageView;
let main_loop = &main_loop_clone;
match msg.view() {
MessageView::Eos(..) => {
println!("received eos");
// An EndOfStream event was sent to the pipeline, so we tell our main loop
// to stop execution here.
main_loop.quit()
}
MessageView::Error(err) => {
println!(
"Error from {:?}: {} ({:?})",
err.src().map(|s| s.path_string()),
err.error(),
err.debug()
);
main_loop.quit();
}
_ => (),
};
let main_loop = &main_loop_clone;
match msg.view() {
MessageView::Eos(..) => {
println!("received eos");
// An EndOfStream event was sent to the pipeline, so we tell our main loop
// to stop execution here.
main_loop.quit()
}
MessageView::Error(err) => {
println!(
"Error from {:?}: {} ({:?})",
err.src().map(|s| s.path_string()),
err.error(),
err.debug()
);
main_loop.quit();
}
_ => (),
};
// Tell the mainloop to continue executing this callback.
glib::ControlFlow::Continue
})
.expect("Failed to add bus watch");
// Tell the mainloop to continue executing this callback.
glib::Continue(true)
})
.expect("Failed to add bus watch");
// Operate GStreamer's bus, facilliating GLib's mainloop here.
// This function call will block until you tell the mainloop to quit
@ -123,6 +123,11 @@ fn example_main() {
pipeline
.set_state(gst::State::Null)
.expect("Unable to set the pipeline to the `Null` state");
// Remove the watch function from the bus.
// Again: There can always only be one watch function.
// Thus we don't have to tell him which function to remove.
bus.remove_watch().unwrap();
}
fn main() {

View file

@ -1,481 +0,0 @@
// This example demonstrates the use of the FdMemory allocator.
// It operates the following two pipelines:
// sender: {videotestsrc} - {appsink}
// receiver: {appsrc} - {FdMemoryVideoFilter} - {videoconvert} - {queue} - {autovideosink}
// The sender creates shared memory files from the appsink which are sent
// to the receiver using a unix domain socket.
// The receiver creates buffers in the appsrc using the FdMemoryAllocator from
// the received file descriptors.
// Additional to demonstrating how the FdMemoryAllocator can be used to share
// file descriptors the example implements a custom VideoFilter demonstrating
// how the file descriptor of FdMemory can be accessed in a pipeline.
// Note that instead of manual mapping the file descriptor it is also possible
// to use map_writable, which will also map the file descriptor.
use std::{
os::unix::{net::UnixStream, prelude::AsRawFd},
sync::{Arc, Mutex},
};
use anyhow::Error;
use futures::StreamExt;
use gst::{element_error, prelude::*};
use memmap2::MmapMut;
use uds::UnixStreamExt;
#[path = "../examples-common.rs"]
mod examples_common;
fn create_receiver_pipeline(
video_info: &gst_video::VideoInfo,
receiver: UnixStream,
) -> Result<gst::Pipeline, Error> {
let caps = video_info.to_caps()?;
let pipeline = gst::Pipeline::default();
let src = gst_app::AppSrc::builder()
.caps(&caps)
.do_timestamp(true)
.is_live(true)
.build();
let filter = video_filter::FdMemoryFadeInVideoFilter::default().upcast::<gst::Element>();
let convert = gst::ElementFactory::make("videoconvert").build()?;
let queue = gst::ElementFactory::make("queue").build()?;
let sink = gst::ElementFactory::make("autovideosink").build()?;
pipeline.add_many([src.upcast_ref(), &filter, &convert, &queue, &sink])?;
gst::Element::link_many([src.upcast_ref(), &filter, &convert, &queue, &sink])?;
let fd_allocator = gst_allocators::FdAllocator::new();
let video_info = video_info.clone();
let mut fd_buf = [-1; 253];
src.set_callbacks(
gst_app::AppSrcCallbacks::builder()
.need_data(move |appsrc, _| {
// Read the next fds from the socket, if 0
// is returned the sender has closed the stream
// which is handled as EOS here.
let fds = match receiver.recv_fds(&mut [0u8; 1], &mut fd_buf) {
Ok((_, 0)) => {
let _ = appsrc.end_of_stream();
return;
}
Ok((_, fds)) => fds,
Err(err) => {
gst::error_msg!(
gst::StreamError::Failed,
("failed to receive fds: {}", err)
);
return;
}
};
for fd in &fd_buf[0..fds] {
// Allocate a new FdMemory for the received file descriptor.
// It is important that the size matches the size of the
// actual backing storage. In this example we just use the
// same video info in both sides, sending and receiving.
// Pass FdMemoryFlags::NONE to make the FdMemory take
// ownership of the passed file descriptor. The file descriptor
// will be closed when the memory is released.
let memory = unsafe {
fd_allocator
.alloc(*fd, video_info.size(), gst_allocators::FdMemoryFlags::NONE)
.unwrap()
};
let mut buffer = gst::Buffer::new();
let buffer_mut = buffer.make_mut();
buffer_mut.append_memory(memory);
let _ = appsrc.push_buffer(buffer);
}
})
.build(),
);
Ok(pipeline)
}
fn create_sender_pipeline(
video_info: &gst_video::VideoInfo,
sender: UnixStream,
) -> Result<gst::Pipeline, Error> {
let sender = Arc::new(Mutex::new(sender));
let caps = video_info.to_caps()?;
let pipeline = gst::Pipeline::default();
let src = gst::ElementFactory::make("videotestsrc")
.property("num-buffers", 250i32)
.build()?;
let sink = gst::ElementFactory::make("appsink").build()?;
sink.downcast_ref::<gst_app::AppSink>()
.ok_or_else(|| anyhow::anyhow!("is not a appsink"))?
.set_caps(Some(&caps));
pipeline.add_many([&src, &sink])?;
gst::Element::link_many([&src, &sink])?;
let appsink = sink
.downcast::<gst_app::AppSink>()
.map_err(|_| anyhow::anyhow!("is not a appsink"))?;
appsink.set_callbacks(
gst_app::AppSinkCallbacks::builder()
// Add a handler to the "eos" signal
.eos({
let sender = sender.clone();
move |_| {
// Close the sender part of the UnixSocket pair, this will automatically
// create a eos in the receiving part.
let _ = sender.lock().unwrap().shutdown(std::net::Shutdown::Write);
}
})
// Add a handler to the "new-sample" signal.
.new_sample(move |appsink| {
// Pull the sample in question out of the appsink's buffer.
let sample = appsink.pull_sample().map_err(|_| gst::FlowError::Eos)?;
let buffer = sample.buffer().ok_or_else(|| {
element_error!(
appsink,
gst::ResourceError::Failed,
("Failed to get buffer from appsink")
);
gst::FlowError::Error
})?;
if buffer.n_memory() != 1 {
element_error!(
appsink,
gst::ResourceError::Failed,
("Expected buffer with single memory")
);
return Err(gst::FlowError::Error);
}
let mem = buffer.peek_memory(0);
// We can use downcast_memory_ref to check if the provided
// memory is allocated by FdMemoryAllocator or a subtype of it.
// Note: This is not used in the example, we will always copy
// the memory to a new shared memory file.
if let Some(fd_memory) = mem.downcast_memory_ref::<gst_allocators::FdMemory>() {
// As we already got a fd we can just directly send it over the socket.
// NOTE: Synchronization is left out of this example, in a real world
// application access to the memory should be synchronized.
// For example wayland provides a release callback to signal that
// the memory is no longer in use.
sender
.lock()
.unwrap()
.send_fds(&[0u8; 1], &[fd_memory.fd()])
.map_err(|_| {
element_error!(
appsink,
gst::ResourceError::Failed,
("Failed to send fd over unix stream")
);
gst::FlowError::Error
})?;
} else {
// At this point, buffer is only a reference to an existing memory region somewhere.
// When we want to access its content, we have to map it while requesting the required
// mode of access (read, read/write).
// This type of abstraction is necessary, because the buffer in question might not be
// on the machine's main memory itself, but rather in the GPU's memory.
// So mapping the buffer makes the underlying memory region accessible to us.
// See: https://gstreamer.freedesktop.org/documentation/plugin-development/advanced/allocation.html
let map = buffer.map_readable().map_err(|_| {
element_error!(
appsink,
gst::ResourceError::Failed,
("Failed to map buffer readable")
);
gst::FlowError::Error
})?;
// Note: To simplify this example we always create a new shared memory file instead
// of using a pool of buffers. When using a pool we need to make sure access to the
// file is synchronized.
let opts = memfd::MemfdOptions::default().allow_sealing(true);
let mfd = opts.create("gst-examples").map_err(|err| {
element_error!(
appsink,
gst::ResourceError::Failed,
("Failed to allocated fd: {}", err)
);
gst::FlowError::Error
})?;
mfd.as_file().set_len(map.size() as u64).map_err(|err| {
element_error!(
appsink,
gst::ResourceError::Failed,
("Failed to resize fd memory: {}", err)
);
gst::FlowError::Error
})?;
let mut seals = memfd::SealsHashSet::new();
seals.insert(memfd::FileSeal::SealShrink);
seals.insert(memfd::FileSeal::SealGrow);
mfd.add_seals(&seals).map_err(|err| {
element_error!(
appsink,
gst::ResourceError::Failed,
("Failed to add fd seals: {}", err)
);
gst::FlowError::Error
})?;
mfd.add_seal(memfd::FileSeal::SealSeal).map_err(|err| {
element_error!(
appsink,
gst::ResourceError::Failed,
("Failed to add fd seals: {}", err)
);
gst::FlowError::Error
})?;
unsafe {
let mut mmap = MmapMut::map_mut(mfd.as_file()).map_err(|_| {
element_error!(
appsink,
gst::ResourceError::Failed,
("Failed to mmap fd")
);
gst::FlowError::Error
})?;
mmap.copy_from_slice(map.as_slice());
};
sender
.lock()
.unwrap()
.send_fds(&[0u8; 1], &[mfd.as_raw_fd()])
.map_err(|_| {
element_error!(
appsink,
gst::ResourceError::Failed,
("Failed to send fd over unix stream")
);
gst::FlowError::Error
})?;
};
Ok(gst::FlowSuccess::Ok)
})
.build(),
);
Ok(pipeline)
}
async fn message_loop(bus: gst::Bus) {
let mut messages = bus.stream();
while let Some(msg) = messages.next().await {
use gst::MessageView;
// Determine whether we want to quit: on EOS or error message
// we quit, otherwise simply continue.
match msg.view() {
MessageView::Eos(..) => break,
MessageView::Error(err) => {
println!(
"Error from {:?}: {} ({:?})",
err.src().map(|s| s.path_string()),
err.error(),
err.debug()
);
break;
}
_ => (),
};
}
}
fn example_main() -> Result<(), Error> {
gst::init()?;
let video_info = gst_video::VideoInfo::builder(gst_video::VideoFormat::Bgra, 1920, 1080)
.fps(gst::Fraction::new(30, 1))
.build()?;
let (sender, receiver) = std::os::unix::net::UnixStream::pair()?;
let sender_pipeline = create_sender_pipeline(&video_info, sender)?;
let receiver_pipeline = create_receiver_pipeline(&video_info, receiver)?;
let receiver_bus = receiver_pipeline.bus().expect("pipeline without bus");
receiver_pipeline.set_state(gst::State::Playing)?;
let sender_bus = sender_pipeline.bus().expect("pipeline without bus");
sender_pipeline.set_state(gst::State::Playing)?;
futures::executor::block_on(futures::future::join(
message_loop(sender_bus),
message_loop(receiver_bus),
));
sender_pipeline.set_state(gst::State::Null)?;
receiver_pipeline.set_state(gst::State::Null)?;
Ok(())
}
fn main() {
// tutorials_common::run is only required to set up the application environment on macOS
// (but not necessary in normal Cocoa applications where this is set up automatically)
match examples_common::run(example_main) {
Ok(r) => r,
Err(e) => eprintln!("Error! {e}"),
}
}
// The purpose of this custom video filter is to demonstrate how
// the file descriptor of a FdMemory can be accessed.
mod video_filter {
glib::wrapper! {
pub struct FdMemoryFadeInVideoFilter(ObjectSubclass<imp::FdMemoryFadeInVideoFilter>) @extends gst_video::VideoFilter, gst_base::BaseTransform, gst::Element, gst::Object;
}
impl Default for FdMemoryFadeInVideoFilter {
fn default() -> Self {
glib::Object::builder().build()
}
}
mod imp {
use std::{mem::ManuallyDrop, os::unix::prelude::FromRawFd};
use anyhow::Error;
use gst::{subclass::prelude::*, PadDirection, PadPresence, PadTemplate};
use gst_app::gst_base::subclass::BaseTransformMode;
use gst_video::{prelude::*, subclass::prelude::*, VideoFrameRef};
use memmap2::MmapMut;
use once_cell::sync::Lazy;
static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
gst::DebugCategory::new(
"fdmemoryfilter",
gst::DebugColorFlags::empty(),
Some("Example FdMemory filter"),
)
});
#[derive(Debug, Default)]
pub struct FdMemoryFadeInVideoFilter;
impl FdMemoryFadeInVideoFilter {
fn transform_fd_mem_ip(
&self,
frame: &mut VideoFrameRef<&mut gst::BufferRef>,
) -> Result<(), Error> {
let buffer = frame.buffer();
if buffer.n_memory() != 1 {
return Err(anyhow::anyhow!(
"only buffers with single memory are supported"
));
}
let mem = buffer.peek_memory(0);
if !mem.is_memory_type::<gst_allocators::FdMemory>() {
return Err(anyhow::anyhow!("only fd memory is supported"));
}
let timestamp = buffer.pts().unwrap();
let factor = (timestamp.nseconds() as f64
/ (5 * gst::ClockTime::SECOND).nseconds() as f64)
.min(1.0f64);
// If the fade-in has finished return early
if factor >= 1.0f64 {
return Ok(());
}
let fd = mem
.downcast_memory_ref::<gst_allocators::FdMemory>()
.unwrap()
.fd();
unsafe {
// We wrap the Memmfd in ManuallyDrop here because from_raw_fd takes ownership of
// the file descriptor which would close it on drop
//
// see: https://github.com/lucab/memfd-rs/issues/29
let mfd = ManuallyDrop::new(memfd::Memfd::from_raw_fd(fd));
let mut mmap = MmapMut::map_mut(mfd.as_file())?;
for pixel in mmap.chunks_exact_mut(4) {
pixel[0] = (pixel[0] as f64 * factor).clamp(0.0, 255.0) as u8;
pixel[1] = (pixel[1] as f64 * factor).clamp(0.0, 255.0) as u8;
pixel[2] = (pixel[2] as f64 * factor).clamp(0.0, 255.0) as u8;
}
}
Ok(())
}
}
impl ElementImpl for FdMemoryFadeInVideoFilter {
fn pad_templates() -> &'static [PadTemplate] {
static PAD_TEMPLATES: std::sync::OnceLock<Vec<PadTemplate>> =
std::sync::OnceLock::new();
PAD_TEMPLATES.get_or_init(|| {
let caps = gst_video::VideoCapsBuilder::new()
.format(gst_video::VideoFormat::Bgra)
.build();
vec![
PadTemplate::new("sink", PadDirection::Sink, PadPresence::Always, &caps)
.unwrap(),
PadTemplate::new("src", PadDirection::Src, PadPresence::Always, &caps)
.unwrap(),
]
})
}
}
impl BaseTransformImpl for FdMemoryFadeInVideoFilter {
const MODE: BaseTransformMode = BaseTransformMode::AlwaysInPlace;
const PASSTHROUGH_ON_SAME_CAPS: bool = false;
const TRANSFORM_IP_ON_PASSTHROUGH: bool = true;
}
impl VideoFilterImpl for FdMemoryFadeInVideoFilter {
fn transform_frame_ip(
&self,
frame: &mut VideoFrameRef<&mut gst::BufferRef>,
) -> Result<gst::FlowSuccess, gst::FlowError> {
self.transform_fd_mem_ip(frame).map_err(|err| {
gst::error!(CAT, imp: self, "Failed to transform frame`: {}", err);
gst::FlowError::Error
})?;
Ok(gst::FlowSuccess::Ok)
}
}
impl ObjectImpl for FdMemoryFadeInVideoFilter {}
impl GstObjectImpl for FdMemoryFadeInVideoFilter {}
#[glib::object_subclass]
impl ObjectSubclass for FdMemoryFadeInVideoFilter {
const NAME: &'static str = "FdMemoryVideoFilter";
type Type = super::FdMemoryFadeInVideoFilter;
type ParentType = gst_video::VideoFilter;
}
}
}

View file

@ -3,11 +3,13 @@
// or for an EOS message. When a message notifying about either of both
// is received, the future is resolved.
use std::env;
use futures::{executor::LocalPool, prelude::*};
use gst::prelude::*;
use futures::executor::LocalPool;
use futures::prelude::*;
use std::env;
#[path = "../examples-common.rs"]
mod examples_common;
@ -42,7 +44,7 @@ fn example_main() {
gst::init().unwrap();
// Create a pipeline from the launch-syntax given on the cli.
let pipeline = gst::parse::launch(&pipeline_str).unwrap();
let pipeline = gst::parse_launch(&pipeline_str).unwrap();
let bus = pipeline.bus().unwrap();
pipeline

View file

@ -35,44 +35,17 @@
// those with lowers (higher number). Thus, Layers with higher priority are "in the front".
// - The timeline is the enclosing element, grouping all layers and providing a timeframe.
use std::env;
use gst::prelude::*;
use ges::prelude::*;
use std::env;
#[allow(unused_imports)]
#[path = "../examples-common.rs"]
mod examples_common;
fn configure_pipeline(pipeline: &ges::Pipeline, output_name: &str) {
// Every audiostream piped into the encodebin should be encoded using opus.
let audio_profile =
gst_pbutils::EncodingAudioProfile::builder(&gst::Caps::builder("audio/x-opus").build())
.build();
// Every videostream piped into the encodebin should be encoded using vp8.
let video_profile =
gst_pbutils::EncodingVideoProfile::builder(&gst::Caps::builder("video/x-vp8").build())
.build();
// All streams are then finally combined into a webm container.
let container_profile =
gst_pbutils::EncodingContainerProfile::builder(&gst::Caps::builder("video/webm").build())
.name("container")
.add_profile(video_profile)
.add_profile(audio_profile)
.build();
// Apply the EncodingProfile to the pipeline, and set it to render mode
let output_uri = format!("{output_name}.webm");
pipeline
.set_render_settings(&output_uri, &container_profile)
.expect("Failed to set render settings");
pipeline
.set_mode(ges::PipelineFlags::RENDER)
.expect("Failed to set pipeline to render mode");
}
fn main_loop(uri: &str, output: Option<&String>) -> Result<(), glib::BoolError> {
fn main_loop(uri: &str) -> Result<(), glib::BoolError> {
ges::init()?;
// Begin by creating a timeline with audio and video tracks
@ -82,11 +55,6 @@ fn main_loop(uri: &str, output: Option<&String>) -> Result<(), glib::BoolError>
let pipeline = ges::Pipeline::new();
pipeline.set_timeline(&timeline)?;
// If requested, configure the pipeline so it renders to a file.
if let Some(output_name) = output {
configure_pipeline(&pipeline, output_name);
}
// Load a clip from the given uri and add it to the layer.
let clip = ges::UriClip::new(uri).expect("Failed to create clip");
layer.add_clip(&clip)?;
@ -158,17 +126,16 @@ fn main_loop(uri: &str, output: Option<&String>) -> Result<(), glib::BoolError>
#[allow(unused_variables)]
fn example_main() {
let args: Vec<_> = env::args().collect();
if args.len() < 2 || args.len() > 3 {
println!("Usage: ges input [output]");
let uri: &str = if args.len() == 2 {
args[1].as_ref()
} else {
println!("Usage: ges launch");
std::process::exit(-1)
}
};
let input_uri: &str = args[1].as_ref();
let output = args.get(2);
match main_loop(input_uri, output) {
match main_loop(uri) {
Ok(r) => r,
Err(e) => eprintln!("Error! {e}"),
Err(e) => eprintln!("Error! {}", e),
}
}

View file

@ -1,7 +1,3 @@
#![allow(clippy::non_send_fields_in_send_ty)]
use anyhow::Result;
#[path = "../glupload.rs"]
mod glupload;
use glupload::*;
@ -30,17 +26,15 @@ void main () {
"#;
mod mirror {
use std::sync::Mutex;
use gst_base::subclass::BaseTransformMode;
use gst_gl::{
prelude::*,
subclass::{prelude::*, GLFilterMode},
*,
};
use once_cell::sync::Lazy;
use super::{gl, FRAGMENT_SHADER};
use gst_base::subclass::BaseTransformMode;
use gst_gl::prelude::*;
use gst_gl::subclass::prelude::*;
use gst_gl::subclass::GLFilterMode;
use gst_gl::*;
use once_cell::sync::Lazy;
use std::sync::Mutex;
pub static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
gst::DebugCategory::new(
@ -56,7 +50,7 @@ mod mirror {
impl GLMirrorFilter {
pub fn new(name: Option<&str>) -> Self {
glib::Object::builder().property("name", name).build()
glib::Object::new(&[("name", &name)]).expect("Failed to create GL Mirror Filter Object")
}
}
@ -71,16 +65,20 @@ mod mirror {
}
impl GLMirrorFilter {
fn create_shader(&self, context: &GLContext) -> Result<(), gst::LoggableError> {
fn create_shader(
&self,
filter: &<Self as ObjectSubclass>::Type,
context: &GLContext,
) -> Result<(), gst::LoggableError> {
let shader = GLShader::new(context);
let vertex = GLSLStage::new_default_vertex(context);
vertex.compile().unwrap();
shader.attach_unlocked(&vertex)?;
gst::debug!(
gst::gst_debug!(
CAT,
imp: self,
obj: filter,
"Compiling fragment shader {}",
FRAGMENT_SHADER
);
@ -97,9 +95,9 @@ mod mirror {
shader.attach_unlocked(&fragment)?;
shader.link().unwrap();
gst::debug!(
gst::gst_debug!(
CAT,
imp: self,
obj: filter,
"Successfully compiled and linked {:?}",
shader
);
@ -119,7 +117,6 @@ mod mirror {
}
impl ElementImpl for GLMirrorFilter {}
impl GstObjectImpl for GLMirrorFilter {}
impl ObjectImpl for GLMirrorFilter {}
impl BaseTransformImpl for GLMirrorFilter {
const MODE: BaseTransformMode = BaseTransformMode::NeverInPlace;
@ -127,14 +124,12 @@ mod mirror {
const TRANSFORM_IP_ON_PASSTHROUGH: bool = false;
}
impl GLBaseFilterImpl for GLMirrorFilter {
fn gl_start(&self) -> Result<(), gst::LoggableError> {
let filter = self.obj();
fn gl_start(&self, filter: &Self::Type) -> Result<(), gst::LoggableError> {
// Create a shader when GL is started, knowing that the OpenGL context is
// available.
let context = GLBaseFilterExt::context(&*filter).unwrap();
self.create_shader(&context)?;
self.parent_gl_start()
let context = GLBaseFilterExt::context(filter).unwrap();
self.create_shader(filter, &context)?;
self.parent_gl_start(filter)
}
}
impl GLFilterImpl for GLMirrorFilter {
@ -142,11 +137,10 @@ mod mirror {
fn filter_texture(
&self,
filter: &Self::Type,
input: &gst_gl::GLMemory,
output: &gst_gl::GLMemory,
) -> Result<(), gst::LoggableError> {
let filter = self.obj();
let shader = self.shader.lock().unwrap();
// Use the underlying filter implementation to transform the input texture into
// an output texture with the shader.
@ -157,18 +151,20 @@ mod mirror {
.as_ref()
.expect("No shader, call `create_shader` first!"),
);
self.parent_filter_texture(input, output)
self.parent_filter_texture(filter, input, output)
}
}
}
}
fn example_main() -> Result<()> {
fn example_main() {
gst::init().unwrap();
let glfilter = mirror::GLMirrorFilter::new(Some("Mirror filter"));
App::new(Some(glfilter.as_ref())).and_then(main_loop)
let glfilter = mirror::GLMirrorFilter::new(Some("foo"));
App::new(Some(glfilter.as_ref()))
.and_then(main_loop)
.unwrap_or_else(|e| eprintln!("Error! {}", e))
}
fn main() -> Result<()> {
examples_common::run(example_main)
fn main() {
examples_common::run(example_main);
}

View file

@ -1,7 +1,8 @@
use std::env;
use gst::prelude::*;
use futures::prelude::*;
use gst::prelude::*;
use std::env;
#[path = "../examples-common.rs"]
mod examples_common;
@ -34,6 +35,7 @@ fn example_main() {
// Get the default main context and make it also the thread default, then create
// a main loop for it
let ctx = glib::MainContext::default();
ctx.push_thread_default();
let loop_ = glib::MainLoop::new(Some(&ctx), false);
// Read the pipeline to launch from the commandline, using the launch syntax.
@ -42,7 +44,7 @@ fn example_main() {
gst::init().unwrap();
// Create a pipeline from the launch-syntax given on the cli.
let pipeline = gst::parse::launch(&pipeline_str).unwrap();
let pipeline = gst::parse_launch(&pipeline_str).unwrap();
let bus = pipeline.bus().unwrap();
pipeline
@ -59,6 +61,8 @@ fn example_main() {
pipeline
.set_state(gst::State::Null)
.expect("Unable to set the pipeline to the `Null` state");
ctx.pop_thread_default();
}
fn main() {

View file

@ -1,7 +1,3 @@
#![allow(clippy::non_send_fields_in_send_ty)]
use anyhow::Result;
#[path = "../glupload.rs"]
mod glupload;
use glupload::*;
@ -9,10 +5,12 @@ use glupload::*;
#[path = "../examples-common.rs"]
pub mod examples_common;
fn example_main() -> Result<()> {
App::new(None).and_then(main_loop)
fn example_main() {
App::new(None)
.and_then(main_loop)
.unwrap_or_else(|e| eprintln!("Error! {}", e))
}
fn main() -> Result<()> {
examples_common::run(example_main)
fn main() {
examples_common::run(example_main);
}

177
examples/src/bin/gtksink.rs Normal file
View file

@ -0,0 +1,177 @@
// This example demonstrates how to use gstreamer in conjunction with the gtk widget toolkit.
// This example shows the video produced by a videotestsrc within a small gtk gui.
// For this, the gtkglsink is used, which creates a gtk widget one can embed the gtk gui.
// For this, there multiple types of widgets. gtkglsink uses OpenGL to render frames, and
// gtksink uses the CPU to render the frames (which is way slower).
// So the example application first tries to use OpenGL, and when that fails, fall back.
// The pipeline looks like the following:
// gtk-gui: {gtkglsink}-widget
// (|)
// {videotestsrc} - {glsinkbin}
use gst::prelude::*;
use gio::prelude::*;
use gtk::prelude::*;
use std::cell::RefCell;
fn create_ui(app: &gtk::Application) {
let pipeline = gst::Pipeline::new(None);
let src = gst::ElementFactory::make("videotestsrc", None).unwrap();
// Create the gtk sink and retrieve the widget from it. The sink element will be used
// in the pipeline, and the widget will be embedded in our gui.
// Gstreamer then displays frames in the gtk widget.
// First, we try to use the OpenGL version - and if that fails, we fall back to non-OpenGL.
let (sink, widget) = if let Ok(gtkglsink) = gst::ElementFactory::make("gtkglsink", None) {
// Using the OpenGL widget succeeded, so we are in for a nice playback experience with
// low cpu usage. :)
// The gtkglsink essentially allocates an OpenGL texture on the GPU, that it will display.
// Now we create the glsinkbin element, which is responsible for conversions and for uploading
// video frames to our texture (if they are not already in the GPU). Now we tell the OpenGL-sink
// about our gtkglsink element, form where it will retrieve the OpenGL texture to fill.
let glsinkbin = gst::ElementFactory::make("glsinkbin", None).unwrap();
glsinkbin.set_property("sink", &gtkglsink).unwrap();
// The gtkglsink creates the gtk widget for us. This is accessible through a property.
// So we get it and use it later to add it to our gui.
let widget = gtkglsink.property("widget").unwrap();
(glsinkbin, widget.get::<gtk::Widget>().unwrap())
} else {
// Unfortunately, using the OpenGL widget didn't work out, so we will have to render
// our frames manually, using the CPU. An example why this may fail is, when
// the PC doesn't have proper graphics drivers installed.
let sink = gst::ElementFactory::make("gtksink", None).unwrap();
// The gtksink creates the gtk widget for us. This is accessible through a property.
// So we get it and use it later to add it to our gui.
let widget = sink.property("widget").unwrap();
(sink, widget.get::<gtk::Widget>().unwrap())
};
pipeline.add_many(&[&src, &sink]).unwrap();
src.link(&sink).unwrap();
// Create a simple gtk gui window to place our widget into.
let window = gtk::Window::new(gtk::WindowType::Toplevel);
window.set_default_size(320, 240);
let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0);
// Add our widget to the gui
vbox.pack_start(&widget, true, true, 0);
let label = gtk::Label::new(Some("Position: 00:00:00"));
vbox.pack_start(&label, true, true, 5);
window.add(&vbox);
window.show_all();
app.add_window(&window);
// Need to move a new reference into the closure.
// !!ATTENTION!!:
// It might seem appealing to use pipeline.clone() here, because that greatly
// simplifies the code within the callback. What this actually does, however, is creating
// a memory leak. The clone of a pipeline is a new strong reference on the pipeline.
// Storing this strong reference of the pipeline within the callback (we are moving it in!),
// which is in turn stored in another strong reference on the pipeline is creating a
// reference cycle.
// DO NOT USE pipeline.clone() TO USE THE PIPELINE WITHIN A CALLBACK
let pipeline_weak = pipeline.downgrade();
// Add a timeout to the main loop that will periodically (every 500ms) be
// executed. This will query the current position within the stream from
// the underlying pipeline, and display it in our gui.
// Since this closure is called by the mainloop thread, we are allowed
// to modify the gui widgets here.
let timeout_id = glib::timeout_add_local(std::time::Duration::from_millis(500), move || {
// Here we temporarily retrieve a strong reference on the pipeline from the weak one
// we moved into this callback.
let pipeline = match pipeline_weak.upgrade() {
Some(pipeline) => pipeline,
None => return glib::Continue(true),
};
// Query the current playing position from the underlying pipeline.
let position = pipeline.query_position::<gst::ClockTime>();
// Display the playing position in the gui.
label.set_text(&format!("Position: {:.0}", position.display()));
// Tell the callback to continue calling this closure.
glib::Continue(true)
});
let bus = pipeline.bus().unwrap();
pipeline
.set_state(gst::State::Playing)
.expect("Unable to set the pipeline to the `Playing` state");
let app_weak = app.downgrade();
bus.add_watch_local(move |_, msg| {
use gst::MessageView;
let app = match app_weak.upgrade() {
Some(app) => app,
None => return glib::Continue(false),
};
match msg.view() {
MessageView::Eos(..) => app.quit(),
MessageView::Error(err) => {
println!(
"Error from {:?}: {} ({:?})",
err.src().map(|s| s.path_string()),
err.error(),
err.debug()
);
app.quit();
}
_ => (),
};
glib::Continue(true)
})
.expect("Failed to add bus watch");
// Pipeline reference is owned by the closure below, so will be
// destroyed once the app is destroyed
let timeout_id = RefCell::new(Some(timeout_id));
let pipeline = RefCell::new(Some(pipeline));
app.connect_shutdown(move |_| {
// Optional, by manually destroying the window here we ensure that
// the gst element is destroyed when shutting down instead of having to wait
// for the process to terminate, allowing us to use the leaks tracer.
unsafe {
window.destroy();
}
// GTK will keep the Application alive for the whole process lifetime.
// Wrapping the pipeline in a RefCell<Option<_>> and removing it from it here
// ensures the pipeline is actually destroyed when shutting down, allowing us
// to use the leaks tracer for example.
if let Some(pipeline) = pipeline.borrow_mut().take() {
pipeline
.set_state(gst::State::Null)
.expect("Unable to set the pipeline to the `Null` state");
pipeline.bus().unwrap().remove_watch().unwrap();
}
if let Some(timeout_id) = timeout_id.borrow_mut().take() {
glib::source_remove(timeout_id);
}
});
}
fn main() {
// Initialize gstreamer and the gtk widget toolkit libraries.
gst::init().unwrap();
gtk::init().unwrap();
{
let app = gtk::Application::new(None, gio::ApplicationFlags::FLAGS_NONE);
app.connect_activate(create_ui);
app.run();
}
// Optional, can be used to detect leaks using the leaks tracer
unsafe {
gst::deinit();
}
}

View file

@ -0,0 +1,288 @@
// This example demonstrates another type of combination of gtk and gstreamer,
// in comparision to the gtksink example.
// This example uses regions that are managed by the window system, and uses
// the window system's api to insert a videostream into these regions.
// So essentially, the window system of the system overlays our gui with
// the video frames - within the region that we tell it to use.
// Disadvantage of this method is, that it's highly platform specific, since
// the big platforms all have their own window system. Thus, this example
// has special code to handle differences between platforms.
// Windows could theoretically be supported by this example, but is not yet implemented.
// One of the very few (if not the single one) platform, that can not provide the API
// needed for this are Linux desktops using Wayland.
// TODO: Add Windows support
// In this case, a testvideo is displayed within our gui, using the
// following pipeline:
// {videotestsrc} - {xvimagesink(on linux)}
// {videotestsrc} - {glimagesink(on mac)}
use gst_video::prelude::*;
use gio::prelude::*;
use gtk::prelude::*;
use std::os::raw::c_void;
use std::cell::RefCell;
use std::process;
#[cfg(all(target_os = "linux", feature = "gtkvideooverlay-x11"))]
fn create_video_sink() -> gst::Element {
// When we are on linux with the Xorg display server, we use the
// X11 protocol's XV extension, which allows to overlay regions
// with video streams. For this, we use the xvimagesink element.
gst::ElementFactory::make("xvimagesink", None).unwrap()
}
#[cfg(all(target_os = "linux", feature = "gtkvideooverlay-x11"))]
fn set_window_handle(video_overlay: &gst_video::VideoOverlay, gdk_window: &gdk::Window) {
let display_type_name = gdk_window.display().type_().name();
// Check if we're using X11 or ...
if display_type_name == "GdkX11Display" {
extern "C" {
pub fn gdk_x11_window_get_xid(window: *mut glib::object::GObject) -> *mut c_void;
}
// This is unsafe because the "window handle" we pass here is basically like a raw pointer.
// If a wrong value were to be passed here (and you can pass any integer), then the window
// system will most likely cause the application to crash.
#[allow(clippy::cast_ptr_alignment)]
unsafe {
// Here we ask gdk what native window handle we got assigned for
// our video region from the window system, and then we will
// pass this unique identifier to the overlay provided by our
// sink - so the sink can then arrange the overlay.
let xid = gdk_x11_window_get_xid(gdk_window.as_ptr() as *mut _);
video_overlay.set_window_handle(xid as usize);
}
} else {
println!("Add support for display type '{}'", display_type_name);
process::exit(-1);
}
}
#[cfg(all(target_os = "macos", feature = "gtkvideooverlay-quartz"))]
fn create_video_sink() -> gst::Element {
// On Mac, this is done by overlaying a window region with an
// OpenGL-texture, using the glimagesink element.
gst::ElementFactory::make("glimagesink", None).unwrap()
}
#[cfg(all(target_os = "macos", feature = "gtkvideooverlay-quartz"))]
fn set_window_handle(video_overlay: &gst_video::VideoOverlay, gdk_window: &gdk::Window) {
let display_type_name = gdk_window.display().type_().name();
if display_type_name == "GdkQuartzDisplay" {
extern "C" {
pub fn gdk_quartz_window_get_nsview(window: *mut glib::object::GObject) -> *mut c_void;
}
// This is unsafe because the "window handle" we pass here is basically like a raw pointer.
// If a wrong value were to be passed here (and you can pass any integer), then the window
// system will most likely cause the application to crash.
#[allow(clippy::cast_ptr_alignment)]
unsafe {
// Here we ask gdk what native window handle we got assigned for
// our video region from the windowing system, and then we will
// pass this unique identifier to the overlay provided by our
// sink - so the sink can then arrange the overlay.
let window = gdk_quartz_window_get_nsview(gdk_window.as_ptr() as *mut _);
video_overlay.set_window_handle(window as usize);
}
} else {
println!("Unsupported display type '{}", display_type_name);
process::exit(-1);
}
}
fn create_ui(app: &gtk::Application) {
let pipeline = gst::Pipeline::new(None);
let src = gst::ElementFactory::make("videotestsrc", None).unwrap();
// Since using the window system to overlay our gui window is making
// direct contact with the windowing system, this is highly platform-
// specific. This example supports Linux and Mac (using X11 and Quartz).
let sink = create_video_sink();
pipeline.add_many(&[&src, &sink]).unwrap();
src.link(&sink).unwrap();
// First, we create our gtk window - which will contain a region where
// our overlayed video will be displayed in.
let window = gtk::Window::new(gtk::WindowType::Toplevel);
window.set_default_size(320, 240);
let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0);
// This creates the widget we will display our overlay in.
// Later, we will try to tell our window system about this region, so
// it can overlay it with our video stream.
let video_window = gtk::DrawingArea::new();
video_window.set_size_request(320, 240);
// Use the platform-specific sink to create our overlay.
// Since we only use the video_overlay in the closure below, we need a weak reference.
// !!ATTENTION!!:
// It might seem appealing to use .clone() here, because that greatly
// simplifies the code within the callback. What this actually does, however, is creating
// a memory leak.
let video_overlay = sink
.dynamic_cast::<gst_video::VideoOverlay>()
.unwrap()
.downgrade();
// Connect to this widget's realize signal, which will be emitted
// after its display has been initialized. This is neccessary, because
// the window system doesn't know about our region until it was initialized.
video_window.connect_realize(move |video_window| {
// Here we temporarily retrieve a strong reference on the video-overlay from the
// weak reference that we moved into the closure.
let video_overlay = match video_overlay.upgrade() {
Some(video_overlay) => video_overlay,
None => return,
};
// Gtk uses gdk under the hood, to handle its drawing. Drawing regions are
// called gdk windows. We request this underlying drawing region from the
// widget we will overlay with our video.
let gdk_window = video_window.window().unwrap();
// This is where we tell our window system about the drawing-region we
// want it to overlay. Most often, the window system would only know
// about our most outer region (or: our window).
if !gdk_window.ensure_native() {
println!("Can't create native window for widget");
process::exit(-1);
}
set_window_handle(&video_overlay, &gdk_window);
});
vbox.pack_start(&video_window, true, true, 0);
let label = gtk::Label::new(Some("Position: 00:00:00"));
vbox.pack_start(&label, true, true, 5);
window.add(&vbox);
window.show_all();
app.add_window(&window);
// Need to move a new reference into the closure.
// !!ATTENTION!!:
// It might seem appealing to use pipeline.clone() here, because that greatly
// simplifies the code within the callback. What this actually does, however, is creating
// a memory leak. The clone of a pipeline is a new strong reference on the pipeline.
// Storing this strong reference of the pipeline within the callback (we are moving it in!),
// which is in turn stored in another strong reference on the pipeline is creating a
// reference cycle.
// DO NOT USE pipeline.clone() TO USE THE PIPELINE WITHIN A CALLBACK
let pipeline_weak = pipeline.downgrade();
// Add a timeout to the main loop that will periodically (every 500ms) be
// executed. This will query the current position within the stream from
// the underlying pipeline, and display it in our gui.
// Since this closure is called by the mainloop thread, we are allowed
// to modify the gui widgets here.
let timeout_id = glib::timeout_add_local(std::time::Duration::from_millis(500), move || {
// Here we temporarily retrieve a strong reference on the pipeline from the weak one
// we moved into this callback.
let pipeline = match pipeline_weak.upgrade() {
Some(pipeline) => pipeline,
None => return glib::Continue(false),
};
// Query the current playing position from the underlying pipeline.
let position = pipeline.query_position::<gst::ClockTime>();
// Display the playing position in the gui.
label.set_text(&format!("Position: {:.0}", position.display()));
// Tell the timeout to continue calling this callback.
glib::Continue(true)
});
let bus = pipeline.bus().unwrap();
pipeline
.set_state(gst::State::Playing)
.expect("Unable to set the pipeline to the `Playing` state");
let app_weak = app.downgrade();
bus.add_watch_local(move |_, msg| {
use gst::MessageView;
let app = match app_weak.upgrade() {
Some(app) => app,
None => return glib::Continue(false),
};
match msg.view() {
MessageView::Eos(..) => app.quit(),
MessageView::Error(err) => {
println!(
"Error from {:?}: {} ({:?})",
err.src().map(|s| s.path_string()),
err.error(),
err.debug()
);
app.quit();
}
_ => (),
};
glib::Continue(true)
})
.expect("Failed to add bus watch");
// Pipeline reference is owned by the closure below, so will be
// destroyed once the app is destroyed
let timeout_id = RefCell::new(Some(timeout_id));
let pipeline = RefCell::new(Some(pipeline));
app.connect_shutdown(move |_| {
// Optional, by manually destroying the window here we ensure that
// the gst element is destroyed when shutting down instead of having to wait
// for the process to terminate, allowing us to use the leaks tracer.
unsafe {
window.destroy();
}
// GTK will keep the Application alive for the whole process lifetime.
// Wrapping the pipeline in a RefCell<Option<_>> and removing it from it here
// ensures the pipeline is actually destroyed when shutting down, allowing us
// to use the leaks tracer for example.
if let Some(pipeline) = pipeline.borrow_mut().take() {
pipeline
.set_state(gst::State::Null)
.expect("Unable to set the pipeline to the `Null` state");
pipeline.bus().unwrap().remove_watch().unwrap();
}
if let Some(timeout_id) = timeout_id.borrow_mut().take() {
glib::source_remove(timeout_id);
}
});
}
fn main() {
#[cfg(not(unix))]
{
println!("Add support for target platform");
process::exit(-1);
}
// Initialize gstreamer and the gtk widget toolkit libraries.
gst::init().unwrap();
gtk::init().unwrap();
{
let app = gtk::Application::new(None, gio::ApplicationFlags::FLAGS_NONE);
app.connect_activate(create_ui);
app.run();
}
// Optional, can be used to detect leaks using the leaks tracer
unsafe {
gst::deinit();
}
}

View file

@ -1,5 +1,5 @@
// This example demonstrates how to use GStreamer's iteration APIs.
// This is used at multiple occasions - for example to iterate an
// This is used at multiple occassions - for example to iterate an
// element's pads.
use gst::prelude::*;
@ -13,7 +13,7 @@ fn example_main() {
// Create and use an identity element here.
// This element does nothing, really. We also never add it to a pipeline.
// We just want to iterate the identity element's pads.
let identity = gst::ElementFactory::make("identity").build().unwrap();
let identity = gst::ElementFactory::make("identity", None).unwrap();
// Get an iterator over all pads of the identity-element.
let mut iter = identity.iterate_pads();
loop {

View file

@ -3,10 +3,11 @@
// as launch syntax.
// When the parsing succeeded, the pipeline is run until the stream ends or an error happens.
use std::{env, process};
use gst::prelude::*;
use std::env;
use std::process;
#[path = "../examples-common.rs"]
mod examples_common;
@ -17,7 +18,7 @@ fn example_main() {
gst::init().unwrap();
// Let GStreamer create a pipeline from the parsed launch syntax on the cli.
// In comparison to the launch_glib_main example, this is using the advanced launch syntax
// In comparision to the launch_glib_main example, this is using the advanced launch syntax
// parsing API of GStreamer. The function returns a Result, handing us the pipeline if
// parsing and creating succeeded, and hands us detailed error information if something
// went wrong. The error is passed as gst::ParseError. In this example, we separately
@ -26,22 +27,19 @@ fn example_main() {
// Especially GUIs should probably handle this case, to tell users that they need to
// install the corresponding gstreamer plugins.
let mut context = gst::ParseContext::new();
let pipeline = match gst::parse::launch_full(
&pipeline_str,
Some(&mut context),
gst::ParseFlags::empty(),
) {
Ok(pipeline) => pipeline,
Err(err) => {
if let Some(gst::ParseError::NoSuchElement) = err.kind::<gst::ParseError>() {
println!("Missing element(s): {:?}", context.missing_elements());
} else {
println!("Failed to parse pipeline: {err}");
}
let pipeline =
match gst::parse_launch_full(&pipeline_str, Some(&mut context), gst::ParseFlags::empty()) {
Ok(pipeline) => pipeline,
Err(err) => {
if let Some(gst::ParseError::NoSuchElement) = err.kind::<gst::ParseError>() {
println!("Missing element(s): {:?}", context.missing_elements());
} else {
println!("Failed to parse pipeline: {}", err);
}
process::exit(-1)
}
};
process::exit(-1)
}
};
let bus = pipeline.bus().unwrap();
pipeline

View file

@ -7,10 +7,10 @@
// things from the main loop (timeouts, UI events, socket events, ...) instead
// of just handling messages from GStreamer's bus.
use std::env;
use gst::prelude::*;
use std::env;
#[path = "../examples-common.rs"]
mod examples_common;
@ -24,7 +24,7 @@ fn example_main() {
let main_loop = glib::MainLoop::new(None, false);
// Let GStreamer create a pipeline from the parsed launch syntax on the cli.
let pipeline = gst::parse::launch(&pipeline_str).unwrap();
let pipeline = gst::parse_launch(&pipeline_str).unwrap();
let bus = pipeline.bus().unwrap();
pipeline
@ -35,34 +35,38 @@ fn example_main() {
//bus.add_signal_watch();
//bus.connect_message(None, move |_, msg| {
let _bus_watch = bus
.add_watch(move |_, msg| {
use gst::MessageView;
bus.add_watch(move |_, msg| {
use gst::MessageView;
let main_loop = &main_loop_clone;
match msg.view() {
MessageView::Eos(..) => main_loop.quit(),
MessageView::Error(err) => {
println!(
"Error from {:?}: {} ({:?})",
err.src().map(|s| s.path_string()),
err.error(),
err.debug()
);
main_loop.quit();
}
_ => (),
};
let main_loop = &main_loop_clone;
match msg.view() {
MessageView::Eos(..) => main_loop.quit(),
MessageView::Error(err) => {
println!(
"Error from {:?}: {} ({:?})",
err.src().map(|s| s.path_string()),
err.error(),
err.debug()
);
main_loop.quit();
}
_ => (),
};
glib::ControlFlow::Continue
})
.expect("Failed to add bus watch");
glib::Continue(true)
})
.expect("Failed to add bus watch");
main_loop.run();
pipeline
.set_state(gst::State::Null)
.expect("Unable to set the pipeline to the `Null` state");
// Here we remove the bus watch we added above. This avoids a memory leak, that might
// otherwise happen because we moved a strong reference (clone of main_loop) into the
// callback closure above.
bus.remove_watch().unwrap();
}
fn main() {

View file

@ -1,359 +0,0 @@
// This example demonstrates how to draw an overlay on a video stream using
// Direct2D/DirectWrite/WIC and the overlay composition element.
// {videotestsrc} - {overlaycomposition} - {capsfilter} - {videoconvert} - {autovideosink}
// The capsfilter element allows us to dictate the video resolution we want for the
// videotestsrc and the overlaycomposition element.
use std::sync::{Arc, Mutex};
use byte_slice_cast::*;
use anyhow::Error;
use derive_more::{Display, Error};
use gst::prelude::*;
use windows::{
Foundation::Numerics::*,
Win32::{
Graphics::{
Direct2D::{Common::*, *},
DirectWrite::*,
Dxgi::Common::*,
Imaging::*,
},
System::Com::*,
},
};
#[derive(Debug, Display, Error)]
#[display(fmt = "Received error from {}: {} (debug: {:?})", src, error, debug)]
struct ErrorMessage {
src: glib::GString,
error: glib::Error,
debug: Option<glib::GString>,
}
struct DrawingContext {
// Factory for creating render target
d2d_factory: ID2D1Factory,
// Used to create WIC bitmap surface
wic_factory: IWICImagingFactory,
// text layout holding text information (string, font, size, etc)
text_layout: IDWriteTextLayout,
// Holding rendred image
bitmap: Option<IWICBitmap>,
// Bound to bitmap and used to actual Direct2D rendering
render_target: Option<ID2D1RenderTarget>,
info: Option<gst_video::VideoInfo>,
}
// Required for IWICBitmap
unsafe impl Send for DrawingContext {}
fn create_pipeline() -> Result<gst::Pipeline, Error> {
gst::init()?;
let pipeline = gst::Pipeline::default();
// The videotestsrc supports multiple test patterns. In this example, we will use the
// pattern with a white ball moving around the video's center point.
let src = gst::ElementFactory::make("videotestsrc")
.property_from_str("pattern", "ball")
.build()?;
let overlay = gst::ElementFactory::make("overlaycomposition").build()?;
let caps = gst_video::VideoCapsBuilder::new()
.width(800)
.height(800)
.framerate((30, 1).into())
.build();
let capsfilter = gst::ElementFactory::make("capsfilter")
.property("caps", &caps)
.build()?;
let videoconvert = gst::ElementFactory::make("videoconvert").build()?;
let sink = gst::ElementFactory::make("autovideosink").build()?;
pipeline.add_many(&[&src, &overlay, &capsfilter, &videoconvert, &sink])?;
gst::Element::link_many(&[&src, &overlay, &capsfilter, &videoconvert, &sink])?;
// Most Direct2D/DirectWrite APIs (including factory methods) are marked as
// "unsafe", but they shouldn't fail in practice
let drawer = unsafe {
let d2d_factory =
D2D1CreateFactory::<ID2D1Factory>(D2D1_FACTORY_TYPE_MULTI_THREADED, None).unwrap();
let dwrite_factory =
DWriteCreateFactory::<IDWriteFactory>(DWRITE_FACTORY_TYPE_SHARED).unwrap();
let text_format = dwrite_factory
.CreateTextFormat(
windows::core::w!("Arial"),
None,
DWRITE_FONT_WEIGHT_BOLD,
DWRITE_FONT_STYLE_NORMAL,
DWRITE_FONT_STRETCH_NORMAL,
32f32,
windows::core::w!("en-us"),
)
.unwrap();
let text_layout = dwrite_factory
.CreateTextLayout(
windows::core::w!("GStreamer").as_wide(),
&text_format,
// Size will be updated later on "caps-changed" signal
800f32,
800f32,
)
.unwrap();
// Top (default) and center alignment
text_layout
.SetTextAlignment(DWRITE_TEXT_ALIGNMENT_CENTER)
.unwrap();
let wic_factory: IWICImagingFactory =
CoCreateInstance(&CLSID_WICImagingFactory, None, CLSCTX_ALL).unwrap();
Arc::new(Mutex::new(DrawingContext {
d2d_factory,
wic_factory,
text_layout,
bitmap: None,
render_target: None,
info: None,
}))
};
overlay.connect_closure(
"draw",
false,
glib::closure!(@strong drawer => move |_overlay: &gst::Element,
sample: &gst::Sample| {
use std::f64::consts::PI;
let drawer = drawer.lock().unwrap();
let buffer = sample.buffer().unwrap();
let timestamp = buffer.pts().unwrap();
let info = drawer.info.as_ref().unwrap();
let text_layout = &drawer.text_layout;
let bitmap = drawer.bitmap.as_ref().unwrap();
let render_target = drawer.render_target.as_ref().unwrap();
let global_angle = 360. * (timestamp % (10 * gst::ClockTime::SECOND)).nseconds() as f64
/ (10.0 * gst::ClockTime::SECOND.nseconds() as f64);
let center_x = (info.width() / 2) as f32;
let center_y = (info.height() / 2) as f32;
let top_margin = (info.height() / 20) as f32;
unsafe {
// Begin drawing
render_target.BeginDraw();
// Clear background
render_target.Clear(Some(&D2D1_COLOR_F {
r: 0f32,
g: 0f32,
b: 0f32,
a: 0f32,
}));
// This loop will render 10 times the string "GStreamer" in a circle
for i in 0..10 {
let angle = (360. * f64::from(i)) / 10.0;
let red = ((1.0 + f64::cos((angle - 60.0) * PI / 180.0)) / 2.0) as f32;
let text_brush = render_target
.CreateSolidColorBrush(
&D2D1_COLOR_F {
r: red,
g: 0f32,
b: 1f32 - red,
a: 1f32,
},
None,
)
.unwrap();
let angle = (angle + global_angle) as f32;
let matrix = Matrix3x2::rotation(angle, center_x, center_y);
render_target.SetTransform(&matrix);
render_target.DrawTextLayout(
D2D_POINT_2F { x: 0f32, y: top_margin },
text_layout,
&text_brush,
D2D1_DRAW_TEXT_OPTIONS_NONE,
);
}
// EndDraw may not be successful for some reasons.
// Ignores any error in this example
let _ = render_target.EndDraw(None, None);
// Make sure all operations is completed before copying
// bitmap to buffer
let _ = render_target.Flush(None::<*mut u64>, None::<*mut u64>);
}
let mut buffer = gst::Buffer::with_size((info.width() * info.height() * 4) as usize).unwrap();
{
let buffer_mut = buffer.get_mut().unwrap();
let mut map = buffer_mut.map_writable().unwrap();
let dst = map.as_mut_slice_of::<u8>().unwrap();
unsafe {
// Bitmap size is equal to the background image size.
// Copy entire memory
bitmap.CopyPixels(std::ptr::null(), info.width() * 4, dst).unwrap();
}
}
gst_video::VideoMeta::add_full(
buffer.get_mut().unwrap(),
gst_video::VideoFrameFlags::empty(),
gst_video::VideoFormat::Bgra,
info.width(),
info.height(),
&[0],
&[(info.width() * 4) as i32],
)
.unwrap();
// Turn the buffer into a VideoOverlayRectangle, then place
// that into a VideoOverlayComposition and return it.
//
// A VideoOverlayComposition can take a Vec of such rectangles
// spaced around the video frame, but we're just outputting 1
// here
let rect = gst_video::VideoOverlayRectangle::new_raw(
&buffer,
0,
0,
info.width(),
info.height(),
gst_video::VideoOverlayFormatFlags::PREMULTIPLIED_ALPHA,
);
gst_video::VideoOverlayComposition::new(Some(&rect))
.unwrap()
}),
);
// Add a signal handler to the overlay's "caps-changed" signal. This could e.g.
// be called when the sink that we render to does not support resizing the image
// itself - but the user just changed the window-size. The element after the overlay
// will then change its caps and we use the notification about this change to
// resize our canvas's size.
// Another possibility for when this might happen is, when our video is a network
// stream that dynamically changes resolution when enough bandwidth is available.
overlay.connect_closure(
"caps-changed",
false,
glib::closure!(move |_overlay: &gst::Element,
caps: &gst::Caps,
_width: u32,
_height: u32| {
let mut drawer = drawer.lock().unwrap();
let info = gst_video::VideoInfo::from_caps(caps).unwrap();
unsafe {
// Update text layout to be identical to new video resolution
drawer.text_layout.SetMaxWidth(info.width() as f32).unwrap();
drawer
.text_layout
.SetMaxHeight(info.height() as f32)
.unwrap();
// Create new WIC bitmap with PBGRA format (pre-multiplied BGRA)
let bitmap = drawer
.wic_factory
.CreateBitmap(
info.width(),
info.height(),
&GUID_WICPixelFormat32bppPBGRA,
WICBitmapCacheOnDemand,
)
.unwrap();
let render_target = drawer
.d2d_factory
.CreateWicBitmapRenderTarget(
&bitmap,
&D2D1_RENDER_TARGET_PROPERTIES {
r#type: D2D1_RENDER_TARGET_TYPE_DEFAULT,
pixelFormat: D2D1_PIXEL_FORMAT {
format: DXGI_FORMAT_B8G8R8A8_UNORM,
alphaMode: D2D1_ALPHA_MODE_PREMULTIPLIED,
},
// zero means default DPI
dpiX: 0f32,
dpiY: 0f32,
usage: D2D1_RENDER_TARGET_USAGE_NONE,
minLevel: D2D1_FEATURE_LEVEL_DEFAULT,
},
)
.unwrap();
drawer.render_target = Some(render_target);
drawer.bitmap = Some(bitmap);
}
drawer.info = Some(info);
}),
);
Ok(pipeline)
}
fn main_loop(pipeline: gst::Pipeline) -> Result<(), Error> {
pipeline.set_state(gst::State::Playing)?;
let bus = pipeline
.bus()
.expect("Pipeline without bus. Shouldn't happen!");
for msg in bus.iter_timed(gst::ClockTime::NONE) {
use gst::MessageView;
match msg.view() {
MessageView::Eos(..) => break,
MessageView::Error(err) => {
pipeline.set_state(gst::State::Null)?;
return Err(ErrorMessage {
src: msg
.src()
.map(|s| s.path_string())
.unwrap_or_else(|| glib::GString::from("UNKNOWN")),
error: err.error(),
debug: err.debug(),
}
.into());
}
_ => (),
}
}
pipeline.set_state(gst::State::Null)?;
Ok(())
}
fn main() {
// WIC requires COM initialization
unsafe {
CoInitializeEx(None, COINIT_MULTITHREADED).unwrap();
}
match create_pipeline().and_then(main_loop) {
Ok(r) => r,
Err(e) => eprintln!("Error! {}", e),
}
unsafe {
CoUninitialize();
}
}

View file

@ -7,30 +7,38 @@
// {videotestsrc} - {overlaycomposition} - {capsfilter} - {videoconvert} - {autovideosink}
// The capsfilter element allows us to dictate the video resolution we want for the
// videotestsrc and the overlaycomposition element.
//
// There is a small amount of unsafe code that demonstrates how to work around
// Cairo's internal refcounting of the target buffer surface
use std::{
ops,
sync::{Arc, Mutex},
};
use gst::prelude::*;
use pango::prelude::*;
use std::ops;
use std::sync::{Arc, Mutex};
use anyhow::Error;
use derive_more::{Display, Error};
use gst::prelude::*;
use pango::prelude::*;
#[path = "../examples-common.rs"]
mod examples_common;
#[derive(Debug, Display, Error)]
#[display(fmt = "Received error from {src}: {error} (debug: {debug:?})")]
#[display(fmt = "Missing element {}", _0)]
struct MissingElement(#[error(not(source))] &'static str);
#[derive(Debug, Display, Error)]
#[display(fmt = "Received error from {}: {} (debug: {:?})", src, error, debug)]
struct ErrorMessage {
src: glib::GString,
error: glib::Error,
debug: Option<glib::GString>,
src: String,
error: String,
debug: Option<String>,
source: glib::Error,
}
struct DrawingContext {
layout: LayoutWrapper,
layout: glib::SendUniqueCell<LayoutWrapper>,
info: Option<gst_video::VideoInfo>,
}
@ -41,49 +49,52 @@ impl ops::Deref for LayoutWrapper {
type Target = pango::Layout;
fn deref(&self) -> &pango::Layout {
assert_eq!(self.0.ref_count(), 1);
&self.0
}
}
// SAFETY: We ensure that there are never multiple references to the layout.
unsafe impl Send for LayoutWrapper {}
unsafe impl glib::SendUnique for LayoutWrapper {
fn is_unique(&self) -> bool {
self.0.ref_count() == 1
}
}
fn create_pipeline() -> Result<gst::Pipeline, Error> {
gst::init()?;
let pipeline = gst::Pipeline::default();
let pipeline = gst::Pipeline::new(None);
let src = gst::ElementFactory::make("videotestsrc", None)
.map_err(|_| MissingElement("videotestsrc"))?;
let overlay = gst::ElementFactory::make("overlaycomposition", None)
.map_err(|_| MissingElement("overlaycomposition"))?;
let capsfilter =
gst::ElementFactory::make("capsfilter", None).map_err(|_| MissingElement("capsfilter"))?;
let videoconvert = gst::ElementFactory::make("videoconvert", None)
.map_err(|_| MissingElement("videoconvert"))?;
let sink = gst::ElementFactory::make("autovideosink", None)
.map_err(|_| MissingElement("autovideosink"))?;
// The videotestsrc supports multiple test patterns. In this example, we will use the
// pattern with a white ball moving around the video's center point.
let src = gst::ElementFactory::make("videotestsrc")
.property_from_str("pattern", "ball")
.build()?;
let overlay = gst::ElementFactory::make("overlaycomposition").build()?;
pipeline.add_many(&[&src, &overlay, &capsfilter, &videoconvert, &sink])?;
gst::Element::link_many(&[&src, &overlay, &capsfilter, &videoconvert, &sink])?;
// Plug in a capsfilter element that will force the videotestsrc and the overlay to work
// with images of the size 800x800, and framerate of 15 fps, since my laptop struggles
// rendering it at the default 30 fps
let caps = gst_video::VideoCapsBuilder::new()
.width(800)
.height(800)
.framerate((15, 1).into())
let caps = gst::Caps::builder("video/x-raw")
.field("width", &800i32)
.field("height", &800i32)
.field("framerate", &gst::Fraction::new(15, 1))
.build();
let capsfilter = gst::ElementFactory::make("capsfilter")
.property("caps", &caps)
.build()?;
capsfilter.set_property("caps", &caps).unwrap();
let videoconvert = gst::ElementFactory::make("videoconvert").build()?;
let sink = gst::ElementFactory::make("autovideosink").build()?;
pipeline.add_many([&src, &overlay, &capsfilter, &videoconvert, &sink])?;
gst::Element::link_many([&src, &overlay, &capsfilter, &videoconvert, &sink])?;
// The videotestsrc supports multiple test patterns. In this example, we will use the
// pattern with a white ball moving around the video's center point.
src.set_property_from_str("pattern", "ball");
// The PangoFontMap represents the set of fonts available for a particular rendering system.
let fontmap = pangocairo::FontMap::new();
let fontmap = pangocairo::FontMap::new().unwrap();
// Create a new pango layouting context for the fontmap.
let context = fontmap.create_context();
let context = fontmap.create_context().unwrap();
// Create a pango layout object. This object is a string of text we want to layout.
// It is wrapped in a LayoutWrapper (defined above) to be able to send it across threads.
let layout = LayoutWrapper(pango::Layout::new(&context));
@ -103,8 +114,12 @@ fn create_pipeline() -> Result<gst::Pipeline, Error> {
// interior mutability (see Rust docs). Via this we can get a mutable reference to the contained
// data which is checked at runtime for uniqueness (blocking in case of mutex, panic in case
// of refcell) instead of compile-time (like with normal references).
let drawer = Arc::new(Mutex::new(DrawingContext { layout, info: None }));
let drawer = Arc::new(Mutex::new(DrawingContext {
layout: glib::SendUniqueCell::new(layout).unwrap(),
info: None,
}));
let drawer_clone = drawer.clone();
// Connect to the overlaycomposition element's "draw" signal, which is emitted for
// each videoframe piped through the element. The signal handler needs to
// return a gst_video::VideoOverlayComposition to be drawn on the frame
@ -116,104 +131,125 @@ fn create_pipeline() -> Result<gst::Pipeline, Error> {
//
// In this case, the signal passes the gst::Element and a gst::Sample with
// the current buffer
overlay.connect_closure(
"draw",
false,
glib::closure!(@strong drawer => move |_overlay: &gst::Element,
sample: &gst::Sample| {
overlay
.connect("draw", false, move |args| {
use std::f64::consts::PI;
let drawer = &drawer_clone;
let drawer = drawer.lock().unwrap();
// Get the signal's arguments
let _overlay = args[0].get::<gst::Element>().unwrap();
let sample = args[1].get::<gst::Sample>().unwrap();
let buffer = sample.buffer().unwrap();
let timestamp = buffer.pts().unwrap();
let info = drawer.info.as_ref().unwrap();
let layout = &drawer.layout;
let layout = drawer.layout.borrow();
let angle = 2.0 * PI * (timestamp % (10 * gst::ClockTime::SECOND)).nseconds() as f64
/ (10.0 * gst::ClockTime::SECOND.nseconds() as f64);
let angle = 2.0
* PI
* (timestamp % (10 * gst::ClockTime::SECOND)).nseconds() as f64
/ (10.0 * gst::ClockTime::SECOND.nseconds() as f64);
/* Create a Cairo image surface to draw into and the context around it. */
let surface = cairo::ImageSurface::create(
cairo::Format::ARgb32,
info.width() as i32,
info.height() as i32,
)
.unwrap();
let cr = cairo::Context::new(&surface).expect("Failed to create cairo context");
cr.save().expect("Failed to save state");
cr.set_operator(cairo::Operator::Clear);
cr.paint().expect("Failed to clear background");
cr.restore().expect("Failed to restore state");
// The image we draw (the text) will be static, but we will change the
// transformation on the drawing context, which rotates and shifts everything
// that we draw afterwards. Like this, we have no complicated calculations
// in the actual drawing below.
// Calling multiple transformation methods after each other will apply the
// new transformation on top. If you repeat the cr.rotate(angle) line below
// this a second time, everything in the canvas will rotate twice as fast.
cr.translate(
f64::from(info.width()) / 2.0,
f64::from(info.height()) / 2.0,
);
cr.rotate(angle);
// This loop will render 10 times the string "GStreamer" in a circle
for i in 0..10 {
// Cairo, like most rendering frameworks, is using a stack for transformations
// with this, we push our current transformation onto this stack - allowing us
// to make temporary changes / render something / and then returning to the
// previous transformations.
cr.save().expect("Failed to save state");
let angle = (360. * f64::from(i)) / 10.0;
let red = (1.0 + f64::cos((angle - 60.0) * PI / 180.0)) / 2.0;
cr.set_source_rgb(red, 0.0, 1.0 - red);
cr.rotate(angle * PI / 180.0);
// Update the text layout. This function is only updating pango's internal state.
// So e.g. that after a 90 degree rotation it knows that what was previously going
// to end up as a 200x100 rectangle would now be 100x200.
pangocairo::functions::update_layout(&cr, layout);
let (width, _height) = layout.size();
// Using width and height of the text, we can properly position it within
// our canvas.
cr.move_to(
-(f64::from(width) / f64::from(pango::SCALE)) / 2.0,
-(f64::from(info.height())) / 2.0,
);
// After telling the layout object where to draw itself, we actually tell
// it to draw itself into our cairo context.
pangocairo::functions::show_layout(&cr, layout);
// Here we go one step up in our stack of transformations, removing any
// changes we did to them since the last call to cr.save();
cr.restore().expect("Failed to restore state");
}
/* Drop the Cairo context to release the additional reference to the data and
* then take ownership of the data. This only works if we have the one and only
* reference to the image surface */
drop(cr);
let stride = surface.stride();
let data = surface.take_data().unwrap();
/* Create a gst::Buffer for Cairo to draw into */
let frame_width = info.width() as usize;
let frame_height = info.height() as usize;
let stride = 4 * frame_width;
let frame_size = stride * frame_height;
/* Create an RGBA buffer, and add a video meta that the videooverlaycomposition expects */
let mut buffer = gst::Buffer::from_mut_slice(data);
let mut buffer = gst::Buffer::with_size(frame_size).unwrap();
gst_video::VideoMeta::add_full(
gst_video::VideoMeta::add(
buffer.get_mut().unwrap(),
gst_video::VideoFrameFlags::empty(),
gst_video::VideoFormat::Bgra,
info.width(),
info.height(),
&[0],
&[stride],
)
.unwrap();
frame_width as u32,
frame_height as u32,
).unwrap();
let buffer = buffer.into_mapped_buffer_writable().unwrap();
let buffer = {
let buffer_ptr = unsafe { buffer.buffer().as_ptr() };
let surface = cairo::ImageSurface::create_for_data(
buffer,
cairo::Format::ARgb32,
frame_width as i32,
frame_height as i32,
stride as i32,
)
.unwrap();
let cr = cairo::Context::new(&surface).expect("Failed to create cairo context");
cr.save().expect("Failed to save state");
cr.set_operator(cairo::Operator::Clear);
cr.paint().expect("Failed to clear background");
cr.restore().expect("Failed to restore state");
// The image we draw (the text) will be static, but we will change the
// transformation on the drawing context, which rotates and shifts everything
// that we draw afterwards. Like this, we have no complicated calulations
// in the actual drawing below.
// Calling multiple transformation methods after each other will apply the
// new transformation on top. If you repeat the cr.rotate(angle) line below
// this a second time, everything in the canvas will rotate twice as fast.
cr.translate(
f64::from(info.width()) / 2.0,
f64::from(info.height()) / 2.0,
);
cr.rotate(angle);
// This loop will render 10 times the string "GStreamer" in a circle
for i in 0..10 {
// Cairo, like most rendering frameworks, is using a stack for transformations
// with this, we push our current transformation onto this stack - allowing us
// to make temporary changes / render something / and then returning to the
// previous transformations.
cr.save().expect("Failed to save state");
let angle = (360. * f64::from(i)) / 10.0;
let red = (1.0 + f64::cos((angle - 60.0) * PI / 180.0)) / 2.0;
cr.set_source_rgb(red, 0.0, 1.0 - red);
cr.rotate(angle * PI / 180.0);
// Update the text layout. This function is only updating pango's internal state.
// So e.g. that after a 90 degree rotation it knows that what was previously going
// to end up as a 200x100 rectangle would now be 100x200.
pangocairo::functions::update_layout(&cr, &**layout);
let (width, _height) = layout.size();
// Using width and height of the text, we can properly possition it within
// our canvas.
cr.move_to(
-(f64::from(width) / f64::from(pango::SCALE)) / 2.0,
-(f64::from(info.height())) / 2.0,
);
// After telling the layout object where to draw itself, we actually tell
// it to draw itself into our cairo context.
pangocairo::functions::show_layout(&cr, &**layout);
// Here we go one step up in our stack of transformations, removing any
// changes we did to them since the last call to cr.save();
cr.restore().expect("Failed to restore state");
}
// Safety: The surface still owns a mutable reference to the buffer but our reference
// to the surface here is the last one. After dropping the surface the buffer would be
// freed, so we keep an additional strong reference here before dropping the surface,
// which is then returned. As such it's guaranteed that nothing is using the buffer
// anymore mutably.
drop(cr);
unsafe {
assert_eq!(
cairo::ffi::cairo_surface_get_reference_count(surface.to_raw_none()),
1
);
let buffer = glib::translate::from_glib_none(buffer_ptr);
drop(surface);
buffer
}
};
/* Turn the buffer into a VideoOverlayRectangle, then place
* that into a VideoOverlayComposition and return it.
@ -222,18 +258,14 @@ fn create_pipeline() -> Result<gst::Pipeline, Error> {
* spaced around the video frame, but we're just outputting 1
* here */
let rect = gst_video::VideoOverlayRectangle::new_raw(
&buffer,
0,
0,
info.width(),
info.height(),
gst_video::VideoOverlayFormatFlags::PREMULTIPLIED_ALPHA,
&buffer,
0, 0, frame_width as u32, frame_height as u32,
gst_video::VideoOverlayFormatFlags::PREMULTIPLIED_ALPHA,
);
gst_video::VideoOverlayComposition::new(Some(&rect))
.unwrap()
}),
);
Some(gst_video::VideoOverlayComposition::new(Some(&rect)).unwrap().to_value())
})
.unwrap();
// Add a signal handler to the overlay's "caps-changed" signal. This could e.g.
// be called when the sink that we render to does not support resizing the image
@ -241,18 +273,18 @@ fn create_pipeline() -> Result<gst::Pipeline, Error> {
// will then change its caps and we use the notification about this change to
// resize our canvas's size.
// Another possibility for when this might happen is, when our video is a network
// stream that dynamically changes resolution when enough bandwidth is available.
overlay.connect_closure(
"caps-changed",
false,
glib::closure!(move |_overlay: &gst::Element,
caps: &gst::Caps,
_width: u32,
_height: u32| {
// stream that dynamically changes resolution when enough bandwith is available.
overlay
.connect("caps-changed", false, move |args| {
let _overlay = args[0].get::<gst::Element>().unwrap();
let caps = args[1].get::<gst::Caps>().unwrap();
let mut drawer = drawer.lock().unwrap();
drawer.info = Some(gst_video::VideoInfo::from_caps(caps).unwrap());
}),
);
drawer.info = Some(gst_video::VideoInfo::from_caps(&caps).unwrap());
None
})
.unwrap();
Ok(pipeline)
}
@ -274,10 +306,11 @@ fn main_loop(pipeline: gst::Pipeline) -> Result<(), Error> {
return Err(ErrorMessage {
src: msg
.src()
.map(|s| s.path_string())
.unwrap_or_else(|| glib::GString::from("UNKNOWN")),
error: err.error(),
.map(|s| String::from(s.path_string()))
.unwrap_or_else(|| String::from("None")),
error: err.error().to_string(),
debug: err.debug(),
source: err.error(),
}
.into());
}
@ -293,7 +326,7 @@ fn main_loop(pipeline: gst::Pipeline) -> Result<(), Error> {
fn example_main() {
match create_pipeline().and_then(main_loop) {
Ok(r) => r,
Err(e) => eprintln!("Error! {e}"),
Err(e) => eprintln!("Error! {}", e),
}
}

View file

@ -6,12 +6,12 @@
// |-[probe]
// /
// {audiotestsrc} - {fakesink}
#![allow(clippy::question_mark)]
use std::i16;
use gst::prelude::*;
use byte_slice_cast::*;
use gst::prelude::*;
use std::i16;
#[path = "../examples-common.rs"]
mod examples_common;
@ -22,7 +22,7 @@ fn example_main() {
// Parse the pipeline we want to probe from a static in-line string.
// Here we give our audiotestsrc a name, so we can retrieve that element
// from the resulting pipeline.
let pipeline = gst::parse::launch(&format!(
let pipeline = gst::parse_launch(&format!(
"audiotestsrc name=src ! audio/x-raw,format={},channels=1 ! fakesink",
gst_audio::AUDIO_FORMAT_S16
))
@ -38,38 +38,36 @@ fn example_main() {
// This handler gets called for every buffer that passes the pad we probe.
src_pad.add_probe(gst::PadProbeType::BUFFER, |_, probe_info| {
// Interpret the data sent over the pad as one buffer
let Some(buffer) = probe_info.buffer() else {
return gst::PadProbeReturn::Ok;
};
if let Some(gst::PadProbeData::Buffer(ref buffer)) = probe_info.data {
// At this point, buffer is only a reference to an existing memory region somewhere.
// When we want to access its content, we have to map it while requesting the required
// mode of access (read, read/write).
// This type of abstraction is necessary, because the buffer in question might not be
// on the machine's main memory itself, but rather in the GPU's memory.
// So mapping the buffer makes the underlying memory region accessible to us.
// See: https://gstreamer.freedesktop.org/documentation/plugin-development/advanced/allocation.html
let map = buffer.map_readable().unwrap();
// At this point, buffer is only a reference to an existing memory region somewhere.
// When we want to access its content, we have to map it while requesting the required
// mode of access (read, read/write).
// This type of abstraction is necessary, because the buffer in question might not be
// on the machine's main memory itself, but rather in the GPU's memory.
// So mapping the buffer makes the underlying memory region accessible to us.
// See: https://gstreamer.freedesktop.org/documentation/plugin-development/advanced/allocation.html
let map = buffer.map_readable().unwrap();
// We know what format the data in the memory region has, since we requested
// it by setting the appsink's caps. So what we do here is interpret the
// memory region we mapped as an array of signed 16 bit integers.
let samples = if let Ok(samples) = map.as_slice_of::<i16>() {
samples
} else {
return gst::PadProbeReturn::Ok;
};
// We know what format the data in the memory region has, since we requested
// it by setting the appsink's caps. So what we do here is interpret the
// memory region we mapped as an array of signed 16 bit integers.
let samples = if let Ok(samples) = map.as_slice_of::<i16>() {
samples
} else {
return gst::PadProbeReturn::Ok;
};
// For buffer (= chunk of samples), we calculate the root mean square:
let sum: f64 = samples
.iter()
.map(|sample| {
let f = f64::from(*sample) / f64::from(i16::MAX);
f * f
})
.sum();
let rms = (sum / (samples.len() as f64)).sqrt();
println!("rms: {rms}");
// For buffer (= chunk of samples), we calculate the root mean square:
let sum: f64 = samples
.iter()
.map(|sample| {
let f = f64::from(*sample) / f64::from(i16::MAX);
f * f
})
.sum();
let rms = (sum / (samples.len() as f64)).sqrt();
println!("rms: {}", rms);
}
gst::PadProbeReturn::Ok
});

View file

@ -11,29 +11,34 @@
// The capsfilter element allows us to dictate the video resolution we want for the
// videotestsrc and the cairooverlay element.
use std::{
ops,
sync::{Arc, Mutex},
};
use gst::prelude::*;
use pango::prelude::*;
use std::ops;
use std::sync::{Arc, Mutex};
use anyhow::Error;
use derive_more::{Display, Error};
use gst::prelude::*;
use pango::prelude::*;
#[path = "../examples-common.rs"]
mod examples_common;
#[derive(Debug, Display, Error)]
#[display(fmt = "Received error from {src}: {error} (debug: {debug:?})")]
#[display(fmt = "Missing element {}", _0)]
struct MissingElement(#[error(not(source))] &'static str);
#[derive(Debug, Display, Error)]
#[display(fmt = "Received error from {}: {} (debug: {:?})", src, error, debug)]
struct ErrorMessage {
src: glib::GString,
error: glib::Error,
debug: Option<glib::GString>,
src: String,
error: String,
debug: Option<String>,
source: glib::Error,
}
struct DrawingContext {
layout: LayoutWrapper,
layout: glib::SendUniqueCell<LayoutWrapper>,
info: Option<gst_video::VideoInfo>,
}
@ -44,45 +49,50 @@ impl ops::Deref for LayoutWrapper {
type Target = pango::Layout;
fn deref(&self) -> &pango::Layout {
assert_eq!(self.0.ref_count(), 1);
&self.0
}
}
// SAFETY: We ensure that there are never multiple references to the layout.
unsafe impl Send for LayoutWrapper {}
unsafe impl glib::SendUnique for LayoutWrapper {
fn is_unique(&self) -> bool {
self.0.ref_count() == 1
}
}
fn create_pipeline() -> Result<gst::Pipeline, Error> {
gst::init()?;
let pipeline = gst::Pipeline::default();
let src = gst::ElementFactory::make("videotestsrc")
// The videotestsrc supports multiple test patterns. In this example, we will use the
// pattern with a white ball moving around the video's center point.
.property_from_str("pattern", "ball")
.build()?;
let overlay = gst::ElementFactory::make("cairooverlay").build()?;
let pipeline = gst::Pipeline::new(None);
let src = gst::ElementFactory::make("videotestsrc", None)
.map_err(|_| MissingElement("videotestsrc"))?;
let overlay = gst::ElementFactory::make("cairooverlay", None)
.map_err(|_| MissingElement("cairooverlay"))?;
let capsfilter =
gst::ElementFactory::make("capsfilter", None).map_err(|_| MissingElement("capsfilter"))?;
let videoconvert = gst::ElementFactory::make("videoconvert", None)
.map_err(|_| MissingElement("videoconvert"))?;
let sink = gst::ElementFactory::make("autovideosink", None)
.map_err(|_| MissingElement("autovideosink"))?;
pipeline.add_many(&[&src, &overlay, &capsfilter, &videoconvert, &sink])?;
gst::Element::link_many(&[&src, &overlay, &capsfilter, &videoconvert, &sink])?;
// Plug in a capsfilter element that will force the videotestsrc and the cairooverlay to work
// with images of the size 800x800.
let caps = gst_video::VideoCapsBuilder::new()
.width(800)
.height(800)
let caps = gst::Caps::builder("video/x-raw")
.field("width", &800i32)
.field("height", &800i32)
.build();
let capsfilter = gst::ElementFactory::make("capsfilter")
.property("caps", &caps)
.build()?;
capsfilter.set_property("caps", &caps).unwrap();
let videoconvert = gst::ElementFactory::make("videoconvert").build()?;
let sink = gst::ElementFactory::make("autovideosink").build()?;
pipeline.add_many([&src, &overlay, &capsfilter, &videoconvert, &sink])?;
gst::Element::link_many([&src, &overlay, &capsfilter, &videoconvert, &sink])?;
// The videotestsrc supports multiple test patterns. In this example, we will use the
// pattern with a white ball moving around the video's center point.
src.set_property_from_str("pattern", "ball");
// The PangoFontMap represents the set of fonts available for a particular rendering system.
let fontmap = pangocairo::FontMap::new();
let fontmap = pangocairo::FontMap::new().unwrap();
// Create a new pango layouting context for the fontmap.
let context = fontmap.create_context();
let context = fontmap.create_context().unwrap();
// Create a pango layout object. This object is a string of text we want to layout.
// It is wrapped in a LayoutWrapper (defined above) to be able to send it across threads.
let layout = LayoutWrapper(pango::Layout::new(&context));
@ -102,7 +112,10 @@ fn create_pipeline() -> Result<gst::Pipeline, Error> {
// interior mutability (see Rust docs). Via this we can get a mutable reference to the contained
// data which is checked at runtime for uniqueness (blocking in case of mutex, panic in case
// of refcell) instead of compile-time (like with normal references).
let drawer = Arc::new(Mutex::new(DrawingContext { layout, info: None }));
let drawer = Arc::new(Mutex::new(DrawingContext {
layout: glib::SendUniqueCell::new(layout).unwrap(),
info: None,
}));
let drawer_clone = drawer.clone();
// Connect to the cairooverlay element's "draw" signal, which is emitted for
@ -112,74 +125,76 @@ fn create_pipeline() -> Result<gst::Pipeline, Error> {
// passed as array of glib::Value. For a documentation about the actual arguments
// it is always a good idea to either check the element's signals using either
// gst-inspect, or the online documentation.
overlay.connect("draw", false, move |args| {
use std::f64::consts::PI;
overlay
.connect("draw", false, move |args| {
use std::f64::consts::PI;
let drawer = &drawer_clone;
let drawer = drawer.lock().unwrap();
let drawer = &drawer_clone;
let drawer = drawer.lock().unwrap();
// Get the signal's arguments
let _overlay = args[0].get::<gst::Element>().unwrap();
// This is the cairo context. This is the root of all of cairo's
// drawing functionality.
let cr = args[1].get::<cairo::Context>().unwrap();
let timestamp = args[2].get::<gst::ClockTime>().unwrap();
let _duration = args[3].get::<gst::ClockTime>().unwrap();
// Get the signal's arguments
let _overlay = args[0].get::<gst::Element>().unwrap();
// This is the cairo context. This is the root of all of cairo's
// drawing functionality.
let cr = args[1].get::<cairo::Context>().unwrap();
let timestamp = args[2].get::<gst::ClockTime>().unwrap();
let _duration = args[3].get::<gst::ClockTime>().unwrap();
let info = drawer.info.as_ref().unwrap();
let layout = &drawer.layout;
let info = drawer.info.as_ref().unwrap();
let layout = drawer.layout.borrow();
let angle = 2.0 * PI * (timestamp % (10 * gst::ClockTime::SECOND)).nseconds() as f64
/ (10.0 * gst::ClockTime::SECOND.nseconds() as f64);
let angle = 2.0 * PI * (timestamp % (10 * gst::ClockTime::SECOND)).nseconds() as f64
/ (10.0 * gst::ClockTime::SECOND.nseconds() as f64);
// The image we draw (the text) will be static, but we will change the
// transformation on the drawing context, which rotates and shifts everything
// that we draw afterwards. Like this, we have no complicated calculations
// in the actual drawing below.
// Calling multiple transformation methods after each other will apply the
// new transformation on top. If you repeat the cr.rotate(angle) line below
// this a second time, everything in the canvas will rotate twice as fast.
cr.translate(
f64::from(info.width()) / 2.0,
f64::from(info.height()) / 2.0,
);
cr.rotate(angle);
// This loop will render 10 times the string "GStreamer" in a circle
for i in 0..10 {
// Cairo, like most rendering frameworks, is using a stack for transformations
// with this, we push our current transformation onto this stack - allowing us
// to make temporary changes / render something / and then returning to the
// previous transformations.
cr.save().expect("Failed to save state");
let angle = (360. * f64::from(i)) / 10.0;
let red = (1.0 + f64::cos((angle - 60.0) * PI / 180.0)) / 2.0;
cr.set_source_rgb(red, 0.0, 1.0 - red);
cr.rotate(angle * PI / 180.0);
// Update the text layout. This function is only updating pango's internal state.
// So e.g. that after a 90 degree rotation it knows that what was previously going
// to end up as a 200x100 rectangle would now be 100x200.
pangocairo::functions::update_layout(&cr, layout);
let (width, _height) = layout.size();
// Using width and height of the text, we can properly position it within
// our canvas.
cr.move_to(
-(f64::from(width) / f64::from(pango::SCALE)) / 2.0,
-(f64::from(info.height())) / 2.0,
// The image we draw (the text) will be static, but we will change the
// transformation on the drawing context, which rotates and shifts everything
// that we draw afterwards. Like this, we have no complicated calulations
// in the actual drawing below.
// Calling multiple transformation methods after each other will apply the
// new transformation on top. If you repeat the cr.rotate(angle) line below
// this a second time, everything in the canvas will rotate twice as fast.
cr.translate(
f64::from(info.width()) / 2.0,
f64::from(info.height()) / 2.0,
);
// After telling the layout object where to draw itself, we actually tell
// it to draw itself into our cairo context.
pangocairo::functions::show_layout(&cr, layout);
cr.rotate(angle);
// Here we go one step up in our stack of transformations, removing any
// changes we did to them since the last call to cr.save();
cr.restore().expect("Failed to restore state");
}
// This loop will render 10 times the string "GStreamer" in a circle
for i in 0..10 {
// Cairo, like most rendering frameworks, is using a stack for transformations
// with this, we push our current transformation onto this stack - allowing us
// to make temporary changes / render something / and then returning to the
// previous transformations.
cr.save().expect("Failed to save state");
None
});
let angle = (360. * f64::from(i)) / 10.0;
let red = (1.0 + f64::cos((angle - 60.0) * PI / 180.0)) / 2.0;
cr.set_source_rgb(red, 0.0, 1.0 - red);
cr.rotate(angle * PI / 180.0);
// Update the text layout. This function is only updating pango's internal state.
// So e.g. that after a 90 degree rotation it knows that what was previously going
// to end up as a 200x100 rectangle would now be 100x200.
pangocairo::functions::update_layout(&cr, &**layout);
let (width, _height) = layout.size();
// Using width and height of the text, we can properly possition it within
// our canvas.
cr.move_to(
-(f64::from(width) / f64::from(pango::SCALE)) / 2.0,
-(f64::from(info.height())) / 2.0,
);
// After telling the layout object where to draw itself, we actually tell
// it to draw itself into our cairo context.
pangocairo::functions::show_layout(&cr, &**layout);
// Here we go one step up in our stack of transformations, removing any
// changes we did to them since the last call to cr.save();
cr.restore().expect("Failed to restore state");
}
None
})
.unwrap();
// Add a signal handler to the overlay's "caps-changed" signal. This could e.g.
// be called when the sink that we render to does not support resizing the image
@ -187,16 +202,18 @@ fn create_pipeline() -> Result<gst::Pipeline, Error> {
// will then change its caps and we use the notification about this change to
// resize our canvas's size.
// Another possibility for when this might happen is, when our video is a network
// stream that dynamically changes resolution when enough bandwidth is available.
overlay.connect("caps-changed", false, move |args| {
let _overlay = args[0].get::<gst::Element>().unwrap();
let caps = args[1].get::<gst::Caps>().unwrap();
// stream that dynamically changes resolution when enough bandwith is available.
overlay
.connect("caps-changed", false, move |args| {
let _overlay = args[0].get::<gst::Element>().unwrap();
let caps = args[1].get::<gst::Caps>().unwrap();
let mut drawer = drawer.lock().unwrap();
drawer.info = Some(gst_video::VideoInfo::from_caps(&caps).unwrap());
let mut drawer = drawer.lock().unwrap();
drawer.info = Some(gst_video::VideoInfo::from_caps(&caps).unwrap());
None
});
None
})
.unwrap();
Ok(pipeline)
}
@ -218,10 +235,11 @@ fn main_loop(pipeline: gst::Pipeline) -> Result<(), Error> {
return Err(ErrorMessage {
src: msg
.src()
.map(|s| s.path_string())
.unwrap_or_else(|| glib::GString::from("UNKNOWN")),
error: err.error(),
.map(|s| String::from(s.path_string()))
.unwrap_or_else(|| String::from("None")),
error: err.error().to_string(),
debug: err.debug(),
source: err.error(),
}
.into());
}
@ -237,7 +255,7 @@ fn main_loop(pipeline: gst::Pipeline) -> Result<(), Error> {
fn example_main() {
match create_pipeline().and_then(main_loop) {
Ok(r) => r,
Err(e) => eprintln!("Error! {e}"),
Err(e) => eprintln!("Error! {}", e),
}
}

View file

@ -1,67 +0,0 @@
// This example shows how to use the GstPlay API.
// The GstPlay API is a convenience API to allow implement playback applications
// without having to write too much code.
// Most of the tasks a play needs to support (such as seeking and switching
// audio / subtitle streams or changing the volume) are all supported by simple
// one-line function calls on the GstPlay.
use std::env;
use anyhow::Error;
#[path = "../examples-common.rs"]
mod examples_common;
use gst_play::{Play, PlayMessage, PlayVideoRenderer};
fn main_loop(uri: &str) -> Result<(), Error> {
gst::init()?;
let play = Play::new(None::<PlayVideoRenderer>);
play.set_uri(Some(uri));
play.play();
let mut result = Ok(());
for msg in play.message_bus().iter_timed(gst::ClockTime::NONE) {
match PlayMessage::parse(&msg) {
Ok(PlayMessage::EndOfStream) => {
play.stop();
break;
}
Ok(PlayMessage::Error { error, details: _ }) => {
result = Err(error);
play.stop();
break;
}
Ok(_) => (),
Err(_) => unreachable!(),
}
}
// Set the message bus to flushing to ensure that all pending messages are dropped and there
// are no further references to the play instance.
play.message_bus().set_flushing(true);
result.map_err(|e| e.into())
}
fn example_main() {
let args: Vec<_> = env::args().collect();
let uri: &str = if args.len() == 2 {
args[1].as_ref()
} else {
println!("Usage: play uri");
std::process::exit(-1)
};
match main_loop(uri) {
Ok(r) => r,
Err(e) => eprintln!("Error! {e}"),
}
}
fn main() {
// tutorials_common::run is only required to set up the application environment on macOS
// (but not necessary in normal Cocoa applications where this is set up automatically)
examples_common::run(example_main);
}

View file

@ -9,10 +9,10 @@
// Much of the playbin's behavior can be controlled by so-called flags, as well
// as the playbin's properties and signals.
use std::env;
use gst::prelude::*;
use std::env;
#[path = "../examples-common.rs"]
mod examples_common;
@ -28,22 +28,20 @@ fn example_main() {
};
// Create a new playbin element, and tell it what uri to play back.
let playbin = gst::ElementFactory::make("playbin")
.property("uri", uri)
.build()
.unwrap();
let playbin = gst::ElementFactory::make("playbin", None).unwrap();
playbin.set_property("uri", &uri).unwrap();
// For flags handling
// With flags, one can configure playbin's behavior such as whether it
// should play back contained video streams, or if it should render subtitles.
// let flags = playbin.property_value("flags");
// let flags_class = FlagsClass::with_type(flags.type_()).unwrap();
// let flags = playbin.get_property("flags").unwrap();
// let flags_class = FlagsClass::new(flags.type_()).unwrap();
// let flags = flags_class.builder_with_value(flags).unwrap()
// .unset_by_nick("text")
// .unset_by_nick("video")
// .build()
// .unwrap();
// playbin.set_property_from_value("flags", &flags);
// playbin.set_property_from_value("flags", &flags).unwrap();
// The playbin also provides any kind of metadata that it found in the played stream.
// For this, the playbin provides signals notifying about changes in the metadata.
@ -52,34 +50,38 @@ fn example_main() {
// - Live streams (such as internet radios) update this metadata during the stream
// Note that this signal will be emitted from the streaming threads usually,
// not the application's threads!
playbin.connect("audio-tags-changed", false, |values| {
// The metadata of any of the contained audio streams changed
// In the case of a live-stream from an internet radio, this could for example
// mark the beginning of a new track, or a new DJ.
let playbin = values[0]
.get::<glib::Object>()
.expect("playbin \"audio-tags-changed\" signal values[1]");
// This gets the index of the stream that changed. This is necessary, since
// there could e.g. be multiple audio streams (english, spanish, ...).
let idx = values[1]
.get::<i32>()
.expect("playbin \"audio-tags-changed\" signal values[1]");
playbin
.connect("audio-tags-changed", false, |values| {
// The metadata of any of the contained audio streams changed
// In the case of a live-stream from an internet radio, this could for example
// mark the beginning of a new track, or a new DJ.
let playbin = values[0]
.get::<glib::Object>()
.expect("playbin \"audio-tags-changed\" signal values[1]");
// This gets the index of the stream that changed. This is neccessary, since
// there could e.g. be multiple audio streams (english, spanish, ...).
let idx = values[1]
.get::<i32>()
.expect("playbin \"audio-tags-changed\" signal values[1]");
println!("audio tags of audio stream {idx} changed:");
println!("audio tags of audio stream {} changed:", idx);
// HELP: is this correct?
// We were only notified about the change of metadata. If we want to do
// something with it, we first need to actually query the metadata from the playbin.
// We do this by facilliating the get-audio-tags action-signal on playbin.
// Sending an action-signal to an element essentially is a function call on the element.
// It is done that way, because elements do not have their own function API, they are
// relying on GStreamer and GLib's API. The only way an element can communicate with an
// application is via properties, signals or action signals (or custom messages, events, queries).
// So what the following code does, is essentially asking playbin to tell us its already
// internally stored tag list for this stream index.
let tags = playbin.emit_by_name::<Option<gst::TagList>>("get-audio-tags", &[&idx]);
// HELP: is this correct?
// We were only notified about the change of metadata. If we want to do
// something with it, we first need to actually query the metadata from the playbin.
// We do this by facilliating the get-audio-tags action-signal on playbin.
// Sending an action-signal to an element essentially is a function call on the element.
// It is done that way, because elements do not have their own function API, they are
// relying on GStreamer and GLib's API. The only way an element can communicate with an
// application is via properties, signals or action signals (or custom messages, events, queries).
// So what the following code does, is essentially asking playbin to tell us its already
// internally stored tag list for this stream index.
let tags = playbin
.emit_by_name("get-audio-tags", &[&idx])
.unwrap()
.unwrap();
let tags = tags.get::<gst::TagList>().expect("tags");
if let Some(tags) = tags {
if let Some(artist) = tags.get::<gst::tags::Artist>() {
println!(" Artist: {}", artist.get());
}
@ -91,10 +93,10 @@ fn example_main() {
if let Some(album) = tags.get::<gst::tags::Album>() {
println!(" Album: {}", album.get());
}
}
None
});
None
})
.unwrap();
// The playbin element itself is a playbin, so it can be used as one, despite being
// created from an element factory.
@ -121,7 +123,7 @@ fn example_main() {
MessageView::StateChanged(state_changed) =>
// We are only interested in state-changed messages from playbin
{
if state_changed.src().map(|s| s == &playbin).unwrap_or(false)
if state_changed.src().map(|s| s == playbin).unwrap_or(false)
&& state_changed.current() == gst::State::Playing
{
// Generate a dot graph of the pipeline to GST_DEBUG_DUMP_DOT_DIR if defined

View file

@ -5,13 +5,12 @@
// audio / subtitle streams or changing the volume) are all supported by simple
// one-line function calls on the GstPlayer.
use std::{
env,
sync::{Arc, Mutex},
};
use gst::prelude::*;
use std::env;
use std::sync::{Arc, Mutex};
use anyhow::Error;
use gst::prelude::*;
#[allow(unused_imports)]
#[path = "../examples-common.rs"]
@ -24,12 +23,12 @@ fn main_loop(uri: &str) -> Result<(), Error> {
let dispatcher = gst_player::PlayerGMainContextSignalDispatcher::new(None);
let player = gst_player::Player::new(
None::<gst_player::PlayerVideoRenderer>,
Some(dispatcher.upcast::<gst_player::PlayerSignalDispatcher>()),
None,
Some(&dispatcher.upcast::<gst_player::PlayerSignalDispatcher>()),
);
// Tell the player what uri to play.
player.set_uri(Some(uri));
player.set_uri(uri);
let error = Arc::new(Mutex::new(Ok(())));
@ -76,7 +75,7 @@ fn example_main() {
match main_loop(uri) {
Ok(r) => r,
Err(e) => eprintln!("Error! {e}"),
Err(e) => eprintln!("Error! {}", e),
}
}

View file

@ -12,10 +12,11 @@
// For convenience, the API has a set of pre-defined queries, but also
// allows custom queries (which can be defined and used by your own elements).
use std::env;
use gst::prelude::*;
use std::convert::TryInto;
use std::env;
#[path = "../examples-common.rs"]
mod examples_common;
@ -28,7 +29,7 @@ fn example_main() {
let main_loop = glib::MainLoop::new(None, false);
// Let GStreamer create a pipeline from the parsed launch syntax on the cli.
let pipeline = gst::parse::launch(&pipeline_str).unwrap();
let pipeline = gst::parse_launch(&pipeline_str).unwrap();
let bus = pipeline.bus().unwrap();
pipeline
@ -50,8 +51,9 @@ fn example_main() {
let timeout_id = glib::timeout_add_seconds(1, move || {
// Here we temporarily retrieve a strong reference on the pipeline from the weak one
// we moved into this callback.
let Some(pipeline) = pipeline_weak.upgrade() else {
return glib::ControlFlow::Break;
let pipeline = match pipeline_weak.upgrade() {
Some(pipeline) => pipeline,
None => return glib::Continue(true),
};
//let pos = pipeline.query_position(gst::Format::Time).unwrap_or(-1);
@ -86,35 +88,34 @@ fn example_main() {
println!("{} / {}", pos.display(), dur.display());
glib::ControlFlow::Continue
glib::Continue(true)
});
// Need to move a new reference into the closure.
let main_loop_clone = main_loop.clone();
//bus.add_signal_watch();
//bus.connect_message(None, move |_, msg| {
let _bus_watch = bus
.add_watch(move |_, msg| {
use gst::MessageView;
bus.add_watch(move |_, msg| {
use gst::MessageView;
let main_loop = &main_loop_clone;
match msg.view() {
MessageView::Eos(..) => main_loop.quit(),
MessageView::Error(err) => {
println!(
"Error from {:?}: {} ({:?})",
err.src().map(|s| s.path_string()),
err.error(),
err.debug()
);
main_loop.quit();
}
_ => (),
};
let main_loop = &main_loop_clone;
match msg.view() {
MessageView::Eos(..) => main_loop.quit(),
MessageView::Error(err) => {
println!(
"Error from {:?}: {} ({:?})",
err.src().map(|s| s.path_string()),
err.error(),
err.debug()
);
main_loop.quit();
}
_ => (),
};
glib::ControlFlow::Continue
})
.expect("Failed to add bus watch");
glib::Continue(true)
})
.expect("Failed to add bus watch");
main_loop.run();
@ -122,7 +123,8 @@ fn example_main() {
.set_state(gst::State::Null)
.expect("Unable to set the pipeline to the `Null` state");
timeout_id.remove();
bus.remove_watch().unwrap();
glib::source_remove(timeout_id);
}
fn main() {

View file

@ -1,6 +1,7 @@
use std::env;
use gst::element_error;
use gst::prelude::*;
use gst::{element_error, prelude::*};
use std::env;
#[path = "../examples-common.rs"]
mod examples_common;
@ -9,25 +10,41 @@ use anyhow::Error;
use derive_more::{Display, Error};
#[derive(Debug, Display, Error)]
#[display(fmt = "No such pad {_0} in {_1}")]
#[display(fmt = "Missing element {}", _0)]
struct MissingElement(#[error(not(source))] &'static str);
#[derive(Debug, Display, Error)]
#[display(fmt = "No such pad {} in {}", _0, _1)]
struct NoSuchPad(#[error(not(source))] &'static str, String);
#[derive(Debug, Display, Error)]
#[display(fmt = "Unknown payload type {_0}")]
#[display(fmt = "Unknown payload type {}", _0)]
struct UnknownPT(#[error(not(source))] u32);
#[derive(Debug, Display, Error)]
#[display(fmt = "Usage: {_0} (play | record) DROP_PROBABILITY")]
#[display(fmt = "Usage: {} (play | record) DROP_PROBABILITY", _0)]
struct UsageError(#[error(not(source))] String);
#[derive(Debug, Display, Error)]
#[display(fmt = "Received error from {src}: {error} (debug: {debug:?})")]
#[display(fmt = "Received error from {}: {} (debug: {:?})", src, error, debug)]
struct ErrorMessage {
src: glib::GString,
error: glib::Error,
debug: Option<glib::GString>,
src: String,
error: String,
debug: Option<String>,
source: glib::Error,
}
fn make_element(
factory_name: &'static str,
element_name: Option<&str>,
) -> Result<gst::Element, Error> {
match gst::ElementFactory::make(factory_name, element_name) {
Ok(elem) => Ok(elem),
Err(_) => Err(Error::from(MissingElement(factory_name))),
}
}
#[doc(alias = "get_static_pad")]
fn static_pad(element: &gst::Element, pad_name: &'static str) -> Result<gst::Pad, Error> {
match element.static_pad(pad_name) {
Some(pad) => Ok(pad),
@ -38,6 +55,7 @@ fn static_pad(element: &gst::Element, pad_name: &'static str) -> Result<gst::Pad
}
}
#[doc(alias = "get_request_pad")]
fn request_pad(element: &gst::Element, pad_name: &'static str) -> Result<gst::Pad, Error> {
match element.request_pad_simple(pad_name) {
Some(pad) => Ok(pad),
@ -65,11 +83,16 @@ fn connect_rtpbin_srcpad(src_pad: &gst::Pad, sink: &gst::Element) -> Result<(),
}
fn make_fec_decoder(rtpbin: &gst::Element, sess_id: u32) -> Result<gst::Element, Error> {
let internal_storage = rtpbin.emit_by_name::<glib::Object>("get-internal-storage", &[&sess_id]);
let fecdec = gst::ElementFactory::make("rtpulpfecdec")
.property("storage", &internal_storage)
.property("pt", 100u32)
.build()?;
let fecdec = make_element("rtpulpfecdec", None)?;
let internal_storage = rtpbin
.emit_by_name("get-internal-storage", &[&sess_id])
.unwrap()
.unwrap()
.get::<glib::Object>()
.unwrap();
fecdec.set_property("storage", &internal_storage)?;
fecdec.set_property("pt", &100u32)?;
Ok(fecdec)
}
@ -85,55 +108,34 @@ fn example_main() -> Result<(), Error> {
let drop_probability = args[2].parse::<f32>()?;
let pipeline = gst::Pipeline::default();
let pipeline = gst::Pipeline::new(None);
let src = make_element("udpsrc", None)?;
let netsim = make_element("netsim", None)?;
let rtpbin = make_element("rtpbin", None)?;
let depay = make_element("rtpvp8depay", None)?;
let dec = make_element("vp8dec", None)?;
let conv = make_element("videoconvert", None)?;
let scale = make_element("videoscale", None)?;
let filter = make_element("capsfilter", None)?;
let rtp_caps = gst::Caps::builder("application/x-rtp")
.field("clock-rate", 90000i32)
.build();
let video_caps = gst_video::VideoCapsBuilder::new()
.width(1920)
.height(1080)
.build();
let src = gst::ElementFactory::make("udpsrc")
.property("address", "127.0.0.1")
.property("caps", &rtp_caps)
.build()?;
let netsim = gst::ElementFactory::make("netsim")
.property("drop-probability", drop_probability)
.build()?;
let rtpbin = gst::ElementFactory::make("rtpbin")
.property("do-lost", true)
.build()?;
let depay = gst::ElementFactory::make("rtpvp8depay").build()?;
let dec = gst::ElementFactory::make("vp8dec").build()?;
let conv = gst::ElementFactory::make("videoconvert").build()?;
let scale = gst::ElementFactory::make("videoscale").build()?;
let filter = gst::ElementFactory::make("capsfilter")
.property("caps", &video_caps)
.build()?;
pipeline.add_many([&src, &netsim, &rtpbin, &depay, &dec, &conv, &scale, &filter])?;
gst::Element::link_many([&depay, &dec, &conv, &scale, &filter])?;
pipeline.add_many(&[&src, &netsim, &rtpbin, &depay, &dec, &conv, &scale, &filter])?;
gst::Element::link_many(&[&depay, &dec, &conv, &scale, &filter])?;
match args[1].as_str() {
"play" => {
let sink = gst::ElementFactory::make("autovideosink").build()?;
let sink = make_element("autovideosink", None)?;
pipeline.add(&sink)?;
filter.link(&sink)?;
}
"record" => {
let enc = gst::ElementFactory::make("x264enc")
.property_from_str("tune", "zerolatency")
.build()?;
let mux = gst::ElementFactory::make("matroskamux").build()?;
let sink = gst::ElementFactory::make("filesink")
.property("location", "out.mkv")
.build()?;
let enc = make_element("x264enc", None)?;
let mux = make_element("matroskamux", None)?;
let sink = make_element("filesink", None)?;
pipeline.add_many([&enc, &mux, &sink])?;
gst::Element::link_many([&filter, &enc, &mux, &sink])?;
pipeline.add_many(&[&enc, &mux, &sink])?;
gst::Element::link_many(&[&filter, &enc, &mux, &sink])?;
sink.set_property("location", &"out.mkv")?;
enc.set_property_from_str("tune", "zerolatency");
eprintln!("Recording to out.mkv");
}
_ => return Err(Error::from(UsageError(args[0].clone()))),
@ -145,10 +147,10 @@ fn example_main() -> Result<(), Error> {
let storage = values[1]
.get::<gst::Element>()
.expect("rtpbin \"new-storage\" signal values[1]");
storage.set_property("size-time", 250_000_000u64);
storage.set_property("size-time", &250_000_000u64).unwrap();
None
});
})?;
rtpbin.connect("request-pt-map", false, |values| {
let pt = values[2]
@ -156,24 +158,30 @@ fn example_main() -> Result<(), Error> {
.expect("rtpbin \"new-storage\" signal values[2]");
match pt {
100 => Some(
gst::Caps::builder("application/x-rtp")
.field("media", "video")
.field("clock-rate", 90000i32)
.field("is-fec", true)
.build()
.to_value(),
gst::Caps::new_simple(
"application/x-rtp",
&[
("media", &"video"),
("clock-rate", &90000i32),
("is-fec", &true),
],
)
.to_value(),
),
96 => Some(
gst::Caps::builder("application/x-rtp")
.field("media", "video")
.field("clock-rate", 90000i32)
.field("encoding-name", "VP8")
.build()
.to_value(),
gst::Caps::new_simple(
"application/x-rtp",
&[
("media", &"video"),
("clock-rate", &90000i32),
("encoding-name", &"VP8"),
],
)
.to_value(),
),
_ => None,
}
});
})?;
rtpbin.connect("request-fec-decoder", false, |values| {
let rtpbin = values[0]
@ -195,7 +203,7 @@ fn example_main() -> Result<(), Error> {
None
}
}
});
})?;
let srcpad = static_pad(&netsim, "src")?;
let sinkpad = request_pad(&rtpbin, "recv_rtp_sink_0")?;
@ -203,8 +211,9 @@ fn example_main() -> Result<(), Error> {
let depay_weak = depay.downgrade();
rtpbin.connect_pad_added(move |rtpbin, src_pad| {
let Some(depay) = depay_weak.upgrade() else {
return;
let depay = match depay_weak.upgrade() {
Some(depay) => depay,
None => return,
};
match connect_rtpbin_srcpad(src_pad, &depay) {
@ -220,6 +229,17 @@ fn example_main() -> Result<(), Error> {
}
});
let rtp_caps = gst::Caps::new_simple("application/x-rtp", &[("clock-rate", &90000i32)]);
let video_caps =
gst::Caps::new_simple("video/x-raw", &[("width", &1920i32), ("height", &1080i32)]);
src.set_property("address", &"127.0.0.1")?;
src.set_property("caps", &rtp_caps)?;
netsim.set_property("drop-probability", &drop_probability)?;
rtpbin.set_property("do-lost", &true)?;
filter.set_property("caps", &video_caps)?;
let bus = pipeline
.bus()
.expect("Pipeline without bus. Shouldn't happen!");
@ -241,18 +261,23 @@ fn example_main() -> Result<(), Error> {
return Err(ErrorMessage {
src: msg
.src()
.map(|s| s.path_string())
.unwrap_or_else(|| glib::GString::from("UNKNOWN")),
error: err.error(),
.map(|s| String::from(s.path_string()))
.unwrap_or_else(|| String::from("None")),
error: err.error().to_string(),
debug: err.debug(),
source: err.error(),
}
.into());
}
MessageView::StateChanged(s) => {
if let Some(element) = msg.src() {
if element == &pipeline && s.current() == gst::State::Playing {
if element == pipeline && s.current() == gst::State::Playing {
eprintln!("PLAYING");
pipeline.debug_to_dot_file(gst::DebugGraphDetails::all(), "client-playing");
gst::debug_bin_to_dot_file(
&pipeline,
gst::DebugGraphDetails::all(),
"client-playing",
);
}
}
}
@ -270,6 +295,6 @@ fn example_main() -> Result<(), Error> {
fn main() {
match examples_common::run(example_main) {
Ok(r) => r,
Err(e) => eprintln!("Error! {e}"),
Err(e) => eprintln!("Error! {}", e),
}
}

View file

@ -1,4 +1,5 @@
use gst::{element_error, prelude::*};
use gst::element_error;
use gst::prelude::*;
#[path = "../examples-common.rs"]
mod examples_common;
@ -9,21 +10,37 @@ use anyhow::Error;
use derive_more::{Display, Error};
#[derive(Debug, Display, Error)]
#[display(fmt = "No such pad {_0} in {_1}")]
#[display(fmt = "Missing element {}", _0)]
struct MissingElement(#[error(not(source))] &'static str);
#[derive(Debug, Display, Error)]
#[display(fmt = "No such pad {} in {}", _0, _1)]
struct NoSuchPad(&'static str, String);
#[derive(Debug, Display, Error)]
#[display(fmt = "Usage: {_0} URI FEC_PERCENTAGE")]
#[display(fmt = "Usage: {} URI FEC_PERCENTAGE", _0)]
struct UsageError(#[error(not(source))] String);
#[derive(Debug, Display, Error)]
#[display(fmt = "Received error from {src}: {error} (debug: {debug:?})")]
#[display(fmt = "Received error from {}: {} (debug: {:?})", src, error, debug)]
struct ErrorMessage {
src: glib::GString,
error: glib::Error,
debug: Option<glib::GString>,
src: String,
error: String,
debug: Option<String>,
source: glib::Error,
}
fn make_element(
factory_name: &'static str,
element_name: Option<&str>,
) -> Result<gst::Element, Error> {
match gst::ElementFactory::make(factory_name, element_name) {
Ok(elem) => Ok(elem),
Err(_) => Err(Error::from(MissingElement(factory_name))),
}
}
#[doc(alias = "get_static_pad")]
fn static_pad(element: &gst::Element, pad_name: &'static str) -> Result<gst::Pad, Error> {
match element.static_pad(pad_name) {
Some(pad) => Ok(pad),
@ -34,6 +51,7 @@ fn static_pad(element: &gst::Element, pad_name: &'static str) -> Result<gst::Pad
}
}
#[doc(alias = "get_request_pad")]
fn request_pad(element: &gst::Element, pad_name: &'static str) -> Result<gst::Pad, Error> {
match element.request_pad_simple(pad_name) {
Some(pad) => Ok(pad),
@ -52,11 +70,11 @@ fn connect_decodebin_pad(src_pad: &gst::Pad, sink: &gst::Element) -> Result<(),
}
fn make_fec_encoder(fec_percentage: u32) -> Result<gst::Element, Error> {
let fecenc = gst::ElementFactory::make("rtpulpfecenc")
.property("pt", 100u32)
.property("multipacket", true)
.property("percentage", fec_percentage)
.build()?;
let fecenc = make_element("rtpulpfecenc", None)?;
fecenc.set_property("pt", &100u32)?;
fecenc.set_property("multipacket", &true)?;
fecenc.set_property("percentage", &fec_percentage)?;
Ok(fecenc)
}
@ -73,33 +91,17 @@ fn example_main() -> Result<(), Error> {
let uri = &args[1];
let fec_percentage = args[2].parse::<u32>()?;
let video_caps = gst::Caps::builder("video/x-raw").build();
let pipeline = gst::Pipeline::new(None);
let src = make_element("uridecodebin", None)?;
let conv = make_element("videoconvert", None)?;
let q1 = make_element("queue", None)?;
let enc = make_element("vp8enc", None)?;
let q2 = make_element("queue", None)?;
let pay = make_element("rtpvp8pay", None)?;
let rtpbin = make_element("rtpbin", None)?;
let sink = make_element("udpsink", None)?;
let pipeline = gst::Pipeline::default();
let src = gst::ElementFactory::make("uridecodebin")
.property_from_str("pattern", "ball")
.property("expose-all-streams", false)
.property("caps", video_caps)
.property("uri", uri)
.build()?;
let conv = gst::ElementFactory::make("videoconvert").build()?;
let q1 = gst::ElementFactory::make("queue").build()?;
let enc = gst::ElementFactory::make("vp8enc")
.property("keyframe-max-dist", 30i32)
.property("threads", 12i32)
.property("cpu-used", -16i32)
.property("deadline", 1i64)
.property_from_str("error-resilient", "default")
.build()?;
let q2 = gst::ElementFactory::make("queue").build()?;
let pay = gst::ElementFactory::make("rtpvp8pay").build()?;
let rtpbin = gst::ElementFactory::make("rtpbin").build()?;
let sink = gst::ElementFactory::make("udpsink")
.property("host", "127.0.0.1")
.property("sync", true)
.build()?;
pipeline.add_many([&src, &conv, &q1, &enc, &q2, &pay, &rtpbin, &sink])?;
pipeline.add_many(&[&src, &conv, &q1, &enc, &q2, &pay, &rtpbin, &sink])?;
conv.link(&q1)?;
q1.link(&enc)?;
@ -123,7 +125,7 @@ fn example_main() -> Result<(), Error> {
None
}
}
});
})?;
let srcpad = static_pad(&q2, "src")?;
let sinkpad = request_pad(&rtpbin, "send_rtp_sink_0")?;
@ -147,6 +149,20 @@ fn example_main() -> Result<(), Error> {
},
);
let video_caps = gst::Caps::new_simple("video/x-raw", &[]);
src.set_property_from_str("pattern", "ball");
sink.set_property("host", &"127.0.0.1")?;
sink.set_property("sync", &true)?;
enc.set_property("keyframe-max-dist", &30i32)?;
enc.set_property("threads", &12i32)?;
enc.set_property("cpu-used", &(-16i32))?;
enc.set_property("deadline", &1i64)?;
enc.set_property_from_str("error-resilient", "default");
src.set_property("expose-all-streams", &false)?;
src.set_property("caps", &video_caps)?;
src.set_property("uri", &uri)?;
let bus = pipeline
.bus()
.expect("Pipeline without bus. Shouldn't happen!");
@ -168,18 +184,23 @@ fn example_main() -> Result<(), Error> {
return Err(ErrorMessage {
src: msg
.src()
.map(|s| s.path_string())
.unwrap_or_else(|| glib::GString::from("UNKNOWN")),
error: err.error(),
.map(|s| String::from(s.path_string()))
.unwrap_or_else(|| String::from("None")),
error: err.error().to_string(),
debug: err.debug(),
source: err.error(),
}
.into());
}
MessageView::StateChanged(s) => {
if let Some(element) = msg.src() {
if element == &pipeline && s.current() == gst::State::Playing {
if element == pipeline && s.current() == gst::State::Playing {
eprintln!("PLAYING");
pipeline.debug_to_dot_file(gst::DebugGraphDetails::all(), "server-playing");
gst::debug_bin_to_dot_file(
&pipeline,
gst::DebugGraphDetails::all(),
"server-playing",
);
}
}
}
@ -197,6 +218,6 @@ fn example_main() -> Result<(), Error> {
fn main() {
match examples_common::run(example_main) {
Ok(r) => r,
Err(e) => eprintln!("Error! {e}"),
Err(e) => eprintln!("Error! {}", e),
}
}

View file

@ -1,223 +0,0 @@
// This example demonstrates how to set up a rtsp server using GStreamer
// and extending the default auth module behaviour by subclassing RTSPAuth
// For this, the example creates a videotestsrc pipeline manually to be used
// by the RTSP server for providing data
#![allow(clippy::non_send_fields_in_send_ty)]
use anyhow::Error;
use derive_more::{Display, Error};
use gst_rtsp_server::prelude::*;
#[path = "../examples-common.rs"]
mod examples_common;
#[derive(Debug, Display, Error)]
#[display(fmt = "Could not get mount points")]
struct NoMountPoints;
fn main_loop() -> Result<(), Error> {
let main_loop = glib::MainLoop::new(None, false);
let server = gst_rtsp_server::RTSPServer::new();
// We create our custom auth module.
// The job of the auth module is to authenticate users and authorize
// factories access/construction.
let auth = auth::Auth::default();
server.set_auth(Some(&auth));
// Much like HTTP servers, RTSP servers have multiple endpoints that
// provide different streams. Here, we ask our server to give
// us a reference to his list of endpoints, so we can add our
// test endpoint, providing the pipeline from the cli.
let mounts = server.mount_points().ok_or(NoMountPoints)?;
// Next, we create a factory for the endpoint we want to create.
// The job of the factory is to create a new pipeline for each client that
// connects, or (if configured to do so) to reuse an existing pipeline.
let factory = gst_rtsp_server::RTSPMediaFactory::new();
// Here we tell the media factory the media we want to serve.
// This is done in the launch syntax. When the first client connects,
// the factory will use this syntax to create a new pipeline instance.
factory.set_launch("( videotestsrc ! vp8enc ! rtpvp8pay name=pay0 )");
// This setting specifies whether each connecting client gets the output
// of a new instance of the pipeline, or whether all connected clients share
// the output of the same pipeline.
// If you want to stream a fixed video you have stored on the server to any
// client, you would not set this to shared here (since every client wants
// to start at the beginning of the video). But if you want to distribute
// a live source, you will probably want to set this to shared, to save
// computing and memory capacity on the server.
factory.set_shared(true);
// Now we add a new mount-point and tell the RTSP server to serve the content
// provided by the factory we configured above, when a client connects to
// this specific path.
mounts.add_factory("/test", factory);
// Attach the server to our main context.
// A main context is the thing where other stuff is registering itself for its
// events (e.g. sockets, GStreamer bus, ...) and the main loop is something that
// polls the main context for its events and dispatches them to whoever is
// interested in them. In this example, we only do have one, so we can
// leave the context parameter empty, it will automatically select
// the default one.
let id = server.attach(None)?;
println!(
"Stream ready at rtsp://127.0.0.1:{}/test",
server.bound_port()
);
println!("user admin/password can access stream");
println!("user demo/demo passes authentication but receives 404");
println!("other users do not pass pass authentication and receive 401");
// Start the mainloop. From this point on, the server will start to serve
// our quality content to connecting clients.
main_loop.run();
id.remove();
Ok(())
}
// Our custom auth module
mod auth {
// In the imp submodule we include the actual implementation
mod imp {
use gst_rtsp::{RTSPHeaderField, RTSPStatusCode};
use gst_rtsp_server::{prelude::*, subclass::prelude::*, RTSPContext};
// This is the private data of our auth
#[derive(Default)]
pub struct Auth;
impl Auth {
// Simulate external auth validation and user extraction
// authorized users are admin/password and demo/demo
fn external_auth(&self, auth: &str) -> Option<String> {
if let Ok(decoded) = data_encoding::BASE64.decode(auth.as_bytes()) {
if let Ok(decoded) = std::str::from_utf8(&decoded) {
let tokens = decoded.split(':').collect::<Vec<_>>();
if tokens == vec!["admin", "password"] || tokens == vec!["demo", "demo"] {
return Some(tokens[0].into());
}
}
}
None
}
// Simulate external role check
// admin user can construct and access media factory
fn external_access_check(&self, user: &str) -> bool {
user == "admin"
}
}
// This trait registers our type with the GObject object system and
// provides the entry points for creating a new instance and setting
// up the class data
#[glib::object_subclass]
impl ObjectSubclass for Auth {
const NAME: &'static str = "RsRTSPAuth";
type Type = super::Auth;
type ParentType = gst_rtsp_server::RTSPAuth;
}
// Implementation of glib::Object virtual methods
impl ObjectImpl for Auth {}
// Implementation of gst_rtsp_server::RTSPAuth virtual methods
impl RTSPAuthImpl for Auth {
fn authenticate(&self, ctx: &RTSPContext) -> bool {
// authenticate should always be called with a valid context request
let req = ctx
.request()
.expect("Context without request. Should not happen !");
if let Some(auth_credentials) = req.parse_auth_credentials().first() {
if let Some(authorization) = auth_credentials.authorization() {
if let Some(user) = self.external_auth(authorization) {
// Update context token with authenticated username
ctx.set_token(
gst_rtsp_server::RTSPToken::builder()
.field("user", user)
.build(),
);
return true;
}
}
}
false
}
fn check(&self, ctx: &RTSPContext, role: &glib::GString) -> bool {
// We only check media factory access
if !role.starts_with("auth.check.media.factory") {
return true;
}
if ctx.token().is_none() {
// If we do not have a context token yet, check if there are any auth credentials in request
if !self.authenticate(ctx) {
// If there were no credentials, send a "401 Unauthorized" response
if let Some(resp) = ctx.response() {
resp.init_response(RTSPStatusCode::Unauthorized, ctx.request());
resp.add_header(
RTSPHeaderField::WwwAuthenticate,
"Basic realm=\"CustomRealm\"",
);
if let Some(client) = ctx.client() {
client.send_message(resp, ctx.session());
}
}
return false;
}
}
if let Some(token) = ctx.token() {
// If we already have a user token...
if self.external_access_check(&token.string("user").unwrap_or_default()) {
// grant access if user may access factory
return true;
} else {
// send a "404 Not Found" response if user may not access factory
if let Some(resp) = ctx.response() {
resp.init_response(RTSPStatusCode::NotFound, ctx.request());
if let Some(client) = ctx.client() {
client.send_message(resp, ctx.session());
}
}
}
}
false
}
}
}
// This here defines the public interface of our auth and implements
// the corresponding traits so that it behaves like any other RTSPAuth
glib::wrapper! {
pub struct Auth(ObjectSubclass<imp::Auth>) @extends gst_rtsp_server::RTSPAuth;
}
impl Default for Auth {
// Creates a new instance of our auth
fn default() -> Self {
glib::Object::new()
}
}
}
fn example_main() -> Result<(), Error> {
gst::init()?;
main_loop()
}
fn main() {
match examples_common::run(example_main) {
Ok(r) => r,
Err(e) => eprintln!("Error! {e}"),
}
}

View file

@ -5,10 +5,13 @@
// to this example's cli is spawned and the client's media is streamed into it.
use std::env;
use std::ptr;
use glib::translate::*;
use gst_rtsp_server::prelude::*;
use anyhow::Error;
use derive_more::{Display, Error};
use gst_rtsp_server::prelude::*;
#[path = "../examples-common.rs"]
mod examples_common;
@ -18,7 +21,7 @@ mod examples_common;
struct NoMountPoints;
#[derive(Debug, Display, Error)]
#[display(fmt = "Usage: {_0} LAUNCH_LINE")]
#[display(fmt = "Usage: {} LAUNCH_LINE", _0)]
struct UsageError(#[error(not(source))] String);
fn main_loop() -> Result<(), Error> {
@ -44,11 +47,12 @@ fn main_loop() -> Result<(), Error> {
// Here we configure a method of authentication that we want the
// server to require from clients.
let auth = gst_rtsp_server::RTSPAuth::new();
let token = gst_rtsp_server::RTSPToken::builder()
.field(gst_rtsp_server::RTSP_TOKEN_MEDIA_FACTORY_ROLE, "user")
.build();
let token = gst_rtsp_server::RTSPToken::new(&[(
*gst_rtsp_server::RTSP_TOKEN_MEDIA_FACTORY_ROLE,
&"user",
)]);
let basic = gst_rtsp_server::RTSPAuth::make_basic("user", "password");
// For proper authentication, we want to use encryption. And there's no
// For propery authentication, we want to use encryption. And there's no
// encryption without a certificate!
let cert = gio::TlsCertificate::from_pem(
"-----BEGIN CERTIFICATE-----\
@ -76,14 +80,24 @@ fn main_loop() -> Result<(), Error> {
W535W8UBbEg=-----END PRIVATE KEY-----",
)?;
// Bindable versions were added in b1f515178a363df0322d7adbd5754e1f6e2083c9
// This declares that the user "user" (once authenticated) has a role that
// allows them to access and construct media factories.
factory.add_role_from_structure(
&gst::Structure::builder("user")
.field(gst_rtsp_server::RTSP_PERM_MEDIA_FACTORY_ACCESS, true)
.field(gst_rtsp_server::RTSP_PERM_MEDIA_FACTORY_CONSTRUCT, true)
.build(),
);
unsafe {
gst_rtsp_server::ffi::gst_rtsp_media_factory_add_role(
factory.to_glib_none().0,
"user".to_glib_none().0,
gst_rtsp_server::RTSP_PERM_MEDIA_FACTORY_ACCESS
.to_glib_none()
.0,
<bool as StaticType>::static_type().into_glib() as *const u8,
true.into_glib() as *const u8,
gst_rtsp_server::RTSP_PERM_MEDIA_FACTORY_CONSTRUCT.as_ptr() as *const u8,
<bool as StaticType>::static_type().into_glib() as *const u8,
true.into_glib() as *const u8,
ptr::null_mut::<u8>(),
);
}
auth.set_tls_certificate(Some(&cert));
auth.add_basic(basic.as_str(), &token);
@ -107,7 +121,7 @@ fn main_loop() -> Result<(), Error> {
// Now we add a new mount-point and tell the RTSP server to use the factory
// we configured beforehand. This factory will take on the job of creating
// a pipeline, which will take on the incoming data of connected clients.
mounts.add_factory("/test", factory);
mounts.add_factory("/test", &factory);
// Attach the server to our main context.
// A main context is the thing where other stuff is registering itself for its
@ -127,7 +141,7 @@ fn main_loop() -> Result<(), Error> {
// incoming connections from clients.
main_loop.run();
id.remove();
glib::source_remove(id);
Ok(())
}
@ -140,6 +154,6 @@ fn example_main() -> Result<(), Error> {
fn main() {
match examples_common::run(example_main) {
Ok(r) => r,
Err(e) => eprintln!("Error! {e}"),
Err(e) => eprintln!("Error! {}", e),
}
}

View file

@ -6,11 +6,11 @@
//
// It also comes with a custom RTSP server/client subclass for hooking into
// the client machinery and printing some status.
#![allow(clippy::non_send_fields_in_send_ty)]
use gst_rtsp_server::prelude::*;
use anyhow::Error;
use derive_more::{Display, Error};
use gst_rtsp_server::prelude::*;
#[path = "../examples-common.rs"]
mod examples_common;
@ -19,6 +19,10 @@ mod examples_common;
#[display(fmt = "Could not get mount points")]
struct NoMountPoints;
#[derive(Debug, Display, Error)]
#[display(fmt = "Usage: {} LAUNCH_LINE", _0)]
struct UsageError(#[error(not(source))] String);
fn main_loop() -> Result<(), Error> {
let main_loop = glib::MainLoop::new(None, false);
let server = server::Server::default();
@ -49,7 +53,7 @@ fn main_loop() -> Result<(), Error> {
// Now we add a new mount-point and tell the RTSP server to serve the content
// provided by the factory we configured above, when a client connects to
// this specific path.
mounts.add_factory("/test", factory);
mounts.add_factory("/test", &factory);
// Attach the server to our main context.
// A main context is the thing where other stuff is registering itself for its
@ -69,17 +73,17 @@ fn main_loop() -> Result<(), Error> {
// our quality content to connecting clients.
main_loop.run();
id.remove();
glib::source_remove(id);
Ok(())
}
// Our custom media factory that creates a media input manually
mod media_factory {
use gst_rtsp_server::subclass::prelude::*;
use super::*;
use gst_rtsp_server::subclass::prelude::*;
// In the imp submodule we include the actual implementation
mod imp {
use super::*;
@ -100,10 +104,9 @@ mod media_factory {
// Implementation of glib::Object virtual methods
impl ObjectImpl for Factory {
fn constructed(&self) {
self.parent_constructed();
fn constructed(&self, factory: &Self::Type) {
self.parent_constructed(factory);
let factory = self.obj();
// All media created by this factory are our custom media type. This would
// not require a media factory subclass and can also be called on the normal
// RTSPMediaFactory.
@ -113,28 +116,27 @@ mod media_factory {
// Implementation of gst_rtsp_server::RTSPMediaFactory virtual methods
impl RTSPMediaFactoryImpl for Factory {
fn create_element(&self, _url: &gst_rtsp::RTSPUrl) -> Option<gst::Element> {
fn create_element(
&self,
_factory: &Self::Type,
_url: &gst_rtsp::RTSPUrl,
) -> Option<gst::Element> {
// Create a simple VP8 videotestsrc input
let bin = gst::Bin::default();
let src = gst::ElementFactory::make("videotestsrc")
// Configure the videotestsrc live
.property("is-live", true)
.build()
.unwrap();
let enc = gst::ElementFactory::make("vp8enc")
// Produce encoded data as fast as possible
.property("deadline", 1i64)
.build()
.unwrap();
let bin = gst::Bin::new(None);
let src = gst::ElementFactory::make("videotestsrc", None).unwrap();
let enc = gst::ElementFactory::make("vp8enc", None).unwrap();
// The names of the payloaders must be payX
let pay = gst::ElementFactory::make("rtpvp8pay")
.name("pay0")
.build()
.unwrap();
let pay = gst::ElementFactory::make("rtpvp8pay", Some("pay0")).unwrap();
bin.add_many([&src, &enc, &pay]).unwrap();
gst::Element::link_many([&src, &enc, &pay]).unwrap();
// Configure the videotestsrc live
src.set_property("is-live", &true).unwrap();
// Produce encoded data as fast as possible
enc.set_property("deadline", &1i64).unwrap();
bin.add_many(&[&src, &enc, &pay]).unwrap();
gst::Element::link_many(&[&src, &enc, &pay]).unwrap();
Some(bin.upcast())
}
@ -147,10 +149,14 @@ mod media_factory {
pub struct Factory(ObjectSubclass<imp::Factory>) @extends gst_rtsp_server::RTSPMediaFactory;
}
// Factories must be Send+Sync, and ours is
unsafe impl Send for Factory {}
unsafe impl Sync for Factory {}
impl Default for Factory {
// Creates a new instance of our factory
fn default() -> Factory {
glib::Object::new()
glib::Object::new(&[]).expect("Failed to create factory")
}
}
}
@ -184,10 +190,11 @@ mod media {
impl RTSPMediaImpl for Media {
fn setup_sdp(
&self,
media: &Self::Type,
sdp: &mut gst_sdp::SDPMessageRef,
info: &gst_rtsp_server::subclass::SDPInfo,
) -> Result<(), gst::LoggableError> {
self.parent_setup_sdp(sdp, info)?;
self.parent_setup_sdp(media, sdp, info)?;
sdp.add_attribute("my-custom-attribute", Some("has-a-value"));
@ -201,15 +208,19 @@ mod media {
glib::wrapper! {
pub struct Media(ObjectSubclass<imp::Media>) @extends gst_rtsp_server::RTSPMedia;
}
// Medias must be Send+Sync, and ours is
unsafe impl Send for Media {}
unsafe impl Sync for Media {}
}
// Our custom RTSP server subclass that reports when clients are connecting and uses
// our custom RTSP client subclass for each client
mod server {
use gst_rtsp_server::subclass::prelude::*;
use super::*;
use gst_rtsp_server::subclass::prelude::*;
// In the imp submodule we include the actual implementation
mod imp {
use super::*;
@ -233,8 +244,7 @@ mod server {
// Implementation of gst_rtsp_server::RTSPServer virtual methods
impl RTSPServerImpl for Server {
fn create_client(&self) -> Option<gst_rtsp_server::RTSPClient> {
let server = self.obj();
fn create_client(&self, server: &Self::Type) -> Option<gst_rtsp_server::RTSPClient> {
let client = super::client::Client::default();
// Duplicated from the default implementation
@ -246,9 +256,9 @@ mod server {
Some(client.upcast())
}
fn client_connected(&self, client: &gst_rtsp_server::RTSPClient) {
self.parent_client_connected(client);
println!("Client {client:?} connected");
fn client_connected(&self, server: &Self::Type, client: &gst_rtsp_server::RTSPClient) {
self.parent_client_connected(server, client);
println!("Client {:?} connected", client);
}
}
}
@ -259,10 +269,14 @@ mod server {
pub struct Server(ObjectSubclass<imp::Server>) @extends gst_rtsp_server::RTSPServer;
}
// Servers must be Send+Sync, and ours is
unsafe impl Send for Server {}
unsafe impl Sync for Server {}
impl Default for Server {
// Creates a new instance of our factory
fn default() -> Server {
glib::Object::new()
glib::Object::new(&[]).expect("Failed to create server")
}
}
}
@ -294,16 +308,9 @@ mod client {
// Implementation of gst_rtsp_server::RTSPClient virtual methods
impl RTSPClientImpl for Client {
fn closed(&self) {
let client = self.obj();
self.parent_closed();
println!("Client {client:?} closed");
}
fn describe_request(&self, ctx: &gst_rtsp_server::RTSPContext) {
self.parent_describe_request(ctx);
let request_uri = ctx.uri().unwrap().request_uri();
println!("Describe request for uri: {request_uri:?}");
fn closed(&self, client: &Self::Type) {
self.parent_closed(client);
println!("Client {:?} closed", client);
}
}
}
@ -314,10 +321,14 @@ mod client {
pub struct Client(ObjectSubclass<imp::Client>) @extends gst_rtsp_server::RTSPClient;
}
// Clients must be Send+Sync, and ours is
unsafe impl Send for Client {}
unsafe impl Sync for Client {}
impl Default for Client {
// Creates a new instance of our factory
fn default() -> Client {
glib::Object::new()
glib::Object::new(&[]).expect("Failed to create client")
}
}
}
@ -347,9 +358,13 @@ mod mount_points {
// Implementation of gst_rtsp_server::RTSPClient virtual methods
impl RTSPMountPointsImpl for MountPoints {
fn make_path(&self, url: &gst_rtsp::RTSPUrl) -> Option<glib::GString> {
println!("Make path called for {url:?} ");
self.parent_make_path(url)
fn make_path(
&self,
mount_points: &Self::Type,
url: &gst_rtsp::RTSPUrl,
) -> Option<glib::GString> {
println!("Make path called for {:?} ", url);
self.parent_make_path(mount_points, url)
}
}
}
@ -358,10 +373,14 @@ mod mount_points {
pub struct MountPoints(ObjectSubclass<imp::MountPoints>) @extends gst_rtsp_server::RTSPMountPoints;
}
// MountPoints must be Send+Sync, and ours is
unsafe impl Send for MountPoints {}
unsafe impl Sync for MountPoints {}
impl Default for MountPoints {
// Creates a new instance of our factory
fn default() -> Self {
glib::Object::new()
glib::Object::new(&[]).expect("Failed to create mount points")
}
}
}
@ -374,6 +393,6 @@ fn example_main() -> Result<(), Error> {
fn main() {
match examples_common::run(example_main) {
Ok(r) => r,
Err(e) => eprintln!("Error! {e}"),
Err(e) => eprintln!("Error! {}", e),
}
}

View file

@ -5,9 +5,10 @@
use std::env;
use gst_rtsp_server::prelude::*;
use anyhow::Error;
use derive_more::{Display, Error};
use gst_rtsp_server::prelude::*;
#[path = "../examples-common.rs"]
mod examples_common;
@ -17,7 +18,7 @@ mod examples_common;
struct NoMountPoints;
#[derive(Debug, Display, Error)]
#[display(fmt = "Usage: {_0} LAUNCH_LINE")]
#[display(fmt = "Usage: {} LAUNCH_LINE", _0)]
struct UsageError(#[error(not(source))] String);
fn main_loop() -> Result<(), Error> {
@ -56,7 +57,7 @@ fn main_loop() -> Result<(), Error> {
// Now we add a new mount-point and tell the RTSP server to serve the content
// provided by the factory we configured above, when a client connects to
// this specific path.
mounts.add_factory("/test", factory);
mounts.add_factory("/test", &factory);
// Attach the server to our main context.
// A main context is the thing where other stuff is registering itself for its
@ -76,7 +77,7 @@ fn main_loop() -> Result<(), Error> {
// our quality content to connecting clients.
main_loop.run();
id.remove();
glib::source_remove(id);
Ok(())
}
@ -89,6 +90,6 @@ fn example_main() -> Result<(), Error> {
fn main() {
match examples_common::run(example_main) {
Ok(r) => r,
Err(e) => eprintln!("Error! {e}"),
Err(e) => eprintln!("Error! {}", e),
}
}

View file

@ -5,19 +5,24 @@
//
// Our filter can only handle F32 mono and acts as a FIR filter. The filter impulse response /
// coefficients are provided via Rust API on the filter as a Vec<f32>.
#![allow(clippy::non_send_fields_in_send_ty)]
use gst::prelude::*;
use gst::{element_error, gst_info, gst_trace};
use anyhow::Error;
use derive_more::{Display, Error};
use gst::prelude::*;
#[path = "../examples-common.rs"]
mod examples_common;
// Our custom FIR filter element is defined in this module
mod fir_filter {
use byte_slice_cast::*;
use super::*;
use gst_base::subclass::prelude::*;
use byte_slice_cast::*;
use once_cell::sync::Lazy;
// The debug category we use below for our filter
@ -31,9 +36,10 @@ mod fir_filter {
// In the imp submodule we include the actual implementation
mod imp {
use std::{collections::VecDeque, sync::Mutex};
use super::*;
use std::collections::VecDeque;
use std::i32;
use std::sync::Mutex;
// This is the private data of our filter
#[derive(Default)]
@ -55,41 +61,40 @@ mod fir_filter {
// Implementation of glib::Object virtual methods
impl ObjectImpl for FirFilter {}
impl GstObjectImpl for FirFilter {}
// Implementation of gst::Element virtual methods
impl ElementImpl for FirFilter {
// The element specific metadata. This information is what is visible from
// gst-inspect-1.0 and can also be programmatically retrieved from the gst::Registry
// gst-inspect-1.0 and can also be programatically retrieved from the gst::Registry
// after initial registration without having to load the plugin in memory.
fn metadata() -> Option<&'static gst::subclass::ElementMetadata> {
static ELEMENT_METADATA: std::sync::OnceLock<gst::subclass::ElementMetadata> =
std::sync::OnceLock::new();
Some(ELEMENT_METADATA.get_or_init(|| {
static ELEMENT_METADATA: Lazy<gst::subclass::ElementMetadata> = Lazy::new(|| {
gst::subclass::ElementMetadata::new(
"FIR Filter",
"Filter/Effect/Audio",
"A FIR audio filter",
"Sebastian Dröge <sebastian@centricular.com>",
)
}))
});
Some(&*ELEMENT_METADATA)
}
fn pad_templates() -> &'static [gst::PadTemplate] {
static PAD_TEMPLATES: std::sync::OnceLock<Vec<gst::PadTemplate>> =
std::sync::OnceLock::new();
PAD_TEMPLATES.get_or_init(|| {
static PAD_TEMPLATES: Lazy<Vec<gst::PadTemplate>> = Lazy::new(|| {
// Create pad templates for our sink and source pad. These are later used for
// actually creating the pads and beforehand already provide information to
// GStreamer about all possible pads that could exist for this type.
// On both of pads we can only handle F32 mono at any sample rate.
let caps = gst_audio::AudioCapsBuilder::new_interleaved()
.format(gst_audio::AUDIO_FORMAT_F32)
.channels(1)
.build();
let caps = gst::Caps::new_simple(
"audio/x-raw",
&[
("format", &gst_audio::AUDIO_FORMAT_F32.to_str()),
("rate", &gst::IntRange::<i32>::new(1, i32::MAX)),
("channels", &1i32),
("layout", &"interleaved"),
],
);
vec![
// The src pad template must be named "src" for basetransform
@ -111,7 +116,9 @@ mod fir_filter {
)
.unwrap(),
]
})
});
PAD_TEMPLATES.as_ref()
}
}
@ -131,18 +138,18 @@ mod fir_filter {
// Returns the size of one processing unit (i.e. a frame in our case) corresponding
// to the given caps. This is used for allocating a big enough output buffer and
// sanity checking the input buffer size, among other things.
fn unit_size(&self, caps: &gst::Caps) -> Option<usize> {
fn unit_size(&self, _element: &Self::Type, caps: &gst::Caps) -> Option<usize> {
let audio_info = gst_audio::AudioInfo::from_caps(caps).ok();
audio_info.map(|info| info.bpf() as usize)
}
// Called when shutting down the element so we can release all stream-related state
// There's also start(), which is called whenever starting the element again
fn stop(&self) -> Result<(), gst::ErrorMessage> {
fn stop(&self, element: &Self::Type) -> Result<(), gst::ErrorMessage> {
// Drop state
self.history.lock().unwrap().clear();
gst::info!(CAT, imp: self, "Stopped");
gst_info!(CAT, obj: element, "Stopped");
Ok(())
}
@ -150,19 +157,20 @@ mod fir_filter {
// Does the actual transformation of the input buffer to the output buffer
fn transform_ip(
&self,
element: &Self::Type,
buf: &mut gst::BufferRef,
) -> Result<gst::FlowSuccess, gst::FlowError> {
// Get coefficients and return directly if we have none
let coeffs = self.coeffs.lock().unwrap();
if coeffs.is_empty() {
gst::trace!(CAT, imp: self, "No coefficients set -- passthrough");
gst_trace!(CAT, obj: element, "No coefficients set -- passthrough");
return Ok(gst::FlowSuccess::Ok);
}
// Try mapping the input buffer as writable
let mut data = buf.map_writable().map_err(|_| {
gst::element_imp_error!(
self,
element_error!(
element,
gst::CoreError::Failed,
["Failed to map input buffer readable"]
);
@ -171,8 +179,8 @@ mod fir_filter {
// And reinterprete it as a slice of f32
let samples = data.as_mut_slice_of::<f32>().map_err(|err| {
gst::element_imp_error!(
self,
element_error!(
element,
gst::CoreError::Failed,
["Failed to cast input buffer as f32 slice: {}", err]
);
@ -181,9 +189,9 @@ mod fir_filter {
let mut history = self.history.lock().unwrap();
gst::trace!(
gst_trace!(
CAT,
imp: self,
obj: element,
"Transforming {} samples with filter of length {}",
samples.len(),
coeffs.len()
@ -215,46 +223,58 @@ mod fir_filter {
pub struct FirFilter(ObjectSubclass<imp::FirFilter>) @extends gst_base::BaseTransform, gst::Element, gst::Object;
}
// GStreamer elements must be Send+Sync, and ours is
unsafe impl Send for FirFilter {}
unsafe impl Sync for FirFilter {}
impl FirFilter {
// Creates a new instance of our filter with the given name
pub fn new(name: Option<&str>) -> FirFilter {
glib::Object::builder().property("name", name).build()
glib::Object::new(&[("name", &name)]).expect("Failed to create fir filter")
}
// Sets the coefficients by getting access to the private
// struct and simply setting them
pub fn set_coeffs(&self, coeffs: Vec<f32>) {
let imp = self.imp();
let imp = imp::FirFilter::from_instance(self);
*imp.coeffs.lock().unwrap() = coeffs;
}
}
}
#[derive(Debug, Display, Error)]
#[display(fmt = "Received error from {src}: {error} (debug: {debug:?})")]
#[display(fmt = "Missing element {}", _0)]
struct MissingElement(#[error(not(source))] &'static str);
#[derive(Debug, Display, Error)]
#[display(fmt = "Received error from {}: {} (debug: {:?})", src, error, debug)]
struct ErrorMessage {
src: glib::GString,
error: glib::Error,
debug: Option<glib::GString>,
src: String,
error: String,
debug: Option<String>,
source: glib::Error,
}
fn create_pipeline() -> Result<gst::Pipeline, Error> {
gst::init()?;
// Create our pipeline with the custom element
let pipeline = gst::Pipeline::default();
let src = gst::ElementFactory::make("audiotestsrc")
.property_from_str("wave", "white-noise")
.build()?;
let pipeline = gst::Pipeline::new(None);
let src = gst::ElementFactory::make("audiotestsrc", None)
.map_err(|_| MissingElement("audiotestsrc"))?;
let filter = fir_filter::FirFilter::new(None);
let conv = gst::ElementFactory::make("audioconvert").build()?;
let sink = gst::ElementFactory::make("autoaudiosink").build()?;
let conv = gst::ElementFactory::make("audioconvert", None)
.map_err(|_| MissingElement("audioconvert"))?;
let sink = gst::ElementFactory::make("autoaudiosink", None)
.map_err(|_| MissingElement("autoaudiosink"))?;
pipeline.add_many([&src, filter.upcast_ref(), &conv, &sink])?;
pipeline.add_many(&[&src, filter.upcast_ref(), &conv, &sink])?;
src.link(&filter)?;
filter.link(&conv)?;
conv.link(&sink)?;
src.set_property_from_str("wave", "white-noise");
// Create a windowed sinc lowpass filter at 1/64 sample rate,
// i.e. 689Hz for 44.1kHz sample rate
let w = 2.0 * std::f32::consts::PI / 64.0;
@ -304,10 +324,11 @@ fn main_loop(pipeline: gst::Pipeline) -> Result<(), Error> {
return Err(ErrorMessage {
src: msg
.src()
.map(|s| s.path_string())
.unwrap_or_else(|| glib::GString::from("UNKNOWN")),
error: err.error(),
.map(|s| String::from(s.path_string()))
.unwrap_or_else(|| String::from("None")),
error: err.error().to_string(),
debug: err.debug(),
source: err.error(),
}
.into());
}
@ -323,7 +344,7 @@ fn main_loop(pipeline: gst::Pipeline) -> Result<(), Error> {
fn example_main() {
match create_pipeline().and_then(main_loop) {
Ok(r) => r,
Err(e) => eprintln!("Error! {e}"),
Err(e) => eprintln!("Error! {}", e),
}
}

View file

@ -1,188 +0,0 @@
// In the imp submodule we include the actual implementation
use std::{collections::VecDeque, sync::Mutex};
use glib::prelude::*;
use gst_audio::subclass::prelude::*;
use once_cell::sync::Lazy;
use byte_slice_cast::*;
use atomic_refcell::AtomicRefCell;
// The debug category we use below for our filter
pub static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
gst::DebugCategory::new(
"rsiirfilter",
gst::DebugColorFlags::empty(),
Some("Rust IIR Filter"),
)
});
#[derive(Default)]
// This is the state of our filter
struct State {
a: Vec<f64>,
b: Vec<f64>,
x: VecDeque<f64>,
y: VecDeque<f64>,
}
// This is the private data of our filter
#[derive(Default)]
pub struct IirFilter {
coeffs: Mutex<Option<(Vec<f64>, Vec<f64>)>>,
state: AtomicRefCell<State>,
}
// This trait registers our type with the GObject object system and
// provides the entry points for creating a new instance and setting
// up the class data
#[glib::object_subclass]
impl ObjectSubclass for IirFilter {
const NAME: &'static str = "RsIirFilter";
const ABSTRACT: bool = true;
type Type = super::IirFilter;
type ParentType = gst_audio::AudioFilter;
type Class = super::Class;
// Here we set default implementations for all the virtual methods.
// This is mandatory for all virtual methods that are not `Option`s.
fn class_init(class: &mut Self::Class) {
class.set_rate = |obj, rate| obj.imp().set_rate_default(rate);
}
}
// Implementation of glib::Object virtual methods
impl ObjectImpl for IirFilter {}
impl GstObjectImpl for IirFilter {}
// Implementation of gst::Element virtual methods
impl ElementImpl for IirFilter {}
// Implementation of gst_base::BaseTransform virtual methods
impl BaseTransformImpl for IirFilter {
// Configure basetransform so that we are always running in-place,
// don't passthrough on same caps and also never call transform_ip
// in passthrough mode (which does not matter for us here).
//
// The way how our processing is implemented, in-place transformation
// is simpler.
const MODE: gst_base::subclass::BaseTransformMode =
gst_base::subclass::BaseTransformMode::AlwaysInPlace;
const PASSTHROUGH_ON_SAME_CAPS: bool = false;
const TRANSFORM_IP_ON_PASSTHROUGH: bool = false;
fn start(&self) -> Result<(), gst::ErrorMessage> {
self.parent_start()?;
*self.state.borrow_mut() = State::default();
Ok(())
}
fn stop(&self) -> Result<(), gst::ErrorMessage> {
self.parent_stop()?;
*self.state.borrow_mut() = State::default();
Ok(())
}
fn transform_ip(&self, buf: &mut gst::BufferRef) -> Result<gst::FlowSuccess, gst::FlowError> {
let mut state = self.state.borrow_mut();
// Update coefficients if new coefficients were set
{
let mut coeffs = self.coeffs.lock().unwrap();
if let Some((a, b)) = coeffs.take() {
state.x.clear();
state.y.clear();
if !a.is_empty() {
state.y.resize(a.len() - 1, 0.0);
}
if !b.is_empty() {
state.x.resize(b.len() - 1, 0.0);
}
state.a = a;
state.b = b;
}
}
if state.a.is_empty() | state.b.is_empty() {
return Ok(gst::FlowSuccess::Ok);
}
let mut map = buf.map_writable().map_err(|_| {
gst::error!(CAT, imp: self, "Failed to map buffer writable");
gst::FlowError::Error
})?;
let samples = map.as_mut_slice_of::<f32>().unwrap();
assert!(state.b.len() - 1 == state.x.len());
assert!(state.a.len() - 1 == state.y.len());
for sample in samples.iter_mut() {
let mut val = state.b[0] * *sample as f64;
for (b, x) in Iterator::zip(state.b.iter().skip(1), state.x.iter()) {
val += b * x;
}
for (a, y) in Iterator::zip(state.a.iter().skip(1), state.y.iter()) {
val -= a * y;
}
val /= state.a[0];
let _ = state.x.pop_back().unwrap();
state.x.push_front(*sample as f64);
let _ = state.y.pop_back().unwrap();
state.y.push_front(val);
*sample = val as f32;
}
Ok(gst::FlowSuccess::Ok)
}
}
impl AudioFilterImpl for IirFilter {
fn allowed_caps() -> &'static gst::Caps {
static CAPS: std::sync::OnceLock<gst::Caps> = std::sync::OnceLock::new();
CAPS.get_or_init(|| {
// On both of pads we can only handle F32 mono at any sample rate.
gst_audio::AudioCapsBuilder::new_interleaved()
.format(gst_audio::AUDIO_FORMAT_F32)
.channels(1)
.build()
})
}
fn setup(&self, info: &gst_audio::AudioInfo) -> Result<(), gst::LoggableError> {
self.parent_setup(info)?;
gst::debug!(CAT, imp: self, "Rate changed to {}", info.rate());
let obj = self.obj();
(obj.class().as_ref().set_rate)(&obj, info.rate());
Ok(())
}
}
/// Wrappers for public methods and associated helper functions.
impl IirFilter {
pub(super) fn set_coeffs(&self, a: Vec<f64>, b: Vec<f64>) {
gst::debug!(CAT, imp: self, "Setting coefficients a: {a:?}, b: {b:?}");
*self.coeffs.lock().unwrap() = Some((a, b));
}
}
/// Default virtual method implementations.
impl IirFilter {
fn set_rate_default(&self, _rate: u32) {}
}

View file

@ -1,86 +0,0 @@
use gst::{prelude::*, subclass::prelude::*};
use gst_audio::subclass::prelude::*;
mod imp;
// This here defines the public interface of our element and implements
// the corresponding traits so that it behaves like any other gst::Element
//
// GObject
// ╰──GstObject
// ╰──GstElement
// ╰──GstBaseTransform
// ╰──GstAudioFilter
// ╰──IirFilter
glib::wrapper! {
pub struct IirFilter(ObjectSubclass<imp::IirFilter>) @extends gst_audio::AudioFilter, gst_base::BaseTransform, gst::Element, gst::Object;
}
/// Trait containing extension methods for `IirFilter`.
pub trait IirFilterExt: IsA<IirFilter> {
// Sets the coefficients by getting access to the private struct and simply setting them
fn set_coeffs(&self, a: Vec<f64>, b: Vec<f64>) {
self.upcast_ref::<IirFilter>().imp().set_coeffs(a, b)
}
}
impl<O: IsA<IirFilter>> IirFilterExt for O {}
/// Trait to implement in `IirFilter` subclasses.
pub trait IirFilterImpl: AudioFilterImpl {
/// Called whenever the sample rate is changing.
fn set_rate(&self, rate: u32) {
self.parent_set_rate(rate);
}
}
/// Trait containing extension methods for `IirFilterImpl`, specifically methods for chaining
/// up to the parent implementation of virtual methods.
pub trait IirFilterImplExt: IirFilterImpl {
fn parent_set_rate(&self, rate: u32) {
unsafe {
let data = Self::type_data();
let parent_class = &*(data.as_ref().parent_class() as *mut Class);
(parent_class.set_rate)(self.obj().unsafe_cast_ref(), rate)
}
}
}
impl<T: IirFilterImpl> IirFilterImplExt for T {}
/// Class struct for `IirFilter`.
#[repr(C)]
pub struct Class {
parent: <<imp::IirFilter as ObjectSubclass>::ParentType as ObjectType>::GlibClassType,
set_rate: fn(&IirFilter, rate: u32),
}
unsafe impl ClassStruct for Class {
type Type = imp::IirFilter;
}
/// This allows directly using `Class` as e.g. `gst_audio::AudioFilterClass` or
/// `gst_base::BaseTransformClass` without having to cast.
impl std::ops::Deref for Class {
type Target = glib::Class<<<Self as ClassStruct>::Type as ObjectSubclass>::ParentType>;
fn deref(&self) -> &Self::Target {
unsafe { &*(&self.parent as *const _ as *const _) }
}
}
/// Overrides the virtual methods with the actual implementation of the subclass as is provided by
/// the subclass' implementation of the `Impl` trait.
unsafe impl<T: IirFilterImpl> IsSubclassable<T> for IirFilter {
fn class_init(class: &mut glib::Class<Self>) {
Self::parent_class_init::<T>(class);
let class = class.as_mut();
class.set_rate = |obj, rate| unsafe {
let imp = obj.unsafe_cast_ref::<T::Type>().imp();
imp.set_rate(rate);
};
}
}

View file

@ -1,170 +0,0 @@
// In the imp submodule we include the actual implementation
use std::sync::Mutex;
use glib::prelude::*;
use gst::prelude::*;
use gst_audio::subclass::prelude::*;
use crate::iirfilter::{IirFilterExt, IirFilterImpl};
// These are the property values of our filter
pub struct Settings {
cutoff: f32,
}
impl Default for Settings {
fn default() -> Self {
Settings { cutoff: 0.0 }
}
}
// This is the state of our filter
#[derive(Default)]
pub struct State {
rate: Option<u32>,
}
// This is the private data of our filter
#[derive(Default)]
pub struct Lowpass {
settings: Mutex<Settings>,
state: Mutex<State>,
}
// This trait registers our type with the GObject object system and
// provides the entry points for creating a new instance and setting
// up the class data
#[glib::object_subclass]
impl ObjectSubclass for Lowpass {
const NAME: &'static str = "RsLowpass";
type Type = super::Lowpass;
type ParentType = crate::iirfilter::IirFilter;
}
// Implementation of glib::Object virtual methods
impl ObjectImpl for Lowpass {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: std::sync::OnceLock<Vec<glib::ParamSpec>> = std::sync::OnceLock::new();
PROPERTIES.get_or_init(|| {
vec![glib::ParamSpecFloat::builder("cutoff")
.nick("Cutoff")
.blurb("Cutoff frequency in Hz")
.default_value(Settings::default().cutoff)
.minimum(0.0)
.mutable_playing()
.build()]
})
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
match pspec.name() {
"cutoff" => {
self.settings.lock().unwrap().cutoff = value.get().unwrap();
self.calculate_coeffs();
}
_ => unimplemented!(),
};
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
"cutoff" => self.settings.lock().unwrap().cutoff.to_value(),
_ => unimplemented!(),
}
}
}
impl GstObjectImpl for Lowpass {}
// Implementation of gst::Element virtual methods
impl ElementImpl for Lowpass {
// The element specific metadata. This information is what is visible from
// gst-inspect-1.0 and can also be programmatically retrieved from the gst::Registry
// after initial registration without having to load the plugin in memory.
fn metadata() -> Option<&'static gst::subclass::ElementMetadata> {
static ELEMENT_METADATA: std::sync::OnceLock<gst::subclass::ElementMetadata> =
std::sync::OnceLock::new();
Some(ELEMENT_METADATA.get_or_init(|| {
gst::subclass::ElementMetadata::new(
"Lowpass Filter",
"Filter/Effect/Audio",
"A Lowpass audio filter",
"Sebastian Dröge <sebastian@centricular.com>",
)
}))
}
}
// Implementation of gst_base::BaseTransform virtual methods
impl BaseTransformImpl for Lowpass {
const MODE: gst_base::subclass::BaseTransformMode =
<<crate::iirfilter::IirFilter as glib::object::ObjectSubclassIs>::Subclass>::MODE;
const PASSTHROUGH_ON_SAME_CAPS: bool =
<<crate::iirfilter::IirFilter as glib::object::ObjectSubclassIs>::Subclass>::PASSTHROUGH_ON_SAME_CAPS;
const TRANSFORM_IP_ON_PASSTHROUGH: bool =
<<crate::iirfilter::IirFilter as glib::object::ObjectSubclassIs>::Subclass>::TRANSFORM_IP_ON_PASSTHROUGH;
fn start(&self) -> Result<(), gst::ErrorMessage> {
self.parent_start()?;
*self.state.lock().unwrap() = State::default();
Ok(())
}
}
// Implement of gst_audio::AudioFilter virtual methods
impl AudioFilterImpl for Lowpass {}
// Implement of IirFilter virtual methods
impl IirFilterImpl for Lowpass {
fn set_rate(&self, rate: u32) {
// Could call
// self.parent_set_rate(rate);
// here but chaining up is not necessary if the base class doesn't require that
// or if the behaviour of the parent class should be completely overridden.
self.state.lock().unwrap().rate = Some(rate);
self.calculate_coeffs();
}
}
impl Lowpass {
fn calculate_coeffs(&self) {
use std::f64;
let Some(rate) = self.state.lock().unwrap().rate else {
return;
};
let cutoff = self.settings.lock().unwrap().cutoff;
// See Audio EQ Cookbook
// https://www.w3.org/TR/audio-eq-cookbook
let cutoff = cutoff as f64 / rate as f64;
let omega = 2.0 * f64::consts::PI * cutoff;
let q = 1.0;
let alpha = f64::sin(omega) / (2.0 * q);
let mut b = vec![
(1.0 - f64::cos(omega)) / 2.0,
1.0 - f64::cos(omega),
(1.0 - f64::cos(omega) / 2.0),
];
let mut a = vec![1.0 + alpha, -2.0 * f64::cos(omega), 1.0 - alpha];
let a0 = a[0];
for a in &mut a {
*a /= a0;
}
for b in &mut b {
*b /= a0;
}
self.obj().set_coeffs(a, b);
}
}

View file

@ -1,15 +0,0 @@
mod imp;
// This here defines the public interface of our element and implements
// the corresponding traits so that it behaves like any other gst::Element
//
// GObject
// ╰──GstObject
// ╰──GstElement
// ╰──GstBaseTransform
// ╰──GstAudioFilter
// ╰──IirFilter
// ╰──Lowpass
glib::wrapper! {
pub struct Lowpass(ObjectSubclass<imp::Lowpass>) @extends crate::iirfilter::IirFilter, gst_audio::AudioFilter, gst_base::BaseTransform, gst::Element, gst::Object;
}

View file

@ -1,66 +0,0 @@
// This example implements a baseclass IirFilter, and a subclass Lowpass of that.
//
// The example shows how to provide and implement virtual methods, and how to provide non-virtual
// methods on the base class.
use gst::prelude::*;
mod iirfilter;
mod lowpass;
#[path = "../../examples-common.rs"]
mod examples_common;
fn example_main() {
gst::init().unwrap();
let pipeline = gst::Pipeline::new();
let src = gst::ElementFactory::make("audiotestsrc")
.property_from_str("wave", "white-noise")
.build()
.unwrap();
let filter = glib::Object::builder::<lowpass::Lowpass>()
.property("cutoff", 4000.0f32)
.build();
let conv = gst::ElementFactory::make("audioconvert").build().unwrap();
let sink = gst::ElementFactory::make("autoaudiosink").build().unwrap();
pipeline
.add_many([&src, filter.as_ref(), &conv, &sink])
.unwrap();
gst::Element::link_many([&src, filter.as_ref(), &conv, &sink]).unwrap();
let bus = pipeline.bus().unwrap();
pipeline
.set_state(gst::State::Playing)
.expect("Unable to set the pipeline to the `Playing` state");
for msg in bus.iter_timed(gst::ClockTime::NONE) {
use gst::MessageView;
match msg.view() {
MessageView::Eos(..) => break,
MessageView::Error(err) => {
println!(
"Error from {:?}: {} ({:?})",
err.src().map(|s| s.path_string()),
err.error(),
err.debug()
);
break;
}
_ => (),
}
}
pipeline
.set_state(gst::State::Null)
.expect("Unable to set the pipeline to the `Null` state");
}
fn main() {
// tutorials_common::run is only required to set up the application environment on macOS
// (but not necessary in normal Cocoa applications where this is set up automatically)
examples_common::run(example_main);
}

View file

@ -18,23 +18,26 @@
// (More modes of operation are possible, see: gst::TagMergeMode)
// This merge-mode can also be supplied to any method that adds new tags.
use anyhow::{anyhow, Error};
use derive_more::{Display, Error};
use gst::prelude::*;
use anyhow::anyhow;
use anyhow::Error;
use derive_more::{Display, Error};
#[path = "../examples-common.rs"]
mod examples_common;
#[derive(Debug, Display, Error)]
#[display(fmt = "Missing element {_0}")]
#[display(fmt = "Missing element {}", _0)]
struct MissingElement(#[error(not(source))] String);
#[derive(Debug, Display, Error)]
#[display(fmt = "Received error from {src}: {error} (debug: {debug:?})")]
#[display(fmt = "Received error from {}: {} (debug: {:?})", src, error, debug)]
struct ErrorMessage {
src: glib::GString,
error: glib::Error,
debug: Option<glib::GString>,
src: String,
error: String,
debug: Option<String>,
source: glib::Error,
}
fn example_main() -> Result<(), Error> {
@ -42,7 +45,7 @@ fn example_main() -> Result<(), Error> {
// Parse the pipeline we want to probe from a static in-line string.
let mut context = gst::ParseContext::new();
let pipeline = match gst::parse::launch_full(
let pipeline = match gst::parse_launch_full(
"audiotestsrc wave=white-noise num-buffers=100 ! flacenc ! filesink location=test.flac",
Some(&mut context),
gst::ParseFlags::empty(),
@ -76,8 +79,7 @@ fn example_main() -> Result<(), Error> {
// Set the "title" tag to "Special randomized white-noise".
// The second parameter gst::TagMergeMode::Append tells the tagsetter to append this title
// if there already is one.
tagsetter
.add_tag::<gst::tags::Title>(&"Special randomized white-noise", gst::TagMergeMode::Append);
tagsetter.add::<gst::tags::Title>(&"Special randomized white-noise", gst::TagMergeMode::Append);
let bus = pipeline.bus().unwrap();
@ -90,12 +92,14 @@ fn example_main() -> Result<(), Error> {
MessageView::Eos(..) => break,
MessageView::Error(err) => {
return Err(ErrorMessage {
src: msg
src: err
.src()
.map(|s| s.path_string())
.unwrap_or_else(|| glib::GString::from("UNKNOWN")),
error: err.error(),
.unwrap_or_else(|| "None".into())
.to_string(),
error: err.error().to_string(),
debug: err.debug(),
source: err.error(),
}
.into());
}
@ -113,6 +117,6 @@ fn main() {
// (but not necessary in normal Cocoa applications where this is set up automatically)
match examples_common::run(example_main) {
Ok(r) => r,
Err(e) => eprintln!("Error! {e}"),
Err(e) => eprintln!("Error! {}", e),
}
}

View file

@ -3,32 +3,38 @@
// {uridecodebin} - {videoconvert} - {appsink}
// The appsink enforces RGBx so that the image crate can use it. The sample layout is passed
// with the correct stride from GStreamer to the image crate as GStreamer does not necessarily
// produce tightly packed pixels, and in case of RGBx never.
// The appsink enforces RGBA so that the image crate can use it. The image crate also requires
// tightly packed pixels, which is the case for RGBA by default in GStreamer.
use gst::element_error;
use gst::prelude::*;
use anyhow::Error;
use derive_more::{Display, Error};
use gst::{element_error, prelude::*};
use gst_video::prelude::*;
#[path = "../examples-common.rs"]
mod examples_common;
#[derive(Debug, Display, Error)]
#[display(fmt = "Received error from {src}: {error} (debug: {debug:?})")]
#[display(fmt = "Missing element {}", _0)]
struct MissingElement(#[error(not(source))] &'static str);
#[derive(Debug, Display, Error)]
#[display(fmt = "Received error from {}: {} (debug: {:?})", src, error, debug)]
struct ErrorMessage {
src: glib::GString,
error: glib::Error,
debug: Option<glib::GString>,
src: String,
error: String,
debug: Option<String>,
source: glib::Error,
}
fn create_pipeline(uri: String, out_path: std::path::PathBuf) -> Result<gst::Pipeline, Error> {
gst::init()?;
// Create our pipeline from a pipeline description string.
let pipeline = gst::parse::launch(&format!(
"uridecodebin uri={uri} ! videoconvert ! appsink name=sink"
let pipeline = gst::parse_launch(&format!(
"uridecodebin uri={} ! videoconvert ! appsink name=sink",
uri
))?
.downcast::<gst::Pipeline>()
.expect("Expected a gst::Pipeline");
@ -41,14 +47,14 @@ fn create_pipeline(uri: String, out_path: std::path::PathBuf) -> Result<gst::Pip
.expect("Sink element is expected to be an appsink!");
// Don't synchronize on the clock, we only want a snapshot asap.
appsink.set_property("sync", false);
appsink.set_property("sync", &false).unwrap();
// Tell the appsink what format we want.
// This can be set after linking the two objects, because format negotiation between
// both elements will happen during pre-rolling of the pipeline.
appsink.set_caps(Some(
&gst_video::VideoCapsBuilder::new()
.format(gst_video::VideoFormat::Rgbx)
&gst::Caps::builder("video/x-raw")
.field("format", &gst_video::VideoFormat::Rgba.to_str())
.build(),
));
@ -72,15 +78,15 @@ fn create_pipeline(uri: String, out_path: std::path::PathBuf) -> Result<gst::Pip
gst::FlowError::Error
})?;
let caps = sample.caps().expect("Sample without caps");
let info = gst_video::VideoInfo::from_caps(caps).expect("Failed to parse caps");
// Make sure that we only get a single buffer
if got_snapshot {
return Err(gst::FlowError::Eos);
}
got_snapshot = true;
let caps = sample.caps().expect("Sample without caps");
let info = gst_video::VideoInfo::from_caps(caps).expect("Failed to parse caps");
// At this point, buffer is only a reference to an existing memory region somewhere.
// When we want to access its content, we have to map it while requesting the required
// mode of access (read, read/write).
@ -88,49 +94,37 @@ fn create_pipeline(uri: String, out_path: std::path::PathBuf) -> Result<gst::Pip
// on the machine's main memory itself, but rather in the GPU's memory.
// So mapping the buffer makes the underlying memory region accessible to us.
// See: https://gstreamer.freedesktop.org/documentation/plugin-development/advanced/allocation.html
let frame = gst_video::VideoFrameRef::from_buffer_ref_readable(buffer, &info)
.map_err(|_| {
element_error!(
appsink,
gst::ResourceError::Failed,
("Failed to map buffer readable")
);
let map = buffer.map_readable().map_err(|_| {
element_error!(
appsink,
gst::ResourceError::Failed,
("Failed to map buffer readable")
);
gst::FlowError::Error
})?;
gst::FlowError::Error
})?;
// We only want to have a single buffer and then have the pipeline terminate
println!("Have video frame");
// Calculate a target width/height that keeps the display aspect ratio while having
// a height of 240 pixels
let display_aspect_ratio = (frame.width() as f64 * info.par().numer() as f64)
/ (frame.height() as f64 * info.par().denom() as f64);
let display_aspect_ratio = (info.width() as f64 * *info.par().numer() as f64)
/ (info.height() as f64 * *info.par().denom() as f64);
let target_height = 240;
let target_width = target_height as f64 * display_aspect_ratio;
// Create a FlatSamples around the borrowed video frame data from GStreamer with
// the correct stride as provided by GStreamer.
let img = image::FlatSamples::<&[u8]> {
samples: frame.plane_data(0).unwrap(),
layout: image::flat::SampleLayout {
channels: 3, // RGB
channel_stride: 1, // 1 byte from component to component
width: frame.width(),
width_stride: 4, // 4 byte from pixel to pixel
height: frame.height(),
height_stride: frame.plane_stride()[0] as usize, // stride from line to line
},
color_hint: Some(image::ColorType::Rgb8),
};
// Create an ImageBuffer around the borrowed video frame data from GStreamer.
let img = image::ImageBuffer::<image::Rgba<u8>, _>::from_raw(
info.width(),
info.height(),
map,
)
.expect("Failed to create ImageBuffer, probably a stride mismatch");
// Scale image to our target dimensions
let scaled_img = image::imageops::thumbnail(
&img.as_view::<image::Rgb<u8>>()
.expect("couldn't create image view"),
target_width as u32,
target_height as u32,
);
let scaled_img =
image::imageops::thumbnail(&img, target_width as u32, target_height as u32);
// Save it at the specific location. This automatically detects the file type
// based on the filename.
@ -174,7 +168,7 @@ fn main_loop(pipeline: gst::Pipeline, position: u64) -> Result<(), Error> {
MessageView::AsyncDone(..) => {
if !seeked {
// AsyncDone means that the pipeline has started now and that we can seek
println!("Got AsyncDone message, seeking to {position}s");
println!("Got AsyncDone message, seeking to {}s", position);
if pipeline
.seek_simple(gst::SeekFlags::FLUSH, position * gst::ClockTime::SECOND)
@ -201,10 +195,11 @@ fn main_loop(pipeline: gst::Pipeline, position: u64) -> Result<(), Error> {
return Err(ErrorMessage {
src: msg
.src()
.map(|s| s.path_string())
.unwrap_or_else(|| glib::GString::from("UNKNOWN")),
error: err.error(),
.map(|s| String::from(s.path_string()))
.unwrap_or_else(|| String::from("None")),
error: err.error().to_string(),
debug: err.debug(),
source: err.error(),
}
.into());
}
@ -240,7 +235,7 @@ fn example_main() {
match create_pipeline(uri, out_path).and_then(|pipeline| main_loop(pipeline, position)) {
Ok(r) => r,
Err(e) => eprintln!("Error! {e}"),
Err(e) => eprintln!("Error! {}", e),
}
}

View file

@ -9,10 +9,10 @@
// {filesrc} - {decodebin} - {queue} - {fakesink}
// \- ...
use std::env;
use gst::prelude::*;
use std::env;
#[path = "../examples-common.rs"]
mod examples_common;
@ -27,15 +27,14 @@ fn example_main() {
std::process::exit(-1)
};
let pipeline = gst::Pipeline::default();
let src = gst::ElementFactory::make("filesrc")
.property("location", uri)
.build()
.unwrap();
let decodebin = gst::ElementFactory::make("decodebin").build().unwrap();
let pipeline = gst::Pipeline::new(None);
let src = gst::ElementFactory::make("filesrc", None).unwrap();
let decodebin = gst::ElementFactory::make("decodebin", None).unwrap();
pipeline.add_many([&src, &decodebin]).unwrap();
gst::Element::link_many([&src, &decodebin]).unwrap();
src.set_property("location", &uri).unwrap();
pipeline.add_many(&[&src, &decodebin]).unwrap();
gst::Element::link_many(&[&src, &decodebin]).unwrap();
// Need to move a new reference into the closure.
// !!ATTENTION!!:
@ -52,15 +51,16 @@ fn example_main() {
decodebin.connect_pad_added(move |_, src_pad| {
// Here we temporarily retrieve a strong reference on the pipeline from the weak one
// we moved into this callback.
let Some(pipeline) = pipeline_weak.upgrade() else {
return;
let pipeline = match pipeline_weak.upgrade() {
Some(pipeline) => pipeline,
None => return,
};
// In this example, we are only interested about parsing the ToC, so
// we simply pipe every encountered stream into a fakesink, essentially
// throwing away the data.
let queue = gst::ElementFactory::make("queue").build().unwrap();
let sink = gst::ElementFactory::make("fakesink").build().unwrap();
let queue = gst::ElementFactory::make("queue", None).unwrap();
let sink = gst::ElementFactory::make("fakesink", None).unwrap();
let elements = &[&queue, &sink];
pipeline.add_many(elements).unwrap();
@ -108,7 +108,7 @@ fn example_main() {
println!("\nReceived toc: {:?} - updated: {}", toc.scope(), updated);
// Get a list of tags that are ToC specific.
if let Some(tags) = toc.tags() {
println!("- tags: {tags}");
println!("- tags: {}", tags.to_string());
}
// ToCs do not have a fixed structure. Depending on the format that
// they were parsed from, they might have different tree-like structures,
@ -123,11 +123,11 @@ fn example_main() {
println!("\t{:?} - {}", toc_entry.entry_type(), toc_entry.uid());
// Every ToC entry can have a set of timestamps (start, stop).
if let Some((start, stop)) = toc_entry.start_stop_times() {
println!("\t- start: {start}, stop: {stop}");
println!("\t- start: {}, stop: {}", start, stop);
}
// Every ToC entry can have tags to it.
if let Some(tags) = toc_entry.tags() {
println!("\t- tags: {tags}");
println!("\t- tags: {}", tags.to_string());
}
// Every ToC entry can have a set of child entries.
// With this structure, you can create trees of arbitrary depth.
@ -138,10 +138,10 @@ fn example_main() {
toc_sub_entry.uid()
);
if let Some((start, stop)) = toc_sub_entry.start_stop_times() {
println!("\t\t- start: {start}, stop: {stop}");
println!("\t\t- start: {}, stop: {}", start, stop);
}
if let Some(tags) = toc_sub_entry.tags() {
println!("\t\t- tags: {tags}");
println!("\t\t- tags: {:?}", tags.to_string());
}
}
}

View file

@ -17,21 +17,28 @@
// {src} - {typefind} - {demuxer} -| {multiqueue} - {matroskamux} - {filesink}
// \-[video]-/
use gst::element_error;
use gst::prelude::*;
use std::env;
use anyhow::Error;
use derive_more::{Display, Error};
use gst::{element_error, prelude::*};
#[path = "../examples-common.rs"]
mod examples_common;
#[derive(Debug, Display, Error)]
#[display(fmt = "Received error from {src}: {error} (debug: {debug:?})")]
#[display(fmt = "Missing element {}", _0)]
struct MissingElement(#[error(not(source))] &'static str);
#[derive(Debug, Display, Error)]
#[display(fmt = "Received error from {}: {} (debug: {:?})", src, error, debug)]
struct ErrorMessage {
src: glib::GString,
error: glib::Error,
debug: Option<glib::GString>,
src: String,
error: String,
debug: Option<String>,
source: glib::Error,
}
fn example_main() -> Result<(), Error> {
@ -49,24 +56,33 @@ fn example_main() -> Result<(), Error> {
std::process::exit(-1)
};
let pipeline = gst::Pipeline::default();
let pipeline = gst::Pipeline::new(None);
let src = gst::Element::make_from_uri(gst::URIType::Src, uri, None)
.expect("We do not seem to support this uri");
let typefinder = gst::ElementFactory::make("typefind").build()?;
let queue = gst::ElementFactory::make("multiqueue")
.property("max-size-buffers", 0u32)
.property("max-size-time", 0u64)
.property("max-size-bytes", 1024u32 * 1024 * 100)
.build()?;
let muxer = gst::ElementFactory::make("matroskamux").build()?;
let sink = gst::ElementFactory::make("filesink")
.property("location", output_file)
.build()?;
let typefinder =
gst::ElementFactory::make("typefind", None).map_err(|_| MissingElement("typefind"))?;
let queue =
gst::ElementFactory::make("multiqueue", None).map_err(|_| MissingElement("multiqueue"))?;
let muxer = gst::ElementFactory::make("matroskamux", None)
.map_err(|_| MissingElement("matroskamux"))?;
let sink =
gst::ElementFactory::make("filesink", None).map_err(|_| MissingElement("filesink"))?;
sink.set_property("location", &output_file)
.expect("setting location property failed");
// Increase the queue capacity to 100MB to avoid a stalling pipeline
queue
.set_property("max-size-buffers", &0u32)
.expect("changing capacity of multiqueue failed");
queue
.set_property("max-size-time", &0u64)
.expect("changing capacity of multiqueue failed");
queue
.set_property("max-size-bytes", &(1024u32 * 1024 * 100))
.expect("changing capacity of multiqueue failed");
pipeline
.add_many([&src, &typefinder, &queue, &muxer, &sink])
.add_many(&[&src, &typefinder, &queue, &muxer, &sink])
.expect("failed to add elements to pipeline");
src.link(&typefinder)?;
@ -74,53 +90,55 @@ fn example_main() -> Result<(), Error> {
let pipeline_clone = pipeline.clone();
let typefinder_clone = typefinder.clone();
typefinder.connect("have-type", false, move |values| {
let (pipeline, typefinder) = (&pipeline_clone, &typefinder_clone);
typefinder
.connect("have-type", false, move |values| {
let (pipeline, typefinder) = (&pipeline_clone, &typefinder_clone);
// Use the detected format to select between a small set of supported demuxers
// Hint: This should probably never be done manually, for stuff like this,
// the decodebin should be used, that does this stuff automatically and handles
// much more corner-cases. This is just for the sake of being an example.
let caps = values[2]
.get::<gst::Caps>()
.expect("typefinder \"have-type\" signal values[2]");
let format_name = caps.structure(0).expect("Failed to get format name").name();
// Use the detected format to select between a small set of supported demuxers
// Hint: This should probably never be done manually, for stuff like this,
// the decodebin should be used, that does this stuff automatically and handles
// much more corner-cases. This is just for the sake of being an example.
let caps = values[2]
.get::<gst::Caps>()
.expect("typefinder \"have-type\" signal values[2]");
let format_name = caps.structure(0).expect("Failed to get format name").name();
let demuxer = match format_name.as_str() {
"video/x-matroska" | "video/webm" => gst::ElementFactory::make("matroskademux")
.build()
.expect("matroskademux missing"),
"video/quicktime" => gst::ElementFactory::make("qtdemux")
.build()
.expect("qtdemux missing"),
_ => {
eprintln!("Sorry, this format is not supported by this example.");
std::process::exit(-1);
}
};
let demuxer = match format_name {
"video/x-matroska" | "video/webm" => {
gst::ElementFactory::make("matroskademux", None).expect("matroskademux missing")
}
"video/quicktime" => {
gst::ElementFactory::make("qtdemux", None).expect("qtdemux missing")
}
_ => {
eprintln!("Sorry, this format is not supported by this example.");
std::process::exit(-1);
}
};
// We found a supported format and created the appropriate demuxer -> link it
pipeline
.add(&demuxer)
.expect("Failed to build remux pipeline");
// We simply keep the typefinder element and pipe the data through it.
// Removing is non-trivial since it started reading data from the pipeline
// that the next element (the format specific demuxer) would need.
typefinder
.link(&demuxer)
.expect("Failed to build remux pipeline");
// We found a supported format and created the appropriate demuxer -> link it
pipeline
.add(&demuxer)
.expect("Failed to build remux pipeline");
// We simply keep the typefinder element and pipe the data through it.
// Removing is non-trivial since it started reading data from the pipeline
// that the next element (the format specific demuxer) would need.
typefinder
.link(&demuxer)
.expect("Failed to build remux pipeline");
let queue_clone = queue.clone();
let muxer_clone = muxer.clone();
demuxer.connect_pad_added(move |demux, src_pad| {
handle_demux_pad_added(demux, src_pad, &queue_clone, &muxer_clone)
});
demuxer
.sync_state_with_parent()
.expect("Failed to build remux pipeline");
let queue_clone = queue.clone();
let muxer_clone = muxer.clone();
demuxer.connect_pad_added(move |demux, src_pad| {
handle_demux_pad_added(demux, src_pad, &queue_clone, &muxer_clone)
});
demuxer
.sync_state_with_parent()
.expect("Failed to build remux pipeline");
None
});
None
})
.expect("Failed to register have-type signal of typefind");
pipeline.set_state(gst::State::Playing)?;
@ -139,10 +157,11 @@ fn example_main() -> Result<(), Error> {
return Err(ErrorMessage {
src: msg
.src()
.map(|s| s.path_string())
.unwrap_or_else(|| glib::GString::from("UNKNOWN")),
error: err.error(),
.map(|s| String::from(s.path_string()))
.unwrap_or_else(|| String::from("None")),
error: err.error().to_string(),
debug: err.debug(),
source: err.error(),
}
.into());
}
@ -212,6 +231,6 @@ fn main() {
// (but not necessary in normal Cocoa applications where this is set up automatically)
match examples_common::run(example_main) {
Ok(r) => r,
Err(e) => eprintln!("Error! {e}"),
Err(e) => eprintln!("Error! {}", e),
}
}

View file

@ -11,7 +11,7 @@ fn example_main() {
gst::init().unwrap();
// This creates a pipeline by parsing the gst-launch pipeline syntax.
let pipeline = gst::parse::launch(
let pipeline = gst::parse_launch(
"videotestsrc name=src ! video/x-raw,width=640,height=480 ! compositor0.sink_0 \
compositor ! video/x-raw,width=1280,height=720 ! videoconvert ! autovideosink",
)
@ -24,10 +24,10 @@ fn example_main() {
/* Completely contrived example that takes the 4:3 input video, cuts out a 5:4 frame
* and then adds pillarbox borders to place it in a 16:9 target area */
/* The output will be the full frame: */
sinkpad.set_property("xpos", 0i32);
sinkpad.set_property("ypos", 0i32);
sinkpad.set_property("width", 1280i32);
sinkpad.set_property("height", 720i32);
sinkpad.set_property("xpos", &0i32).unwrap();
sinkpad.set_property("ypos", &0i32).unwrap();
sinkpad.set_property("width", &1280i32).unwrap();
sinkpad.set_property("height", &720i32).unwrap();
let mut converter_config = gst_video::VideoConverterConfig::new();
/* Crop the input frame to 5:4: */
@ -41,7 +41,9 @@ fn example_main() {
converter_config.set_dest_y(0);
converter_config.set_dest_height(Some(720));
sinkpad.set_property("converter-config", &*converter_config);
sinkpad
.set_property("converter-config", &*converter_config)
.unwrap();
pipeline
.set_state(gst::State::Playing)
.expect("Unable to set the pipeline to the `Playing` state");

View file

@ -1,229 +0,0 @@
// Zoom example using navigation events and a compositor
// Use can change the video player zoom using the next keys:
// * +: Zoom in
// * -: Zoom out
// * Up/Down/Right/Left: Move the frame
// * r: reset the zoom
// Also mouse navigation events can be used for a better UX.
use gst::prelude::*;
use gst_video::video_event::NavigationEvent;
use std::sync::Mutex;
#[path = "../examples-common.rs"]
mod examples_common;
const WIDTH: i32 = 1280;
const HEIGHT: i32 = 720;
#[derive(Default)]
struct MouseState {
clicked: bool,
clicked_x: f64,
clicked_y: f64,
clicked_xpos: i32,
clicked_ypos: i32,
}
fn zoom(mixer_sink_pad: gst::Pad, x: i32, y: i32, zoom_in: bool) {
let xpos = mixer_sink_pad.property::<i32>("xpos");
let ypos = mixer_sink_pad.property::<i32>("ypos");
let width = mixer_sink_pad.property::<i32>("width");
let height = mixer_sink_pad.property::<i32>("height");
let (width_offset, height_offset) = if zoom_in {
(WIDTH / 10, HEIGHT / 10)
} else {
(-WIDTH / 10, -HEIGHT / 10)
};
if width_offset + width <= 0 {
return;
}
mixer_sink_pad.set_property("width", width + width_offset);
mixer_sink_pad.set_property("height", height + height_offset);
let xpos_offset = ((x as f32 / WIDTH as f32) * width_offset as f32) as i32;
let new_xpos = xpos - xpos_offset;
let ypos_offset = ((y as f32 / HEIGHT as f32) * height_offset as f32) as i32;
let new_ypos = ypos - ypos_offset;
if new_xpos != xpos {
mixer_sink_pad.set_property("xpos", new_xpos);
}
if new_ypos != ypos {
mixer_sink_pad.set_property("ypos", new_ypos);
}
}
fn reset_zoom(mixer_sink_pad: gst::Pad) {
let xpos = mixer_sink_pad.property::<i32>("xpos");
let ypos = mixer_sink_pad.property::<i32>("ypos");
let width = mixer_sink_pad.property::<i32>("width");
let height = mixer_sink_pad.property::<i32>("height");
if 0 != xpos {
mixer_sink_pad.set_property("xpos", 0);
}
if 0 != ypos {
mixer_sink_pad.set_property("ypos", 0);
}
if WIDTH != width {
mixer_sink_pad.set_property("width", WIDTH);
}
if HEIGHT != height {
mixer_sink_pad.set_property("height", HEIGHT);
}
}
fn example_main() {
let clicked = Mutex::new(MouseState::default());
gst::init().unwrap();
let pipeline = gst::parse::launch(&format!(
"compositor name=mix background=1 sink_0::xpos=0 sink_0::ypos=0 sink_0::zorder=0 sink_0::width={WIDTH} sink_0::height={HEIGHT} ! xvimagesink \
videotestsrc name=src ! video/x-raw,framerate=30/1,width={WIDTH},height={HEIGHT},pixel-aspect-ratio=1/1 ! queue ! mix.sink_0"
)).unwrap().downcast::<gst::Pipeline>().unwrap();
let mixer = pipeline.by_name("mix").unwrap();
let mixer_src_pad = mixer.static_pad("src").unwrap();
let mixer_sink_pad_weak = mixer.static_pad("sink_0").unwrap().downgrade();
// Probe added in the sink pad to get direct navigation events w/o transformation done by the mixer
mixer_src_pad.add_probe(gst::PadProbeType::EVENT_UPSTREAM, move |_, probe_info| {
let mixer_sink_pad = mixer_sink_pad_weak.upgrade().unwrap();
let Some(ev) = probe_info.event() else {
return gst::PadProbeReturn::Ok;
};
if ev.type_() != gst::EventType::Navigation {
return gst::PadProbeReturn::Ok;
};
let Ok(nav_event) = NavigationEvent::parse(ev) else {
return gst::PadProbeReturn::Ok;
};
match nav_event {
NavigationEvent::KeyPress { key, .. } => match key.as_str() {
"Left" => {
let xpos = mixer_sink_pad.property::<i32>("xpos");
mixer_sink_pad.set_property("xpos", xpos - 10);
}
"Right" => {
let xpos = mixer_sink_pad.property::<i32>("xpos");
mixer_sink_pad.set_property("xpos", xpos + 10);
}
"Up" => {
let ypos = mixer_sink_pad.property::<i32>("ypos");
mixer_sink_pad.set_property("ypos", ypos - 10);
}
"Down" => {
let ypos = mixer_sink_pad.property::<i32>("ypos");
mixer_sink_pad.set_property("ypos", ypos + 10);
}
"plus" => {
zoom(mixer_sink_pad, WIDTH / 2, HEIGHT / 2, true);
}
"minus" => {
zoom(mixer_sink_pad, WIDTH / 2, HEIGHT / 2, false);
}
"r" => {
reset_zoom(mixer_sink_pad);
}
_ => (),
},
NavigationEvent::MouseMove { x, y, .. } => {
let state = clicked.lock().unwrap();
if state.clicked {
let xpos = mixer_sink_pad.property::<i32>("xpos");
let ypos = mixer_sink_pad.property::<i32>("ypos");
let new_xpos = state.clicked_xpos + (x - state.clicked_x) as i32;
let new_ypos = state.clicked_ypos + (y - state.clicked_y) as i32;
if new_xpos != xpos {
mixer_sink_pad.set_property("xpos", new_xpos);
}
if new_ypos != ypos {
mixer_sink_pad.set_property("ypos", new_ypos);
}
}
}
NavigationEvent::MouseButtonPress { button, x, y, .. } => {
if button == 1 || button == 272 {
let mut state = clicked.lock().unwrap();
state.clicked = true;
state.clicked_x = x;
state.clicked_y = y;
state.clicked_xpos = mixer_sink_pad.property("xpos");
state.clicked_ypos = mixer_sink_pad.property("ypos");
} else if button == 2 || button == 3 || button == 274 || button == 273 {
reset_zoom(mixer_sink_pad);
} else if button == 4 {
zoom(mixer_sink_pad, x as i32, y as i32, true);
} else if button == 5 {
zoom(mixer_sink_pad, x as i32, y as i32, false);
}
}
NavigationEvent::MouseButtonRelease { button, .. } => {
if button == 1 || button == 272 {
let mut state = clicked.lock().unwrap();
state.clicked = false;
}
}
#[cfg(feature = "v1_18")]
NavigationEvent::MouseScroll { x, y, delta_y, .. } => {
if delta_y > 0.0 {
zoom(mixer_sink_pad, x as i32, y as i32, true);
} else if delta_y < 0.0 {
zoom(mixer_sink_pad, x as i32, y as i32, false);
}
}
_ => (),
}
gst::PadProbeReturn::Ok
});
pipeline
.set_state(gst::State::Playing)
.expect("Unable to set the pipeline to the `Playing` state");
let bus = pipeline.bus().unwrap();
for msg in bus.iter_timed(gst::ClockTime::NONE) {
use gst::MessageView;
match msg.view() {
MessageView::Eos(..) => {
println!("received eos");
break;
}
MessageView::Error(err) => {
println!(
"Error from {:?}: {} ({:?})",
err.src().map(|s| s.path_string()),
err.error(),
err.debug()
);
break;
}
_ => (),
};
}
pipeline
.set_state(gst::State::Null)
.expect("Unable to set the pipeline to the `Null` state");
}
fn main() {
// tutorials_common::run is only required to set up the application environment on macOS
// (but not necessary in normal Cocoa applications where this is set up automatically)
examples_common::run(example_main);
}

View file

@ -17,47 +17,13 @@ pub fn run<T, F: FnOnce() -> T + Send + 'static>(main: F) -> T
where
T: Send + 'static,
{
use std::{
ffi::c_void,
sync::mpsc::{channel, Sender},
thread,
};
use cocoa::appkit::NSApplication;
use cocoa::{
appkit::{NSApplication, NSWindow},
base::id,
delegate,
};
use objc::{
class, msg_send,
runtime::{Object, Sel},
sel, sel_impl,
};
use std::thread;
unsafe {
let app = cocoa::appkit::NSApp();
let (send, recv) = channel::<()>();
extern "C" fn on_finish_launching(this: &Object, _cmd: Sel, _notification: id) {
let send = unsafe {
let send_pointer = *this.get_ivar::<*const c_void>("send");
let boxed = Box::from_raw(send_pointer as *mut Sender<()>);
*boxed
};
send.send(()).unwrap();
}
let delegate = delegate!("AppDelegate", {
app: id = app,
send: *const c_void = Box::into_raw(Box::new(send)) as *const c_void,
(applicationDidFinishLaunching:) => on_finish_launching as extern fn(&Object, Sel, id)
});
app.setDelegate_(delegate);
let t = thread::spawn(move || {
// Wait for the NSApp to launch to avoid possibly calling stop_() too early
recv.recv().unwrap();
let t = thread::spawn(|| {
let res = main();
let app = cocoa::appkit::NSApp();

View file

@ -1,37 +1,32 @@
//! This example demonstrates how to output GL textures, within an EGL/X11 context provided by the
//! application, and render those textures in the GL application.
//!
//! This example follow common patterns from `glutin`:
//! <https://github.com/rust-windowing/glutin/blob/master/glutin_examples/src/lib.rs>
// This example demonstrates how to output GL textures, within an
// EGL/X11 context provided by the application, and render those
// textures in the GL application.
// {videotestsrc} - { glsinkbin }
use std::{
ffi::{CStr, CString},
mem,
num::NonZeroU32,
ptr,
};
use anyhow::{Context, Result};
use derive_more::{Display, Error};
use glutin::{
config::GetGlConfig as _,
context::AsRawContext as _,
display::{AsRawDisplay as _, GetGlDisplay as _},
prelude::*,
};
use glutin_winit::GlWindow as _;
use gst::element_error;
use gst_gl::prelude::*;
use raw_window_handle::HasRawWindowHandle as _;
use std::ffi::CStr;
use std::mem;
use std::ptr;
use std::sync;
use anyhow::Error;
use derive_more::{Display, Error};
#[derive(Debug, Display, Error)]
#[display(fmt = "Received error from {src}: {error} (debug: {debug:?})")]
#[display(fmt = "Missing element {}", _0)]
struct MissingElement(#[error(not(source))] &'static str);
#[derive(Debug, Display, Error)]
#[display(fmt = "Received error from {}: {} (debug: {:?})", src, error, debug)]
struct ErrorMessage {
src: glib::GString,
error: glib::Error,
debug: Option<glib::GString>,
src: String,
error: String,
debug: Option<String>,
source: glib::Error,
}
#[rustfmt::skip]
@ -80,7 +75,6 @@ void main() {
#[allow(clippy::unused_unit)]
#[allow(clippy::too_many_arguments)]
#[allow(clippy::manual_non_exhaustive)]
#[allow(clippy::upper_case_acronyms)]
pub(crate) mod gl {
pub use self::Gles2 as Gl;
include!(concat!(env!("OUT_DIR"), "/test_gl_bindings.rs"));
@ -186,7 +180,7 @@ impl Gl {
}
}
fn resize(&self, size: winit::dpi::PhysicalSize<u32>) {
fn resize(&self, size: glutin::dpi::PhysicalSize<u32>) {
unsafe {
self.gl
.Viewport(0, 0, size.width as i32, size.height as i32);
@ -194,20 +188,17 @@ impl Gl {
}
}
fn load(gl_display: &impl glutin::display::GlDisplay) -> Gl {
let gl = gl::Gl::load_with(|symbol| {
let symbol = CString::new(symbol).unwrap();
gl_display.get_proc_address(&symbol).cast()
});
fn load(gl_context: &glutin::WindowedContext<glutin::PossiblyCurrent>) -> Gl {
let gl = gl::Gl::load_with(|ptr| gl_context.get_proc_address(ptr) as *const _);
let version = unsafe {
let version = gl.GetString(gl::VERSION);
assert!(!version.is_null());
let version = CStr::from_ptr(version.cast());
version.to_string_lossy()
let data = CStr::from_ptr(gl.GetString(gl::VERSION) as *const _)
.to_bytes()
.to_vec();
String::from_utf8(data).unwrap()
};
println!("OpenGL version {version}");
println!("OpenGL version {}", version);
let (program, attr_position, attr_texture, vao, vertex_buffer, vbo_indices) = unsafe {
let vs = gl.CreateShader(gl::VERTEX_SHADER);
@ -224,10 +215,9 @@ fn load(gl_display: &impl glutin::display::GlDisplay) -> Gl {
gl.LinkProgram(program);
{
let mut success = 1;
gl.GetProgramiv(program, gl::LINK_STATUS, &mut success);
assert_ne!(success, 0);
assert_eq!(gl.GetError(), 0);
let mut success: gl::types::GLint = 1;
gl.GetProgramiv(fs, gl::LINK_STATUS, &mut success);
assert!(success != 0);
}
let attr_position = gl.GetAttribLocation(program, b"a_position\0".as_ptr() as *const _);
@ -298,8 +288,6 @@ fn load(gl_display: &impl glutin::display::GlDisplay) -> Gl {
gl.BindBuffer(gl::ELEMENT_ARRAY_BUFFER, 0);
gl.BindBuffer(gl::ARRAY_BUFFER, 0);
assert_eq!(gl.GetError(), 0);
(
program,
attr_position,
@ -323,256 +311,198 @@ fn load(gl_display: &impl glutin::display::GlDisplay) -> Gl {
#[derive(Debug)]
enum Message {
Frame(gst_video::VideoInfo, gst::Buffer),
Sample(gst::Sample),
BusEvent,
}
pub(crate) struct App {
pipeline: gst::Pipeline,
appsink: gst_app::AppSink,
glupload: gst::Element,
bus: gst::Bus,
event_loop: winit::event_loop::EventLoop<Message>,
window: Option<winit::window::Window>,
not_current_gl_context: Option<glutin::context::NotCurrentContext>,
event_loop: glutin::event_loop::EventLoop<Message>,
windowed_context: glutin::WindowedContext<glutin::PossiblyCurrent>,
shared_context: gst_gl::GLContext,
}
impl App {
pub(crate) fn new(gl_element: Option<&gst::Element>) -> Result<App> {
pub(crate) fn new(gl_element: Option<&gst::Element>) -> Result<App, Error> {
gst::init()?;
let (pipeline, appsink) = App::create_pipeline(gl_element)?;
let (pipeline, appsink, glupload) = App::create_pipeline(gl_element)?;
let bus = pipeline
.bus()
.context("Pipeline without bus. Shouldn't happen!")?;
.expect("Pipeline without bus. Shouldn't happen!");
let event_loop = winit::event_loop::EventLoopBuilder::with_user_event().build()?;
let event_loop = glutin::event_loop::EventLoop::with_user_event();
let window = glutin::window::WindowBuilder::new().with_title("GL rendering");
let windowed_context = glutin::ContextBuilder::new()
.with_vsync(true)
.build_windowed(window, &event_loop)?;
// Only Windows requires the window to be present before creating a `glutin::Display`. Other
// platforms don't really need one (and on Android, none exists until `Event::Resumed`).
let window_builder = cfg!(windows).then(|| {
winit::window::WindowBuilder::new()
.with_transparent(true)
.with_title("GL rendering")
});
let windowed_context = unsafe { windowed_context.make_current().map_err(|(_, err)| err)? };
let display_builder =
glutin_winit::DisplayBuilder::new().with_window_builder(window_builder);
// XXX on macOS/cgl only one config can be queried at a time. If transparency is needed,
// add .with_transparency(true) to ConfigTemplateBuilder. EGL on X11 doesn't support
// transparency at all.
let template = glutin::config::ConfigTemplateBuilder::new().with_alpha_size(8);
let (window, gl_config) = display_builder
.build(&event_loop, template, |configs| {
configs
.reduce(|current, new_config| {
let prefer_transparency =
new_config.supports_transparency().unwrap_or(false)
& !current.supports_transparency().unwrap_or(false);
#[cfg(any(feature = "gst-gl-x11", feature = "gst-gl-wayland"))]
let inner_window = windowed_context.window();
if prefer_transparency || new_config.num_samples() > current.num_samples() {
new_config
} else {
current
}
})
.unwrap()
})
.expect("Failed to build display");
println!(
"Picked a config with {} samples and transparency {}. Pixel format: {:?}",
gl_config.num_samples(),
gl_config.supports_transparency().unwrap_or(false),
gl_config.color_buffer_type()
);
println!("Config supports GL API(s) {:?}", gl_config.api());
let shared_context: gst_gl::GLContext;
if cfg!(target_os = "linux") {
use glutin::platform::unix::RawHandle;
#[cfg(any(feature = "gst-gl-x11", feature = "gst-gl-wayland"))]
use glutin::platform::unix::WindowExtUnix;
use glutin::platform::ContextTraitExt;
// XXX The display could be obtained from any object created by it, so we can query it from
// the config.
let gl_display = gl_config.display();
let raw_gl_display = gl_display.raw_display();
let api = App::map_gl_api(windowed_context.get_api());
println!("Using raw display connection {:?}", raw_gl_display);
let (gl_context, gl_display, platform) = match unsafe { windowed_context.raw_handle() }
{
#[cfg(any(feature = "gst-gl-egl", feature = "gst-gl-wayland"))]
RawHandle::Egl(egl_context) => {
let mut gl_display = None;
let raw_window_handle = window.as_ref().map(|window| window.raw_window_handle());
#[cfg(feature = "gst-gl-egl")]
if let Some(display) = unsafe { windowed_context.get_egl_display() } {
gl_display = Some(
unsafe { gst_gl_egl::GLDisplayEGL::with_egl_display(display as usize) }
.unwrap()
.upcast::<gst_gl::GLDisplay>(),
)
};
// The context creation part. It can be created before surface and that's how
// it's expected in multithreaded + multiwindow operation mode, since you
// can send NotCurrentContext, but not Surface.
let context_attributes =
glutin::context::ContextAttributesBuilder::new().build(raw_window_handle);
// Since glutin by default tries to create OpenGL core context, which may not be
// present we should try gles.
let fallback_context_attributes = glutin::context::ContextAttributesBuilder::new()
.with_context_api(glutin::context::ContextApi::Gles(None))
.build(raw_window_handle);
// There are also some old devices that support neither modern OpenGL nor GLES.
// To support these we can try and create a 2.1 context.
let legacy_context_attributes = glutin::context::ContextAttributesBuilder::new()
.with_context_api(glutin::context::ContextApi::OpenGl(Some(
glutin::context::Version::new(2, 1),
)))
.build(raw_window_handle);
let not_current_gl_context = unsafe {
gl_display
.create_context(&gl_config, &context_attributes)
.or_else(|_| {
gl_display
.create_context(&gl_config, &fallback_context_attributes)
.or_else(|_| {
gl_display.create_context(&gl_config, &legacy_context_attributes)
})
})
}
.context("failed to create context")?;
let raw_gl_context = not_current_gl_context.raw_context();
println!("Using raw GL context {:?}", raw_gl_context);
#[cfg(not(target_os = "linux"))]
compile_error!("This example only has Linux support");
let api = App::map_gl_api(gl_config.api());
let (raw_gl_context, gst_gl_display, platform) = match (raw_gl_display, raw_gl_context) {
#[cfg(feature = "gst-gl-egl")]
(
glutin::display::RawDisplay::Egl(egl_display),
glutin::context::RawContext::Egl(egl_context),
) => {
let gl_display =
unsafe { gst_gl_egl::GLDisplayEGL::with_egl_display(egl_display as usize) }
.context("Failed to create GLDisplayEGL from raw `EGLDisplay`")?
.upcast::<gst_gl::GLDisplay>();
(egl_context as usize, gl_display, gst_gl::GLPlatform::EGL)
}
#[cfg(feature = "gst-gl-x11")]
(
glutin::display::RawDisplay::Glx(glx_display),
glutin::context::RawContext::Glx(glx_context),
) => {
let gl_display =
unsafe { gst_gl_x11::GLDisplayX11::with_display(glx_display as usize) }
.context("Failed to create GLDisplayX11 from raw X11 `Display`")?
.upcast::<gst_gl::GLDisplay>();
(glx_context as usize, gl_display, gst_gl::GLPlatform::GLX)
}
#[allow(unreachable_patterns)]
handler => anyhow::bail!("Unsupported platform: {handler:?}."),
};
let shared_context = unsafe {
gst_gl::GLContext::new_wrapped(&gst_gl_display, raw_gl_context, platform, api)
}
.context("Couldn't wrap GL context")?;
let gl_context = shared_context.clone();
let event_proxy = event_loop.create_proxy();
#[allow(clippy::single_match)]
bus.set_sync_handler(move |_, msg| {
match msg.view() {
gst::MessageView::NeedContext(ctxt) => {
let context_type = ctxt.context_type();
if context_type == *gst_gl::GL_DISPLAY_CONTEXT_TYPE {
if let Some(el) =
msg.src().map(|s| s.downcast_ref::<gst::Element>().unwrap())
{
let context = gst::Context::new(context_type, true);
context.set_gl_display(&gst_gl_display);
el.set_context(&context);
}
}
if context_type == "gst.gl.app_context" {
if let Some(el) =
msg.src().map(|s| s.downcast_ref::<gst::Element>().unwrap())
{
let mut context = gst::Context::new(context_type, true);
{
let context = context.get_mut().unwrap();
let s = context.structure_mut();
s.set("context", &gl_context);
#[cfg(feature = "gst-gl-wayland")]
if let Some(display) = inner_window.wayland_display() {
gl_display = Some(
unsafe {
gst_gl_wayland::GLDisplayWayland::with_display(display as usize)
}
.unwrap()
.upcast::<gst_gl::GLDisplay>(),
)
};
(
egl_context as usize,
gl_display.expect("Could not retrieve GLDisplay through EGL context and/or Wayland display"),
gst_gl::GLPlatform::EGL,
)
}
#[cfg(feature = "gst-gl-x11")]
RawHandle::Glx(glx_context) => {
let gl_display = if let Some(display) = inner_window.xlib_display() {
unsafe { gst_gl_x11::GLDisplayX11::with_display(display as usize) }.unwrap()
} else {
panic!("X11 window without X Display");
};
(
glx_context as usize,
gl_display.upcast::<gst_gl::GLDisplay>(),
gst_gl::GLPlatform::GLX,
)
}
#[allow(unreachable_patterns)]
handler => panic!("Unsupported platform: {:?}.", handler),
};
shared_context =
unsafe { gst_gl::GLContext::new_wrapped(&gl_display, gl_context, platform, api) }
.unwrap();
shared_context
.activate(true)
.expect("Couldn't activate wrapped GL context");
shared_context.fill_info()?;
let gl_context = shared_context.clone();
let event_proxy = sync::Mutex::new(event_loop.create_proxy());
#[allow(clippy::single_match)]
bus.set_sync_handler(move |_, msg| {
match msg.view() {
gst::MessageView::NeedContext(ctxt) => {
let context_type = ctxt.context_type();
if context_type == *gst_gl::GL_DISPLAY_CONTEXT_TYPE {
if let Some(el) =
msg.src().map(|s| s.downcast::<gst::Element>().unwrap())
{
let context = gst::Context::new(context_type, true);
context.set_gl_display(&gl_display);
el.set_context(&context);
}
}
if context_type == "gst.gl.app_context" {
if let Some(el) =
msg.src().map(|s| s.downcast::<gst::Element>().unwrap())
{
let mut context = gst::Context::new(context_type, true);
{
let context = context.get_mut().unwrap();
let s = context.structure_mut();
s.set("context", &gl_context);
}
el.set_context(&context);
}
el.set_context(&context);
}
}
_ => (),
}
_ => (),
}
if let Err(e) = event_proxy.send_event(Message::BusEvent) {
eprintln!("Failed to send BusEvent to event proxy: {e}")
}
if let Err(e) = event_proxy.lock().unwrap().send_event(Message::BusEvent) {
eprintln!("Failed to send BusEvent to event proxy: {}", e)
}
gst::BusSyncReply::Pass
});
gst::BusSyncReply::Pass
});
} else {
panic!("This example only has Linux support");
}
Ok(App {
pipeline,
appsink,
glupload,
bus,
event_loop,
window,
not_current_gl_context: Some(not_current_gl_context),
windowed_context,
shared_context,
})
}
fn setup(&self, event_loop: &winit::event_loop::EventLoop<Message>) -> Result<()> {
fn setup(&self, event_loop: &glutin::event_loop::EventLoop<Message>) -> Result<(), Error> {
let event_proxy = event_loop.create_proxy();
self.appsink.set_callbacks(
gst_app::AppSinkCallbacks::builder()
.new_sample(move |appsink| {
let sample = appsink.pull_sample().map_err(|_| gst::FlowError::Eos)?;
let info = sample
.caps()
.and_then(|caps| gst_video::VideoInfo::from_caps(caps).ok())
.ok_or_else(|| {
{
let _buffer = sample.buffer().ok_or_else(|| {
element_error!(
appsink,
gst::ResourceError::Failed,
("Failed to get video info from sample")
("Failed to get buffer from appsink")
);
gst::FlowError::NotNegotiated
gst::FlowError::Error
})?;
let mut buffer = sample.buffer_owned().unwrap();
{
let context = match (buffer.n_memory() > 0)
.then(|| buffer.peek_memory(0))
.and_then(|m| m.downcast_memory_ref::<gst_gl::GLBaseMemory>())
.map(|m| m.context())
{
Some(context) => context.clone(),
None => {
let _info = sample
.caps()
.and_then(|caps| gst_video::VideoInfo::from_caps(caps).ok())
.ok_or_else(|| {
element_error!(
appsink,
gst::ResourceError::Failed,
("Failed to get GL context from buffer")
("Failed to get video info from sample")
);
return Err(gst::FlowError::Error);
}
};
if let Some(meta) = buffer.meta::<gst_gl::GLSyncMeta>() {
meta.set_sync_point(&context);
} else {
let buffer = buffer.make_mut();
let meta = gst_gl::GLSyncMeta::add(buffer, &context);
meta.set_sync_point(&context);
}
gst::FlowError::Error
})?;
}
event_proxy
.send_event(Message::Frame(info, buffer))
.send_event(Message::Sample(sample))
.map(|()| gst::FlowSuccess::Ok)
.map_err(|e| {
element_error!(
@ -587,52 +517,47 @@ impl App {
.build(),
);
self.pipeline.set_state(gst::State::Playing)?;
Ok(())
}
/// Converts from <https://docs.rs/glutin/latest/glutin/config/struct.Api.html> to
/// <https://gstreamer.freedesktop.org/documentation/gl/gstglapi.html?gi-language=c#GstGLAPI>.
fn map_gl_api(api: glutin::config::Api) -> gst_gl::GLAPI {
use glutin::config::Api;
use gst_gl::GLAPI;
let mut gst_gl_api = GLAPI::empty();
// In gstreamer:
// GLAPI::OPENGL: Desktop OpenGL up to and including 3.1. The compatibility profile when the OpenGL version is >= 3.2
// GLAPI::OPENGL3: Desktop OpenGL >= 3.2 core profile
// In glutin, API::OPENGL is set for every context API, except EGL where it is set based on
// EGL_RENDERABLE_TYPE containing EGL_OPENGL_BIT:
// https://registry.khronos.org/EGL/sdk/docs/man/html/eglChooseConfig.xhtml
gst_gl_api.set(GLAPI::OPENGL | GLAPI::OPENGL3, api.contains(Api::OPENGL));
gst_gl_api.set(GLAPI::GLES1, api.contains(Api::GLES1));
// OpenGL ES 2.x and 3.x
gst_gl_api.set(GLAPI::GLES2, api.intersects(Api::GLES2 | Api::GLES3));
gst_gl_api
fn map_gl_api(api: glutin::Api) -> gst_gl::GLAPI {
match api {
glutin::Api::OpenGl => gst_gl::GLAPI::OPENGL3,
glutin::Api::OpenGlEs => gst_gl::GLAPI::GLES2,
_ => gst_gl::GLAPI::empty(),
}
}
fn create_pipeline(
gl_element: Option<&gst::Element>,
) -> Result<(gst::Pipeline, gst_app::AppSink)> {
let pipeline = gst::Pipeline::default();
let src = gst::ElementFactory::make("videotestsrc").build()?;
) -> Result<(gst::Pipeline, gst_app::AppSink, gst::Element), Error> {
let pipeline = gst::Pipeline::new(None);
let src = gst::ElementFactory::make("videotestsrc", None)
.map_err(|_| MissingElement("videotestsrc"))?;
let caps = gst_video::VideoCapsBuilder::new()
.features([gst_gl::CAPS_FEATURE_MEMORY_GL_MEMORY])
.format(gst_video::VideoFormat::Rgba)
.field("texture-target", "2D")
.build();
let appsink = gst::ElementFactory::make("appsink", None)
.map_err(|_| MissingElement("appsink"))?
.dynamic_cast::<gst_app::AppSink>()
.expect("Sink element is expected to be an appsink!");
let appsink = gst_app::AppSink::builder()
.enable_last_sample(true)
.max_buffers(1)
.caps(&caps)
appsink.set_property("enable-last-sample", &false)?;
appsink.set_property("emit-signals", &false)?;
appsink.set_property("max-buffers", &1u32)?;
let caps = gst::Caps::builder("video/x-raw")
.features(&[&gst_gl::CAPS_FEATURE_MEMORY_GL_MEMORY])
.field("format", &gst_video::VideoFormat::Rgba.to_str())
.field("texture-target", &"2D")
.build();
appsink.set_caps(Some(&caps));
if let Some(gl_element) = gl_element {
let glupload = gst::ElementFactory::make("glupload").build()?;
let glupload = gst::ElementFactory::make("glupload", None)
.map_err(|_| MissingElement("glupload"))?;
pipeline.add_many([&src, &glupload])?;
pipeline.add_many(&[&src, &glupload])?;
pipeline.add(gl_element)?;
pipeline.add(&appsink)?;
@ -640,20 +565,35 @@ impl App {
glupload.link(gl_element)?;
gl_element.link(&appsink)?;
Ok((pipeline, appsink))
Ok((pipeline, appsink, glupload))
} else {
let sink = gst::ElementFactory::make("glsinkbin")
.property("sink", &appsink)
.build()?;
let sink = gst::ElementFactory::make("glsinkbin", None)
.map_err(|_| MissingElement("glsinkbin"))?;
pipeline.add_many([&src, &sink])?;
sink.set_property("sink", &appsink)?;
pipeline.add_many(&[&src, &sink])?;
src.link(&sink)?;
Ok((pipeline, appsink))
// get the glupload element to extract later the used context in it
let mut iter = sink.dynamic_cast::<gst::Bin>().unwrap().iterate_elements();
let glupload = loop {
match iter.next() {
Ok(Some(element)) => {
if "glupload" == element.factory().unwrap().name() {
break Some(element);
}
}
Err(gst::IteratorError::Resync) => iter.resync(),
_ => break None,
}
};
Ok((pipeline, appsink, glupload.unwrap()))
}
}
fn handle_messages(bus: &gst::Bus) -> Result<()> {
fn handle_messages(bus: &gst::Bus) -> Result<(), Error> {
use gst::MessageView;
for msg in bus.iter() {
@ -663,10 +603,11 @@ impl App {
return Err(ErrorMessage {
src: msg
.src()
.map(|s| s.path_string())
.unwrap_or_else(|| glib::GString::from("UNKNOWN")),
error: err.error(),
.map(|s| String::from(s.path_string()))
.unwrap_or_else(|| String::from("None")),
error: err.error().to_string(),
debug: err.debug(),
source: err.error(),
}
.into());
}
@ -678,143 +619,98 @@ impl App {
}
}
pub(crate) fn main_loop(app: App) -> Result<()> {
pub(crate) fn main_loop(app: App) -> Result<(), Error> {
app.setup(&app.event_loop)?;
println!(
"Pixel format of the window's GL context {:?}",
app.windowed_context.get_pixel_format()
);
let gl = load(&app.windowed_context);
let mut curr_frame: Option<gst_video::VideoFrame<gst_video::video_frame::Readable>> = None;
let mut gst_gl_context: Option<gst_gl::GLContext> = None;
let App {
pipeline,
bus,
event_loop,
mut window,
mut not_current_gl_context,
glupload,
pipeline,
shared_context,
windowed_context,
..
} = app;
let mut curr_frame: Option<gst_gl::GLVideoFrame<gst_gl::gl_video_frame::Readable>> = None;
let mut running_state = None::<(
Gl,
glutin::context::PossiblyCurrentContext,
glutin::surface::Surface<glutin::surface::WindowSurface>,
)>;
Ok(event_loop.run(move |event, window_target| {
window_target.set_control_flow(winit::event_loop::ControlFlow::Wait);
event_loop.run(move |event, _, cf| {
*cf = glutin::event_loop::ControlFlow::Wait;
let mut needs_redraw = false;
match event {
winit::event::Event::LoopExiting => {
glutin::event::Event::LoopDestroyed => {
pipeline.send_event(gst::event::Eos::new());
pipeline.set_state(gst::State::Null).unwrap();
}
winit::event::Event::WindowEvent { event, .. } => match event {
winit::event::WindowEvent::CloseRequested
| winit::event::WindowEvent::KeyboardInput {
event:
winit::event::KeyEvent {
state: winit::event::ElementState::Released,
logical_key:
winit::keyboard::Key::Named(winit::keyboard::NamedKey::Escape),
glutin::event::Event::WindowEvent { event, .. } => match event {
glutin::event::WindowEvent::CloseRequested
| glutin::event::WindowEvent::KeyboardInput {
input:
glutin::event::KeyboardInput {
state: glutin::event::ElementState::Released,
virtual_keycode: Some(glutin::event::VirtualKeyCode::Escape),
..
},
..
} => window_target.exit(),
winit::event::WindowEvent::Resized(size) => {
// Some platforms like EGL require resizing GL surface to update the size
// Notable platforms here are Wayland and macOS, other don't require it
// and the function is no-op, but it's wise to resize it for portability
// reasons.
if let Some((gl, gl_context, gl_surface)) = &running_state {
gl_surface.resize(
gl_context,
// XXX Ignore minimizing
NonZeroU32::new(size.width).unwrap(),
NonZeroU32::new(size.height).unwrap(),
);
gl.resize(size);
}
} => *cf = glutin::event_loop::ControlFlow::Exit,
glutin::event::WindowEvent::Resized(physical_size) => {
windowed_context.resize(physical_size);
gl.resize(physical_size);
}
winit::event::WindowEvent::RedrawRequested => needs_redraw = true,
_ => (),
},
glutin::event::Event::RedrawRequested(_) => needs_redraw = true,
// Receive a frame
winit::event::Event::UserEvent(Message::Frame(info, buffer)) => {
if let Ok(frame) = gst_gl::GLVideoFrame::from_buffer_readable(buffer, &info) {
glutin::event::Event::UserEvent(Message::Sample(sample)) => {
let buffer = sample.buffer_owned().unwrap();
let info = sample
.caps()
.and_then(|caps| gst_video::VideoInfo::from_caps(caps).ok())
.unwrap();
{
if gst_gl_context.is_none() {
gst_gl_context = glupload
.property("context")
.unwrap()
.get::<Option<gst_gl::GLContext>>()
.unwrap();
}
let sync_meta = buffer.meta::<gst_gl::GLSyncMeta>().unwrap();
sync_meta.set_sync_point(gst_gl_context.as_ref().unwrap());
}
if let Ok(frame) = gst_video::VideoFrame::from_buffer_readable_gl(buffer, &info) {
curr_frame = Some(frame);
needs_redraw = true;
}
}
// Handle all pending messages when we are awaken by set_sync_handler
winit::event::Event::UserEvent(Message::BusEvent) => {
glutin::event::Event::UserEvent(Message::BusEvent) => {
App::handle_messages(&bus).unwrap();
}
winit::event::Event::Resumed => {
let not_current_gl_context = not_current_gl_context
.take()
.expect("There must be a NotCurrentContext prior to Event::Resumed");
let gl_config = not_current_gl_context.config();
let gl_display = gl_config.display();
let window = window.get_or_insert_with(|| {
let window_builder = winit::window::WindowBuilder::new().with_transparent(true);
glutin_winit::finalize_window(window_target, window_builder, &gl_config)
.unwrap()
});
let attrs = window.build_surface_attributes(<_>::default());
let gl_surface = unsafe {
gl_config
.display()
.create_window_surface(&gl_config, &attrs)
.unwrap()
};
// Make it current.
let gl_context = not_current_gl_context.make_current(&gl_surface).unwrap();
// Tell GStreamer that the context has been made current (for borrowed contexts,
// this does not try to make it current again)
shared_context.activate(true).unwrap();
shared_context
.fill_info()
.expect("Couldn't fill context info");
// The context needs to be current for the Renderer to set up shaders and buffers.
// It also performs function loading, which needs a current context on WGL.
let gl = load(&gl_display);
// Try setting vsync.
if let Err(res) = gl_surface.set_swap_interval(
&gl_context,
glutin::surface::SwapInterval::Wait(std::num::NonZeroU32::new(1).unwrap()),
) {
eprintln!("Error setting vsync: {res:?}");
}
pipeline.set_state(gst::State::Playing).unwrap();
assert!(running_state
.replace((gl, gl_context, gl_surface))
.is_none());
}
_ => (),
}
if needs_redraw {
if let Some((gl, gl_context, gl_surface)) = &running_state {
if let Some(frame) = curr_frame.as_ref() {
let sync_meta = frame.buffer().meta::<gst_gl::GLSyncMeta>().unwrap();
sync_meta.wait(&shared_context);
if let Ok(texture) = frame.texture_id(0) {
gl.draw_frame(texture as gl::types::GLuint);
}
if let Some(frame) = curr_frame.as_ref() {
let sync_meta = frame.buffer().meta::<gst_gl::GLSyncMeta>().unwrap();
sync_meta.wait(&shared_context);
if let Some(texture) = frame.texture_id(0) {
gl.draw_frame(texture as gl::types::GLuint);
}
gl_surface.swap_buffers(gl_context).unwrap();
}
windowed_context.swap_buffers().unwrap();
}
})?)
})
}

2
gir

@ -1 +1 @@
Subproject commit 5223ce91b97a833b09d6cbd04bbeab1bf18112b7
Subproject commit 1bef39f6176e84feb082d536ce4ce6e99d85a418

@ -1 +1 @@
Subproject commit 6cd7b656acd61172ab7f125a7059e4d0ecfc9637
Subproject commit 7d95377690e9c3b75ee4d55483a4406f60e23929

@ -1 +1 @@
Subproject commit c988e03b5e99349efb8ffb6f502879ad4ddbc248
Subproject commit 831b4449f0d5d1d30166f40cdf8409c9f395f34d

View file

@ -1 +0,0 @@
../gstreamer/CHANGELOG.md

View file

@ -1 +0,0 @@
../COPYRIGHT

View file

@ -1,38 +0,0 @@
[package]
name = "gstreamer-allocators"
authors = ["Sebastian Dröge <sebastian@centricular.com>"]
description = "Rust bindings for GStreamer Allocators library"
license = "MIT OR Apache-2.0"
readme = "README.md"
documentation = "https://gstreamer.pages.freedesktop.org/gstreamer-rs/stable/latest/docs/gstreamer_allocators/"
keywords = ["gstreamer", "multimedia", "audio", "video", "gnome"]
version.workspace = true
categories.workspace = true
repository.workspace = true
homepage.workspace = true
edition.workspace = true
rust-version.workspace = true
[dependencies]
libc = "0.2"
ffi = { package = "gstreamer-allocators-sys", path = "sys" }
glib.workspace = true
gst.workspace = true
once_cell = "1"
[dev-dependencies]
gir-format-check = "0.1"
[features]
default = []
v1_16 = ["gst/v1_16", "ffi/v1_16"]
v1_18 = ["gst/v1_18", "ffi/v1_18", "v1_16"]
v1_20 = ["gst/v1_20", "ffi/v1_20", "v1_18"]
v1_22 = ["gst/v1_22", "ffi/v1_22", "v1_20"]
v1_24 = ["gst/v1_24", "ffi/v1_24", "v1_22"]
v1_26 = ["gst/v1_26", "ffi/v1_26", "v1_24"]
[package.metadata.docs.rs]
all-features = true
rustc-args = ["--cfg", "docsrs"]
rustdoc-args = ["--cfg", "docsrs", "--generate-link-to-definition"]

View file

@ -1,108 +0,0 @@
[options]
concurrency = "send+sync"
generate_display_trait = false
generate_safety_asserts = true
girs_directories = ["../gir-files", "../gst-gir-files"]
library = "GstAllocators"
min_cfg_version = "1.14"
single_version_file = true
trust_return_value_nullability = true
version = "1.0"
work_mode = "normal"
external_libraries = [
"GLib",
"GObject",
"Gst",
]
generate = [
"GstAllocators.FdMemoryFlags",
"GstAllocators.PhysMemoryAllocator",
]
manual = [
"Gst.Allocator",
"Gst.Memory",
]
[[object]]
name = "Gst.Buffer"
ref_mode = "ref"
status = "manual"
[[object]]
name = "GstAllocators.*"
status = "generate"
[[object.function]]
name = "dmabuf_memory_get_fd"
manual = true
[[object.function]]
name = "fd_memory_get_fd"
manual = true
[[object.function]]
name = "is_dmabuf_memory"
manual = true
[[object.function]]
name = "is_fd_memory"
manual = true
[[object.function]]
name = "is_phys_memory"
manual = true
[[object.function]]
name = "phys_memory_get_phys_addr"
manual = true
[[object.function]]
name = "is_drm_dumb_memory"
manual = true
[[object.function]]
name = "drm_dumb_memory_get_handle"
manual = true
[[object.function]]
name = "drm_dumb_memory_export_dmabuf"
manual = true
[[object]]
name = "GstAllocators.DmaBufAllocator"
status = "generate"
cfg_condition = "target_os = \"linux\""
[[object.function]]
name = "alloc"
manual = true
[[object.function]]
name = "alloc_with_flags"
manual = true
[[object]]
name = "GstAllocators.DRMDumbAllocator"
status = "generate"
cfg_condition = "target_os = \"linux\""
[[object.function]]
name = "alloc"
manual = true
[[object.function]]
name = "new_with_fd"
manual = true
[[object.function]]
name = "new_with_device_path"
[object.function.return]
nullable_return_is_error = "Failed to create allocator"
[[object]]
name = "GstAllocators.FdAllocator"
status = "generate"
[[object.function]]
name = "alloc"
manual = true
[[object]]
name = "GstAllocators.ShmAllocator"
status = "generate"
cfg_condition = "unix"
[[object.function]]
name = "get"
manual = true
[[object.function]]
name = "init_once"
manual = true

View file

@ -1 +0,0 @@
../LICENSE-APACHE

View file

@ -1 +0,0 @@
../LICENSE-MIT

View file

@ -1,214 +0,0 @@
# gstreamer-rs [![crates.io](https://img.shields.io/crates/v/gstreamer-allocators.svg)](https://crates.io/crates/gstreamer-allocators) [![pipeline status](https://gitlab.freedesktop.org/gstreamer/gstreamer-rs/badges/main/pipeline.svg)](https://gitlab.freedesktop.org/gstreamer/gstreamer-rs/commits/main)
[GStreamer](https://gstreamer.freedesktop.org/) (Allocators library) bindings for Rust.
Documentation can be found [here](https://gstreamer.pages.freedesktop.org/gstreamer-rs/stable/latest/docs/gstreamer_allocators/).
These bindings are providing a safe API that can be used to interface with
GStreamer, e.g. for writing GStreamer-based applications and GStreamer plugins.
The bindings are mostly autogenerated with [gir](https://github.com/gtk-rs/gir/)
based on the [GObject-Introspection](https://wiki.gnome.org/Projects/GObjectIntrospection/)
API metadata provided by the GStreamer project.
## Table of Contents
1. [Installation](#installation)
1. [Linux/BSDs](#installation-linux)
1. [macOS](#installation-macos)
1. [Windows](#installation-windows)
1. [Getting Started](#getting-started)
1. [License](#license)
1. [Contribution](#contribution)
<a name="installation"/>
## Installation
To build the GStreamer bindings or anything depending on them, you need to
have at least GStreamer 1.14 and gst-plugins-base 1.14 installed. In addition,
some of the examples/tutorials require various GStreamer plugins to be
available, which can be found in gst-plugins-base, gst-plugins-good,
gst-plugins-bad, gst-plugins-ugly and/or gst-libav.
<a name="installation-linux"/>
### Linux/BSDs
You need to install the above mentioned packages with your distributions
package manager, or build them from source.
On Debian/Ubuntu they can be installed with
```console
$ apt-get install libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \
gstreamer1.0-plugins-base gstreamer1.0-plugins-good \
gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly \
gstreamer1.0-libav libgstrtspserver-1.0-dev libges-1.0-dev
```
The minimum required version of the above libraries is >= 1.14. If you
build the gstreamer-player sub-crate, or any of the examples that
depend on gstreamer-player, you must ensure that in addition to the above
packages, `libgstreamer-plugins-bad1.0-dev` is installed. See the `Cargo.toml`
files for the full details,
```console
$ apt-get install libgstreamer-plugins-bad1.0-dev
```
Package names on other distributions should be similar.
Please submit a pull request with instructions for yours.
<a name="installation-macos"/>
### macOS
You can install GStreamer and the plugins via [Homebrew](https://brew.sh/) or
by installing the [binaries](https://gstreamer.freedesktop.org/data/pkg/osx/)
provided by the GStreamer project.
We recommend using the official GStreamer binaries over Homebrew, especially
as GStreamer in Homebrew is [currently broken](https://github.com/orgs/Homebrew/discussions/3740#discussioncomment-3804964).
#### GStreamer Binaries
You need to download the *two* `.pkg` files from the GStreamer website and
install them, e.g. `gstreamer-1.0-1.20.4-universal.pkg` and
`gstreamer-1.0-devel-1.20.4-universal.pkg`.
After installation, you also need to set the `PATH` environment variable as
follows
```console
$ export PATH="/Library/Frameworks/GStreamer.framework/Versions/1.0/bin${PATH:+:$PATH}"
```
Also note that the `pkg-config` from GStreamer should be the first one in
the `PATH` as other versions have all kinds of quirks that will cause
problems.
#### Homebrew
Homebrew only installs various plugins if explicitly enabled, so some extra
`--with-*` flags may be required.
```console
$ brew install gstreamer gst-plugins-base gst-plugins-good \
gst-plugins-bad gst-plugins-ugly gst-libav gst-rtsp-server \
gst-editing-services --with-orc --with-libogg --with-opus \
--with-pango --with-theora --with-libvorbis --with-libvpx \
--enable-gtk3
```
Make sure the version of these libraries is >= 1.14.
<a name="installation-windows"/>
### Windows
You can install GStreamer and the plugins via [MSYS2](http://www.msys2.org/)
with `pacman` or by installing the
[binaries](https://gstreamer.freedesktop.org/data/pkg/windows/) provided by
the GStreamer project.
We recommend using the official GStreamer binaries over MSYS2.
#### GStreamer Binaries
You need to download the *two* `.msi` files for your platform from the
GStreamer website and install them, e.g. `gstreamer-1.0-x86_64-1.20.4.msi` and
`gstreamer-1.0-devel-x86_64-1.20.4.msi`. Make sure to select the version that
matches your Rust toolchain, i.e. MinGW or MSVC.
After installation set the ``PATH` environment variable as follows:
```console
# For a UNIX-style shell:
$ export PATH="c:/gstreamer/1.0/msvc_x86_64/bin${PATH:+:$PATH}"
# For cmd.exe:
$ set PATH=C:\gstreamer\1.0\msvc_x86_64\bin;%PATH%
```
Make sure to update the path to where you have actually installed GStreamer
and for the corresponding toolchain.
Also note that the `pkg-config.exe` from GStreamer should be the first one in
the `PATH` as other versions have all kinds of quirks that will cause
problems.
#### MSYS2 / pacman
```console
$ pacman -S glib2-devel pkg-config \
mingw-w64-x86_64-gstreamer mingw-w64-x86_64-gst-plugins-base \
mingw-w64-x86_64-gst-plugins-good mingw-w64-x86_64-gst-plugins-bad \
mingw-w64-x86_64-gst-plugins-ugly mingw-w64-x86_64-gst-libav \
mingw-w64-x86_64-gst-rtsp-server
```
Make sure the version of these libraries is >= 1.14.
Note that the version of `pkg-config` included in `MSYS2` is
[known to have problems](https://github.com/rust-lang/pkg-config-rs/issues/51#issuecomment-346300858)
compiling GStreamer, so you may need to install another version. One option
would be [`pkg-config-lite`](https://sourceforge.net/projects/pkgconfiglite/).
<a name="getting-started"/>
## Getting Started
The API reference can be found
[here](https://gstreamer.pages.freedesktop.org/gstreamer-rs/stable/latest/docs/gstreamer/), however it is
only the Rust API reference and does not explain any of the concepts.
For getting started with GStreamer development, the best would be to follow
the [documentation](https://gstreamer.freedesktop.org/documentation/) on the
GStreamer website, especially the [Application Development
Manual](https://gstreamer.freedesktop.org/documentation/application-development/).
While being C-centric, it explains all the fundamental concepts of GStreamer
and the code examples should be relatively easily translatable to Rust. The
API is basically the same, function/struct names are the same and everything
is only more convenient (hopefully) and safer.
In addition there are
[tutorials](https://gstreamer.freedesktop.org/documentation/tutorials/) on the
GStreamer website. Many of them were ported to Rust already and the code can
be found in the
[tutorials](https://gitlab.freedesktop.org/gstreamer/gstreamer-rs/tree/main/tutorials)
directory.
Some further examples for various aspects of GStreamer and how to use it from
Rust can be found in the
[examples](https://gitlab.freedesktop.org/gstreamer/gstreamer-rs/tree/main/examples)
directory.
Various GStreamer plugins written in Rust can be found in the
[gst-plugins-rs](https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs)
repository.
<a name="license"/>
## LICENSE
gstreamer-rs and all crates contained in here are licensed under either of
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
http://www.apache.org/licenses/LICENSE-2.0)
* MIT license ([LICENSE-MIT](LICENSE-MIT) or
http://opensource.org/licenses/MIT)
at your option.
GStreamer itself is licensed under the Lesser General Public License version
2.1 or (at your option) any later version:
https://www.gnu.org/licenses/lgpl-2.1.html
<a name="contribution"/>
## Contribution
Any kinds of contributions are welcome as a pull request.
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in gstreamer-rs by you, as defined in the Apache-2.0 license, shall be
dual licensed as above, without any additional terms or conditions.

View file

@ -1,21 +0,0 @@
// This file was generated by gir (https://github.com/gtk-rs/gir)
// from gir-files (https://github.com/gtk-rs/gir-files)
// from gst-gir-files (https://gitlab.freedesktop.org/gstreamer/gir-files-rs.git)
// DO NOT EDIT
use glib::GStr;
#[doc(alias = "GST_ALLOCATOR_DMABUF")]
pub static ALLOCATOR_DMABUF: &GStr =
unsafe { GStr::from_utf8_with_nul_unchecked(ffi::GST_ALLOCATOR_DMABUF) };
#[doc(alias = "GST_ALLOCATOR_FD")]
pub static ALLOCATOR_FD: &GStr =
unsafe { GStr::from_utf8_with_nul_unchecked(ffi::GST_ALLOCATOR_FD) };
#[cfg(feature = "v1_24")]
#[cfg_attr(docsrs, doc(cfg(feature = "v1_24")))]
#[doc(alias = "GST_ALLOCATOR_SHM")]
pub static ALLOCATOR_SHM: &GStr =
unsafe { GStr::from_utf8_with_nul_unchecked(ffi::GST_ALLOCATOR_SHM) };
#[doc(alias = "GST_CAPS_FEATURE_MEMORY_DMABUF")]
pub static CAPS_FEATURE_MEMORY_DMABUF: &GStr =
unsafe { GStr::from_utf8_with_nul_unchecked(ffi::GST_CAPS_FEATURE_MEMORY_DMABUF) };

View file

@ -1,35 +0,0 @@
// This file was generated by gir (https://github.com/gtk-rs/gir)
// from gir-files (https://github.com/gtk-rs/gir-files)
// from gst-gir-files (https://gitlab.freedesktop.org/gstreamer/gir-files-rs.git)
// DO NOT EDIT
use crate::FdAllocator;
use glib::{prelude::*, translate::*};
glib::wrapper! {
#[doc(alias = "GstDmaBufAllocator")]
pub struct DmaBufAllocator(Object<ffi::GstDmaBufAllocator, ffi::GstDmaBufAllocatorClass>) @extends FdAllocator, gst::Allocator;
match fn {
type_ => || ffi::gst_dmabuf_allocator_get_type(),
}
}
impl DmaBufAllocator {
pub const NONE: Option<&'static DmaBufAllocator> = None;
#[doc(alias = "gst_dmabuf_allocator_new")]
pub fn new() -> DmaBufAllocator {
assert_initialized_main_thread!();
unsafe { gst::Allocator::from_glib_full(ffi::gst_dmabuf_allocator_new()).unsafe_cast() }
}
}
impl Default for DmaBufAllocator {
fn default() -> Self {
Self::new()
}
}
unsafe impl Send for DmaBufAllocator {}
unsafe impl Sync for DmaBufAllocator {}

View file

@ -1,60 +0,0 @@
// This file was generated by gir (https://github.com/gtk-rs/gir)
// from gir-files (https://github.com/gtk-rs/gir-files)
// from gst-gir-files (https://gitlab.freedesktop.org/gstreamer/gir-files-rs.git)
// DO NOT EDIT
use glib::{prelude::*, translate::*};
glib::wrapper! {
#[doc(alias = "GstDRMDumbAllocator")]
pub struct DRMDumbAllocator(Object<ffi::GstDRMDumbAllocator, ffi::GstDRMDumbAllocatorClass>) @extends gst::Allocator;
match fn {
type_ => || ffi::gst_drm_dumb_allocator_get_type(),
}
}
impl DRMDumbAllocator {
#[doc(alias = "gst_drm_dumb_allocator_new_with_device_path")]
#[doc(alias = "new_with_device_path")]
pub fn with_device_path(
drm_device_path: impl AsRef<std::path::Path>,
) -> Result<DRMDumbAllocator, glib::BoolError> {
assert_initialized_main_thread!();
unsafe {
Option::<gst::Allocator>::from_glib_full(
ffi::gst_drm_dumb_allocator_new_with_device_path(
drm_device_path.as_ref().to_glib_none().0,
),
)
.map(|o| o.unsafe_cast())
.ok_or_else(|| glib::bool_error!("Failed to create allocator"))
}
}
#[doc(alias = "gst_drm_dumb_allocator_has_prime_export")]
pub fn has_prime_export(&self) -> bool {
unsafe {
from_glib(ffi::gst_drm_dumb_allocator_has_prime_export(
self.to_glib_none().0,
))
}
}
#[cfg(feature = "v1_24")]
#[cfg_attr(docsrs, doc(cfg(feature = "v1_24")))]
#[doc(alias = "drm-device-path")]
pub fn drm_device_path(&self) -> Option<std::path::PathBuf> {
ObjectExt::property(self, "drm-device-path")
}
#[cfg(feature = "v1_24")]
#[cfg_attr(docsrs, doc(cfg(feature = "v1_24")))]
#[doc(alias = "drm-fd")]
pub fn drm_fd(&self) -> i32 {
ObjectExt::property(self, "drm-fd")
}
}
unsafe impl Send for DRMDumbAllocator {}
unsafe impl Sync for DRMDumbAllocator {}

View file

@ -1,34 +0,0 @@
// This file was generated by gir (https://github.com/gtk-rs/gir)
// from gir-files (https://github.com/gtk-rs/gir-files)
// from gst-gir-files (https://gitlab.freedesktop.org/gstreamer/gir-files-rs.git)
// DO NOT EDIT
use glib::{prelude::*, translate::*};
glib::wrapper! {
#[doc(alias = "GstFdAllocator")]
pub struct FdAllocator(Object<ffi::GstFdAllocator, ffi::GstFdAllocatorClass>) @extends gst::Allocator;
match fn {
type_ => || ffi::gst_fd_allocator_get_type(),
}
}
impl FdAllocator {
pub const NONE: Option<&'static FdAllocator> = None;
#[doc(alias = "gst_fd_allocator_new")]
pub fn new() -> FdAllocator {
assert_initialized_main_thread!();
unsafe { gst::Allocator::from_glib_full(ffi::gst_fd_allocator_new()).unsafe_cast() }
}
}
impl Default for FdAllocator {
fn default() -> Self {
Self::new()
}
}
unsafe impl Send for FdAllocator {}
unsafe impl Sync for FdAllocator {}

View file

@ -1,40 +0,0 @@
// This file was generated by gir (https://github.com/gtk-rs/gir)
// from gir-files (https://github.com/gtk-rs/gir-files)
// from gst-gir-files (https://gitlab.freedesktop.org/gstreamer/gir-files-rs.git)
// DO NOT EDIT
use glib::{bitflags::bitflags, translate::*};
bitflags! {
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[doc(alias = "GstFdMemoryFlags")]
pub struct FdMemoryFlags: u32 {
#[doc(alias = "GST_FD_MEMORY_FLAG_NONE")]
const NONE = ffi::GST_FD_MEMORY_FLAG_NONE as _;
#[doc(alias = "GST_FD_MEMORY_FLAG_KEEP_MAPPED")]
const KEEP_MAPPED = ffi::GST_FD_MEMORY_FLAG_KEEP_MAPPED as _;
#[doc(alias = "GST_FD_MEMORY_FLAG_MAP_PRIVATE")]
const MAP_PRIVATE = ffi::GST_FD_MEMORY_FLAG_MAP_PRIVATE as _;
#[doc(alias = "GST_FD_MEMORY_FLAG_DONT_CLOSE")]
const DONT_CLOSE = ffi::GST_FD_MEMORY_FLAG_DONT_CLOSE as _;
}
}
#[doc(hidden)]
impl IntoGlib for FdMemoryFlags {
type GlibType = ffi::GstFdMemoryFlags;
#[inline]
fn into_glib(self) -> ffi::GstFdMemoryFlags {
self.bits()
}
}
#[doc(hidden)]
impl FromGlib<ffi::GstFdMemoryFlags> for FdMemoryFlags {
#[inline]
unsafe fn from_glib(value: ffi::GstFdMemoryFlags) -> Self {
skip_assert_initialized!();
Self::from_bits_truncate(value)
}
}

View file

@ -1,6 +0,0 @@
// This file was generated by gir (https://github.com/gtk-rs/gir)
// from gir-files (https://github.com/gtk-rs/gir-files)
// from gst-gir-files (https://gitlab.freedesktop.org/gstreamer/gir-files-rs.git)
// DO NOT EDIT
use glib::translate::*;

View file

@ -1,56 +0,0 @@
// This file was generated by gir (https://github.com/gtk-rs/gir)
// from gir-files (https://github.com/gtk-rs/gir-files)
// from gst-gir-files (https://gitlab.freedesktop.org/gstreamer/gir-files-rs.git)
// DO NOT EDIT
#[cfg(target_os = "linux")]
#[cfg_attr(docsrs, doc(cfg(target_os = "linux")))]
#[cfg(feature = "v1_24")]
#[cfg_attr(docsrs, doc(cfg(feature = "v1_24")))]
mod drm_dumb_allocator;
#[cfg(target_os = "linux")]
#[cfg_attr(docsrs, doc(cfg(target_os = "linux")))]
#[cfg(feature = "v1_24")]
#[cfg_attr(docsrs, doc(cfg(feature = "v1_24")))]
pub use self::drm_dumb_allocator::DRMDumbAllocator;
#[cfg(target_os = "linux")]
#[cfg_attr(docsrs, doc(cfg(target_os = "linux")))]
mod dma_buf_allocator;
#[cfg(target_os = "linux")]
#[cfg_attr(docsrs, doc(cfg(target_os = "linux")))]
pub use self::dma_buf_allocator::DmaBufAllocator;
mod fd_allocator;
pub use self::fd_allocator::FdAllocator;
mod phys_memory_allocator;
pub use self::phys_memory_allocator::PhysMemoryAllocator;
#[cfg(unix)]
#[cfg_attr(docsrs, doc(cfg(unix)))]
#[cfg(feature = "v1_24")]
#[cfg_attr(docsrs, doc(cfg(feature = "v1_24")))]
mod shm_allocator;
#[cfg(unix)]
#[cfg_attr(docsrs, doc(cfg(unix)))]
#[cfg(feature = "v1_24")]
#[cfg_attr(docsrs, doc(cfg(feature = "v1_24")))]
pub use self::shm_allocator::ShmAllocator;
mod flags;
pub use self::flags::FdMemoryFlags;
pub(crate) mod functions;
mod constants;
pub use self::constants::ALLOCATOR_DMABUF;
pub use self::constants::ALLOCATOR_FD;
#[cfg(feature = "v1_24")]
#[cfg_attr(docsrs, doc(cfg(feature = "v1_24")))]
pub use self::constants::ALLOCATOR_SHM;
pub use self::constants::CAPS_FEATURE_MEMORY_DMABUF;
pub(crate) mod traits {
pub use super::phys_memory_allocator::PhysMemoryAllocatorExt;
}

View file

@ -1,31 +0,0 @@
// This file was generated by gir (https://github.com/gtk-rs/gir)
// from gir-files (https://github.com/gtk-rs/gir-files)
// from gst-gir-files (https://gitlab.freedesktop.org/gstreamer/gir-files-rs.git)
// DO NOT EDIT
use glib::prelude::*;
glib::wrapper! {
#[doc(alias = "GstPhysMemoryAllocator")]
pub struct PhysMemoryAllocator(Interface<ffi::GstPhysMemoryAllocator, ffi::GstPhysMemoryAllocatorInterface>) @requires gst::Allocator;
match fn {
type_ => || ffi::gst_phys_memory_allocator_get_type(),
}
}
impl PhysMemoryAllocator {
pub const NONE: Option<&'static PhysMemoryAllocator> = None;
}
unsafe impl Send for PhysMemoryAllocator {}
unsafe impl Sync for PhysMemoryAllocator {}
mod sealed {
pub trait Sealed {}
impl<T: super::IsA<super::PhysMemoryAllocator>> Sealed for T {}
}
pub trait PhysMemoryAllocatorExt: IsA<PhysMemoryAllocator> + sealed::Sealed + 'static {}
impl<O: IsA<PhysMemoryAllocator>> PhysMemoryAllocatorExt for O {}

View file

@ -1,20 +0,0 @@
// This file was generated by gir (https://github.com/gtk-rs/gir)
// from gir-files (https://github.com/gtk-rs/gir-files)
// from gst-gir-files (https://gitlab.freedesktop.org/gstreamer/gir-files-rs.git)
// DO NOT EDIT
use crate::FdAllocator;
glib::wrapper! {
#[doc(alias = "GstShmAllocator")]
pub struct ShmAllocator(Object<ffi::GstShmAllocator, ffi::GstShmAllocatorClass>) @extends FdAllocator, gst::Allocator;
match fn {
type_ => || ffi::gst_shm_allocator_get_type(),
}
}
impl ShmAllocator {}
unsafe impl Send for ShmAllocator {}
unsafe impl Sync for ShmAllocator {}

View file

@ -1,3 +0,0 @@
Generated by gir (https://github.com/gtk-rs/gir @ 5223ce91b97a)
from gir-files (https://github.com/gtk-rs/gir-files @ 6cd7b656acd6)
from gst-gir-files (https://gitlab.freedesktop.org/gstreamer/gir-files-rs.git @ c988e03b5e99)

View file

@ -1,7 +0,0 @@
// Take a look at the license at the top of the repository in the LICENSE file.
use gst::CapsFeatures;
use once_cell::sync::Lazy;
pub static CAPS_FEATURES_MEMORY_DMABUF: Lazy<CapsFeatures> =
Lazy::new(|| CapsFeatures::new([crate::CAPS_FEATURE_MEMORY_DMABUF]));

View file

@ -1,79 +0,0 @@
use std::{
fmt,
os::unix::prelude::{IntoRawFd, RawFd},
};
use glib::{prelude::*, translate::*};
use gst::{Memory, MemoryRef};
#[cfg(feature = "v1_16")]
#[cfg_attr(docsrs, doc(cfg(feature = "v1_16")))]
use crate::FdMemoryFlags;
use crate::{DmaBufAllocator, FdMemory, FdMemoryRef};
gst::memory_object_wrapper!(
DmaBufMemory,
DmaBufMemoryRef,
gst::ffi::GstMemory,
|mem: &gst::MemoryRef| { unsafe { from_glib(ffi::gst_is_dmabuf_memory(mem.as_mut_ptr())) } },
FdMemory,
FdMemoryRef,
Memory,
MemoryRef
);
impl fmt::Debug for DmaBufMemory {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
DmaBufMemoryRef::fmt(self, f)
}
}
impl fmt::Debug for DmaBufMemoryRef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
MemoryRef::fmt(self, f)
}
}
impl DmaBufMemoryRef {
#[doc(alias = "gst_dmabuf_memory_get_fd")]
pub fn fd(&self) -> RawFd {
skip_assert_initialized!();
unsafe { ffi::gst_dmabuf_memory_get_fd(self.as_mut_ptr()) }
}
}
impl DmaBufAllocator {
#[doc(alias = "gst_dmabuf_allocator_alloc")]
pub unsafe fn alloc<A: IntoRawFd>(
&self,
fd: A,
size: usize,
) -> Result<gst::Memory, glib::BoolError> {
skip_assert_initialized!();
Option::<_>::from_glib_full(ffi::gst_dmabuf_allocator_alloc(
self.unsafe_cast_ref::<gst::Allocator>().to_glib_none().0,
fd.into_raw_fd(),
size,
))
.ok_or_else(|| glib::bool_error!("Failed to allocate memory"))
}
#[cfg(feature = "v1_16")]
#[cfg_attr(docsrs, doc(cfg(feature = "v1_16")))]
#[doc(alias = "gst_dmabuf_allocator_alloc_with_flags")]
pub unsafe fn alloc_with_flags(
&self,
fd: RawFd,
size: usize,
flags: FdMemoryFlags,
) -> Result<gst::Memory, glib::BoolError> {
skip_assert_initialized!();
Option::<_>::from_glib_full(ffi::gst_dmabuf_allocator_alloc_with_flags(
self.unsafe_cast_ref::<gst::Allocator>().to_glib_none().0,
fd,
size,
flags.into_glib(),
))
.ok_or_else(|| glib::bool_error!("Failed to allocate memory"))
}
}

View file

@ -1,81 +0,0 @@
use std::{fmt, mem, os::unix::prelude::IntoRawFd};
use glib::{prelude::*, translate::*};
use gst::{Memory, MemoryRef};
use crate::{DRMDumbAllocator, DmaBufMemory};
gst::memory_object_wrapper!(
DRMDumbMemory,
DRMDumbMemoryRef,
gst::ffi::GstMemory,
|mem: &gst::MemoryRef| { unsafe { from_glib(ffi::gst_is_drm_dumb_memory(mem.as_mut_ptr())) } },
Memory,
MemoryRef
);
impl fmt::Debug for DRMDumbMemory {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
DRMDumbMemoryRef::fmt(self, f)
}
}
impl fmt::Debug for DRMDumbMemoryRef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
MemoryRef::fmt(self, f)
}
}
impl DRMDumbMemoryRef {
#[doc(alias = "gst_drm_dumb_memory_get_handle")]
pub fn fd(&self) -> u32 {
skip_assert_initialized!();
unsafe { ffi::gst_drm_dumb_memory_get_handle(self.as_mut_ptr()) }
}
#[doc(alias = "gst_drm_dumb_memory_export_dmabuf")]
pub fn export_dmabuf(&self) -> Result<DmaBufMemory, glib::BoolError> {
skip_assert_initialized!();
unsafe {
Option::<DmaBufMemory>::from_glib_full(ffi::gst_drm_dumb_memory_export_dmabuf(
self.as_mut_ptr(),
))
.ok_or_else(|| glib::bool_error!("Failed to export as dmabuf"))
}
}
}
impl DRMDumbAllocator {
#[doc(alias = "gst_drm_dumb_allocator_new_with_fd")]
#[doc(alias = "new_with_fd")]
pub fn with_fd<A: IntoRawFd>(drm_fd: A) -> Result<DRMDumbAllocator, glib::BoolError> {
assert_initialized_main_thread!();
unsafe {
Option::<gst::Allocator>::from_glib_full(ffi::gst_drm_dumb_allocator_new_with_fd(
drm_fd.into_raw_fd(),
))
.map(|o| o.unsafe_cast())
.ok_or_else(|| glib::bool_error!("Failed to create allocator"))
}
}
#[doc(alias = "gst_drm_dumb_allocator_alloc")]
pub unsafe fn alloc(
&self,
drm_fourcc: u32,
width: u32,
height: u32,
) -> Result<(gst::Memory, u32), glib::BoolError> {
skip_assert_initialized!();
let mut out_pitch = mem::MaybeUninit::uninit();
Option::<_>::from_glib_full(ffi::gst_drm_dumb_allocator_alloc(
self.to_glib_none().0,
drm_fourcc,
width,
height,
out_pitch.as_mut_ptr(),
))
.ok_or_else(|| glib::bool_error!("Failed to allocate memory"))
.map(|mem| (mem, unsafe { out_pitch.assume_init() }))
}
}

View file

@ -1,64 +0,0 @@
use std::{fmt, os::unix::prelude::RawFd};
use glib::{prelude::*, translate::*};
use gst::{Memory, MemoryRef};
use crate::{FdAllocator, FdMemoryFlags};
gst::memory_object_wrapper!(
FdMemory,
FdMemoryRef,
gst::ffi::GstMemory,
|mem: &gst::MemoryRef| { unsafe { from_glib(ffi::gst_is_fd_memory(mem.as_mut_ptr())) } },
Memory,
MemoryRef,
);
impl fmt::Debug for FdMemory {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
FdMemoryRef::fmt(self, f)
}
}
impl fmt::Debug for FdMemoryRef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("FdMemory")
.field("ptr", &self.as_ptr())
.field("allocator", &self.allocator())
.field("parent", &self.parent())
.field("maxsize", &self.maxsize())
.field("align", &self.align())
.field("offset", &self.offset())
.field("size", &self.size())
.field("flags", &self.flags())
.field("fd", &self.fd())
.finish()
}
}
impl FdMemoryRef {
#[doc(alias = "gst_fd_memory_get_fd")]
pub fn fd(&self) -> RawFd {
skip_assert_initialized!();
unsafe { ffi::gst_fd_memory_get_fd(self.as_mut_ptr()) }
}
}
impl FdAllocator {
#[doc(alias = "gst_fd_allocator_alloc")]
pub unsafe fn alloc(
&self,
fd: RawFd,
size: usize,
flags: FdMemoryFlags,
) -> Result<gst::Memory, glib::BoolError> {
skip_assert_initialized!();
Option::<_>::from_glib_full(ffi::gst_fd_allocator_alloc(
self.unsafe_cast_ref::<gst::Allocator>().to_glib_none().0,
fd,
size,
flags.into_glib(),
))
.ok_or_else(|| glib::bool_error!("Failed to allocate memory"))
}
}

View file

@ -1,63 +0,0 @@
// Take a look at the license at the top of the repository in the LICENSE file.
#![cfg_attr(docsrs, feature(doc_cfg))]
#![allow(clippy::missing_safety_doc)]
#![doc = include_str!("../README.md")]
pub use ffi;
pub use glib;
pub use gst;
macro_rules! assert_initialized_main_thread {
() => {
if !gst::INITIALIZED.load(std::sync::atomic::Ordering::SeqCst) {
gst::assert_initialized();
}
};
}
macro_rules! skip_assert_initialized {
() => {};
}
#[allow(unused_imports)]
mod auto;
pub use crate::auto::*;
mod caps_features;
pub use crate::caps_features::CAPS_FEATURES_MEMORY_DMABUF;
mod fd_allocator;
pub use fd_allocator::*;
#[cfg(any(target_os = "linux", docsrs))]
#[cfg_attr(docsrs, doc(cfg(target_os = "linux")))]
mod dma_buf_allocator;
#[cfg(any(target_os = "linux", docsrs))]
#[cfg_attr(docsrs, doc(cfg(target_os = "linux")))]
pub use dma_buf_allocator::*;
#[cfg(any(all(feature = "v1_24", target_os = "linux"), docsrs))]
#[cfg_attr(docsrs, doc(cfg(all(feature = "v1_24", target_os = "linux"))))]
mod drm_dumb_allocator;
#[cfg(any(all(feature = "v1_24", target_os = "linux"), docsrs))]
#[cfg_attr(docsrs, doc(cfg(all(feature = "v1_24", target_os = "linux"))))]
pub use drm_dumb_allocator::*;
#[cfg(any(all(feature = "v1_24", unix), docsrs))]
#[cfg_attr(docsrs, doc(cfg(all(feature = "v1_24", unix))))]
mod shm_allocator;
mod phys_memory;
pub use phys_memory::*;
// Re-export all the traits in a prelude module, so that applications
// can always "use gst_base::prelude::*" without getting conflicts
pub mod prelude {
#[doc(hidden)]
pub use gst::prelude::*;
pub use crate::auto::traits::*;
}
pub mod subclass;

View file

@ -1,43 +0,0 @@
use std::fmt;
use glib::translate::*;
use gst::{Memory, MemoryRef};
gst::memory_object_wrapper!(
PhysMemory,
PhysMemoryRef,
gst::ffi::GstMemory,
|mem: &gst::MemoryRef| { unsafe { from_glib(ffi::gst_is_phys_memory(mem.as_mut_ptr())) } },
Memory,
MemoryRef,
);
impl fmt::Debug for PhysMemory {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
PhysMemoryRef::fmt(self, f)
}
}
impl fmt::Debug for PhysMemoryRef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("FdMemory")
.field("ptr", &self.as_ptr())
.field("allocator", &self.allocator())
.field("parent", &self.parent())
.field("maxsize", &self.maxsize())
.field("align", &self.align())
.field("offset", &self.offset())
.field("size", &self.size())
.field("flags", &self.flags())
.field("phys_addr", &format!("{:x}", self.phys_addr()))
.finish()
}
}
impl PhysMemoryRef {
#[doc(alias = "gst_phys_memory_get_phys_addr")]
pub fn phys_addr(&self) -> libc::uintptr_t {
skip_assert_initialized!();
unsafe { ffi::gst_phys_memory_get_phys_addr(self.as_mut_ptr()) }
}
}

Some files were not shown because too many files have changed in this diff Show more