forked from mirrors/gstreamer-rs
Compare commits
42 commits
Author | SHA1 | Date | |
---|---|---|---|
|
6c9815f059 | ||
|
0b7b8bd60f | ||
|
ad0dbadc74 | ||
|
8deac14876 | ||
|
3b40b8e2d5 | ||
|
e009fd0a4c | ||
|
9f5c9adadf | ||
|
69eecdcc83 | ||
|
c5771f0373 | ||
|
1ae8b0d4a2 | ||
|
dd4330f899 | ||
|
562c8563ce | ||
|
e1f197c91c | ||
|
6cf9b9a3d3 | ||
|
35d2a2852b | ||
|
52b5f30b29 | ||
|
0fb2f605b6 | ||
|
7868b700ee | ||
|
6c46db6e61 | ||
|
e3ad81d473 | ||
|
566cfb879b | ||
|
5189a4d175 | ||
|
3fc45a9254 | ||
|
c1905f2f24 | ||
|
061f85d410 | ||
|
3b03ab0660 | ||
|
e535924367 | ||
|
810ff23bed | ||
|
c44fde7279 | ||
|
f33594bdd9 | ||
|
9a0be6f1a8 | ||
|
cb526abb5a | ||
|
7de8a64fc0 | ||
|
b0c6fd1543 | ||
|
b58b9d234c | ||
|
223a12597c | ||
|
72da9ea826 | ||
|
864072220b | ||
|
11d04ad7b6 | ||
|
ecf9a43692 | ||
|
2aeaf6fa7a | ||
|
e403542a28 |
1183 changed files with 48628 additions and 114176 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
|||
target/
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
||||
|
|
516
.gitlab-ci.yml
516
.gitlab-ci.yml
|
@ -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'
|
||||
|
|
|
@ -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
3
.gitmodules
vendored
|
@ -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
2823
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
102
Cargo.toml
102
Cargo.toml
|
@ -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
111
README.md
|
@ -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
|
||||
|
|
|
@ -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()
|
|
@ -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'
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
22
deny.toml
22
deny.toml
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(())
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
177
examples/src/bin/gtksink.rs
Normal 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: >k::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", >kglsink).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();
|
||||
}
|
||||
}
|
288
examples/src/bin/gtkvideooverlay.rs
Normal file
288
examples/src/bin/gtkvideooverlay.rs
Normal 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: >k::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();
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}"),
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {}
|
||||
}
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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
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
|
|
@ -1 +0,0 @@
|
|||
../gstreamer/CHANGELOG.md
|
|
@ -1 +0,0 @@
|
|||
../COPYRIGHT
|
|
@ -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"]
|
|
@ -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
|
||||
|
|
@ -1 +0,0 @@
|
|||
../LICENSE-APACHE
|
|
@ -1 +0,0 @@
|
|||
../LICENSE-MIT
|
|
@ -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.
|
|
@ -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) };
|
|
@ -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 {}
|
|
@ -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 {}
|
|
@ -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 {}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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::*;
|
|
@ -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;
|
||||
}
|
|
@ -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 {}
|
|
@ -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 {}
|
|
@ -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)
|
|
@ -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]));
|
|
@ -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"))
|
||||
}
|
||||
}
|
|
@ -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() }))
|
||||
}
|
||||
}
|
|
@ -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"))
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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
Loading…
Reference in a new issue