Compare commits

...

265 commits

Author SHA1 Message Date
Thibault Saunier 01f0c0a330 cargo.lock: Downgrade clap to support rustc 1.64
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1117>
2023-03-02 21:08:11 +02:00
Thibault Saunier 4b867d27fe Add a webrtcsrc element
Updating the docker image to include:
https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/3236

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1117>
2023-03-02 14:56:30 -03:00
Sebastian Dröge f2b03d3796 Update Cargo.lock 2023-03-02 13:26:19 +02:00
Sebastian Dröge 9a779607c7 Update versions to 0.9.10 2023-03-02 13:18:00 +02:00
Sebastian Dröge ccfa25aa60 Update Cargo.lock
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1116>
2023-03-02 11:04:47 +02:00
Sebastian Dröge a0cfe054c8 deny: Update to allow socket2 0.4
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1116>
2023-03-02 11:01:18 +02:00
Sebastian Dröge e2c7e7ebe1 deny: Update
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1116>
2023-03-02 11:01:18 +02:00
Sebastian Dröge f5feb34fcb deny: Update
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1116>
2023-03-02 11:01:18 +02:00
Vivia Nikolaidou a0fe1aba5f ndisinkcombiner: Properly handle caps changes
We are caching one video buffer, so previously we were changing the src
caps one buffer too early.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1116>
2023-03-02 11:01:18 +02:00
Sebastian Dröge fef81b1c97 threadshare: Update to socket2 0.5
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1116>
2023-03-02 11:01:18 +02:00
Thibault Saunier e4c9ba43df webrtc: Enhance debug messages when using unknown peer ID
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1116>
2023-03-02 11:01:18 +02:00
Guillaume Desmottes e14777573f tracers: queue_levels: add appsrc support
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1116>
2023-03-02 10:53:19 +02:00
Sebastian Dröge 4aacf4d3ad livesync: Correctly calculate fallback buffer duration from framerate
Numerator and denominator were switched.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1116>
2023-03-02 10:53:19 +02:00
Matthew Waters 0d3dc25414 webrtcsink: also support nvvidconv in lieu of nvvideoconvert
nvvideoconvert may not exist and nvvidconv might on some Jetson
platforms.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1116>
2023-03-02 10:53:19 +02:00
Sebastian Dröge ef7f0d12bf gtk4: Set sync point on the video frame after mapping it
Otherwise it is not always ready for use yet in GTK even after waiting
on the sync point, and a fully transparent texture is rendered instead.

Fixes https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/issues/320

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1106>
2023-02-25 12:52:46 +02:00
Sebastian Dröge 2723fc4713 gtk4: Attach channel receiver to the default main context from the main thread
It requires acquiring the main context for thread-safety reasons and
that is only possible from the main thread itself.

Fixes https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/issues/319

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1101>
2023-02-22 14:24:51 +02:00
Sebastian Dröge e100506194 gtk4: Don't unnecessarily set the sink to READY to retrieve the context
That's not needed and will cause the GL context messages to be not
distributed inside the pipeline.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1101>
2023-02-22 14:24:49 +02:00
Sebastian Dröge 5c21b10841 gtk4: Refactor and simplify GL context handling
Create a single, global GDK GL context and the corresponding GStreamer
GL display and wrapped GStreamer GL context when initializing the first
sink and continue using that for all further sinks.

Additionally, don't create a full GStreamer GL context inside the sink
but only distribute the wrapped GL context in the pipeline so that
elements that actually need a full GL context can create one that is
sharing with that one. The sink itself does not need a full GStreamer GL
context.

Then inside the sink check that any GL memory that arrives was created
by a GL context that can share with the wrapped GDK GL context and only
then use it.

And lastly, use the correct GL contexts for a) creating a sync point and
b) actually waiting on it.

Fixes https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/issues/318

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1101>
2023-02-22 14:24:21 +02:00
Seungha Yang 8f612b9003 mp4mux: Ignore framerate update
like mp4mux in -good does already

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1098>
2023-02-21 16:17:59 +02:00
Seungha Yang 365dcfa730 fmp4mux: Ignore framerate update
like mp4mux in -good does already

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1098>
2023-02-21 16:17:52 +02:00
Arun Raghavan 611c7d6cd3 hlssink3: Allow GIOStream signal handlers to return None
If creating a playlist or fragment stream fails (disk is full, the
directory is removed, ...), we will currently crash because the signal
handler expects a non-None GIOStream. The actual callback is allowed to
return None values and we handle this in the caller, so let's not have
this restriction on the signal handler.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1098>
2023-02-21 16:17:45 +02:00
Sebastian Dröge bfbde450db Add mp4 plugin to README.md
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1098>
2023-02-21 16:17:38 +02:00
Seungha Yang 562b429388 rtpav1pay: Fix Leb128Bytes size parsing
There are multiple ways of encoding the value, and don't assume
that bitstream used the way used in this plugin. Instead, count
the number of used bytes.

Fixes: https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/issues/312
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1092>
2023-02-11 19:44:51 +02:00
Sebastian Dröge 09cffe0e70 Update Cargo.lock 2023-02-09 22:08:26 +02:00
Sebastian Dröge eb3d3b3088 Update versions to 0.9.9 2023-02-09 22:08:17 +02:00
Sebastian Dröge ee46b103b8 ci: Update 0.19 gstreamer-rs CI template
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1086>
2023-02-09 21:43:57 +02:00
Sebastian Dröge 8a384aa8b2 Update Cargo.lock
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1086>
2023-02-09 21:43:57 +02:00
rajneeshksoni 01d3b0f9da awss3sink: Add properties to set content-Type and content-disposition.
for uploaded object default content-type is set to binary/octet-stream,
which is correct.
metadata cannot be used to set content-type and content-disposition as
setting metadata add a prefix x-amz-meta to key
e.g. setting metadate "content-type=video/mp4" actually set value as
x-amz-meta-content-type. So these has to be seaprate property.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1086>
2023-02-09 21:43:57 +02:00
Sebastian Dröge 58a6dbacca fmp4mux: Pass one more buffer in test_buffer_multi_stream_short_gops test
This works around non-determinism in aggregator where depending on
timing it can happen that it consumes all buffers from both pads or
waits for another buffer on one pad while the other one already has one.

The effect in this test was that it sometimes timed out. By providing
one more buffer it is guaranteed now that at this point the muxer is
beyond the end of the first fragment.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1086>
2023-02-09 21:43:57 +02:00
Sebastian Dröge e52bb160c7 fmp4mux: Accept more data on already filled streams if the remaining streams need more data for finishing a GOP
In other words, continue queueing buffers in sync from all streams until
all of them are ready for draining instead of stopping to queue buffers
on every stream that is already filled individually.

Fixes https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/issues/310

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1086>
2023-02-09 21:43:57 +02:00
Jan Alexander Steffens (heftig) 19c527a9c5 livesync: Document State's fields
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1086>
2023-02-09 21:43:57 +02:00
Jan Alexander Steffens (heftig) fbf50b395d livesync: Improve formatting
Move some code around to make it a bit more readable. No change in
behavior.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1086>
2023-02-09 21:43:57 +02:00
Jan Alexander Steffens (heftig) 9d67753fd6 livesync: Fix log message capitalization
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1086>
2023-02-09 21:43:57 +02:00
Jan Alexander Steffens (heftig) 07adcf5ecf livesync: Extract LiveSync::flow_error
And add details so it behaves more like the `GST_ELEMENT_FLOW_ERROR`
macro.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1086>
2023-02-09 21:43:57 +02:00
Jan Alexander Steffens (heftig) 35552dc73c livesync: Extract audio_info_from_caps
And adjust it slightly so it never panics.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1086>
2023-02-09 21:43:57 +02:00
Jan Alexander Steffens (heftig) 1e67259462 livesync: Move single segment prop
Keep it with the settings, not after the stats.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1086>
2023-02-09 21:43:57 +02:00
Jan Alexander Steffens (heftig) cccf90f59e livesync: Fix queueing
The logic of the element requires the next buffer to be available
immediately after we are done pushing the previous, otherwise we insert
a repeat.

Making the src loop handle events and queries broke this, as upstream is
almost guaranteed not to deliver a buffer in time if we allow non-buffer
items to block upstream's push.

To fix this, replace our single-item `Option` with a `VecDeque` that we
allow to hold an unlimited number of events or queries, but only one
buffer at a time.

In addition, the code was confused about the current caps and segment.

This wasn't an issue before making the src loop handle events and
queries, as only the sinkpad cared about the current segment, using it
to buffers received, and only the srcpad cared about the current caps,
sending it just before sending the next received buffer.

Now the sinkpad cares about caps (through `update_fallback_duration`)
and the srcpad cares about the segment (when not in single-segment
mode).

Fix this by
  - making `in_caps` always hold the current caps of the sinkpad,
  - adding `pending_caps`, which is used by the srcpad to store
    caps to be sent with the next received buffer,
  - adding `in_segment`, holding the current segment of the sinkpad,
  - adding `pending_segment`, which is used by the srcpad to store
    the segment to be sent with the next received buffer,
  - adding `out_segment`, holding the current segment of the srcpad.

Maybe a fix for
https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/issues/298.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1086>
2023-02-09 21:43:57 +02:00
Simon Himmelbauer fffecca624 spotifyaudiosrc: Support configurable bitrate
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1086>
2023-02-09 21:43:57 +02:00
rajneeshksoni f96b64e1c1 hlssink3: Allow setting i-frame-only playlist.
HLS allows manifest where all segments are single ifames.
This manifest requires `EXT-X-I-FRAMES-ONLY` tag in the
manifest.
I-FRAMES-ONLY playlist segments are video only segments.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1086>
2023-02-09 21:43:57 +02:00
Sebastian Dröge c805c3bb3a dav1ddec: Make sure to call get_picture() twice in a row when draining
The first time might return `EAGAIN` if there are pending frames but
there is no decoded frame available yet. The second time it will
actually wait for frames to become available and only start returning
`EAGAIN` again once no more frames are left.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1086>
2023-02-09 21:43:57 +02:00
Sebastian Dröge 5f70c0f5fe rtpgccbwe: Don't use clamp() if there's no clear min/max value
Fixes https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/issues/305

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1086>
2023-02-09 21:43:57 +02:00
Sebastian Dröge da7743d2e7 fmp4mux: Handle GOPs ending after the desired fragment end correctly
Either create further chunks if enough data is queued or simply start
the new fragment at a later time if the keyframe is later.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1086>
2023-02-09 21:17:51 +02:00
Sebastian Dröge dae1d8b5ef mp4/fmp4: Update docs
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1086>
2023-02-09 21:17:34 +02:00
Sebastian Dröge 7ba1100a92 mp4: Add support for AV1
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1086>
2023-02-09 21:17:23 +02:00
Sebastian Dröge a65feb7ef9 fmp4: Add support for AV1
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1086>
2023-02-09 21:16:40 +02:00
Sebastian Dröge 1029669427 fmp4: Add support for VP8
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1086>
2023-02-09 21:15:50 +02:00
Sebastian Dröge 40cada5f69 mp4: Add support for VP8
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1086>
2023-02-09 21:15:05 +02:00
Sanchayan Maity 0e55e19d57 aws/s3hlssink: Fix deadlock on EOS
In state change to NULL, we take state lock and call stop. When stop
is called, we will try to upload queued segments in S3 request thread.
That tries to take the state lock again and deadlocks.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1086>
2023-02-09 21:14:06 +02:00
Sanchayan Maity 7a8ecb5343 aws/s3hlssink: Use factory name when checking name of child element
Commit ad3f1cf fixed the name of hlssink child element to be the same
for hlssink2 and hlssink3. However, we rely on element name to return
boolean in case of hlssink3 or None in case of hlssink2 as the return
value of the delete-fragment closure.

Fix this by using the factory name instead of the element name.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1086>
2023-02-09 21:14:01 +02:00
Sebastian Dröge 17dec1cb26 rtpav1pay: Add support for tu/frame aligned input
In this case every buffer can be sent out immediately and makes up a
whole frame.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1086>
2023-02-09 21:13:40 +02:00
Sebastian Dröge ba0904630d rtpav1pay: Consider the marker flag to output packets immediately at the end of a frame
Otherwise it is necessary to wait for the beginning of the following
frame, which unnecessarily increases the latency.

Fixes https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/issues/255

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1086>
2023-02-09 21:13:33 +02:00
Sebastian Dröge af0e6281d2 rtpav1depay: Fix depayloading of packets starting with a leading OBU fragment followed by more OBUs
Fixes https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/issues/288

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1086>
2023-02-09 21:13:26 +02:00
Sebastian Dröge e79221f386 rtpav1depay: Fix error handling
Don't error out immediately on errors anymore but try again with the
next packet.

Fixes https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/issues/289

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1086>
2023-02-09 21:13:19 +02:00
Sebastian Dröge dc47b35536 rtpav1depay: Set DISCONT flag on buffers following a corrupted packet
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1086>
2023-02-09 21:13:13 +02:00
Sebastian Dröge 3520fc67de rtpav1depay: Don't output full TUs but just OBUs as they come
Simplifies state tracking and potentially reduces latency as it's not
necessary to wait until all fragments of an OBU are received.

The last OBU of a TU is marked with the marker flag to allow parsers to
detect this without first seeing the beginning of the next TU.

Also use a simple `Vec` for collecting complete OBUs instead of a
`gst_base::Adapter` as this reduces the number of allocations.

And also handle invalid packets a little bit more gracefully.

Fixes https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/issues/244

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1086>
2023-02-09 21:13:06 +02:00
Jan Alexander Steffens (heftig) 402d96b80c livesync: Only resend segment if not in single-segment mode
In single-segment mode, the outgoing segment does not change when the
incoming segment changes. We only need to resend the segment if we got
flushed or deactivated.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1086>
2023-02-09 21:07:06 +02:00
Sebastian Dröge 68bec4a0db fmp4mux: Fix a couple of assertions by handling these cases cleaner
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1086>
2023-02-09 21:06:56 +02:00
Sebastian Dröge adbb8b6495 fmp4mux: Refactor and clean up code
Split many longer functions into multiple functions and simplify various
parts. Overall this is functionally still the same as before.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1086>
2023-02-09 21:06:56 +02:00
Sebastian Dröge a01437b675 fmp4mux: Add support for sub-fragments / chunking
Allow outputting sub-fragments (chunks in CMAF terms) that are shorter
than the fragment duration and don't usually start on a keyframe. By
this the buffering requirements of the element is reduced to one chunk
duration, as is the latency.

This is used for formats like low-latency / LL-HLS and DASH.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1086>
2023-02-09 21:06:56 +02:00
Guillaume Desmottes b9e203d6c1 fmp4mux: add 'offset-to-zero' property
Add it only to 'isofmp4mux', the onvif variant already does this and
CMAF and DASH are always single-stream so you rely on inter-container
synchronization via the running-time.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1086>
2023-02-09 21:06:08 +02:00
Arun Raghavan f3b8288ef9 aws: s3hlssink: Fix the name of the hlssink child element
It's easier to set child element properties if the name doesn't depend
on the factory.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1086>
2023-02-09 21:05:58 +02:00
Sebastian Dröge 4eeb8ffb63 fmp4mux: Don't write the first sample flags into any trun but the first
The flags are based on the first sample of this fragment so writing it
into any trun but the first is not very useful.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1086>
2023-02-09 21:05:48 +02:00
Sebastian Dröge 01aa9380d4 fmp4mux: Fix decision whether per-sample flags are needed in the trun
Previously it would never use per-sample flags if any later sample
needed different flags than the first two.

Also comment the code a bit better.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1086>
2023-02-09 21:05:40 +02:00
Sebastian Dröge d9e9468f9a meson: Update version to 0.9.8 2023-01-24 15:46:09 +02:00
Sebastian Dröge 59f575888e Downgrade clap dependency to 4.0
4.1 does not build with Rust 1.63 anymore.
2023-01-23 11:33:20 +02:00
Sebastian Dröge 74a40060ce Update Cargo.lock 2023-01-23 11:30:40 +02:00
Sebastian Dröge 5c2582d105 Update version to 0.9.8 2023-01-23 11:30:27 +02:00
Sebastian Dröge 4b9392938f dav1d: Don't treat any kind of bitstream error immediately as fatal
Instead use the videodecoder error handling to allow up to max-errors
consecutive decoding errors, i.e. infinite by default in 1.22 and newer.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1058>
2023-01-23 11:08:49 +02:00
Sebastian Dröge 407a367529 dav1d: Get rid of some unnecessary unwrap()s
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1056>
2023-01-22 00:59:54 +02:00
Sebastian Dröge 88a437ac32 dav1d: Remove unnecessary frame dropping loop
After flushing there are no frames left anymore that could be dropped.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1056>
2023-01-22 00:59:52 +02:00
Sebastian Dröge 853acfc4fe dav1d: Don't flush the decoder when draining
This directly discards all frames and it won't be possible to output
them anymore.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1056>
2023-01-22 00:59:51 +02:00
Sebastian Dröge 3cd6074a8e dav1d: Only drain at most one decoded frame per input frame unless the decoder requires more before accepting new data
This works around a race condition in dav1d where the decoder deadlocks
if multiple threads are used, and also is generally beneficial as it
allows for proper frame threading.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1056>
2023-01-22 00:59:48 +02:00
Sebastian Dröge ab5ee0511b Update Cargo.lock 2023-01-19 19:06:52 +02:00
Sebastian Dröge 4ba452dcc3 Update versions to 0.9.7 2023-01-19 19:06:43 +02:00
Sebastian Dröge 711313c4c5 gtk4: Only provide a buffer pool to upstream if it requested one 2023-01-19 16:40:45 +02:00
Sebastian Dröge c83f48f0a1 gtk4: Make no caps in the allocation query a non-error 2023-01-19 16:40:26 +02:00
Sebastian Dröge 101bcbc1a0 gtk4: Asynchronously flush frames from GDK
There is no need to wait until the frames are flushed as the textures
will be kept alive until GDK is finished with them, and doing so can
cause deadlocks.

Fixes https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/issues/287

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1053>
2023-01-19 15:40:04 +02:00
Sebastian Dröge 2a68be2000 gtk4: Keep GstGLMemory alive as long as it is used inside GDK
Otherwise the texture might be released in the meantime and GDK would
use an invalid GL texture ID.

Fixes https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/issues/287

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1053>
2023-01-19 15:40:03 +02:00
Sebastian Dröge 3ea77d7a74 Update Cargo.lock 2023-01-18 17:19:28 +02:00
Sebastian Dröge c818a575b4 Update versions to 0.9.6 2023-01-18 17:19:17 +02:00
Sebastian Dröge 43e5bd7b3a Update Cargo.lock
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1050>
2023-01-18 16:57:28 +02:00
Guillaume Desmottes c6158b7a4e livesync: fix late-threshold property min value
The code is handling 0 as "always over threshold" but it was not
possible to set the property to 0.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1050>
2023-01-18 16:56:34 +02:00
Philippe Normand 27f5b5cc33 meson: Only enable cargo features when options are enabled (bis)
Fixes https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/issues/285 even more.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1050>
2023-01-18 16:56:24 +02:00
Sebastian Dröge d02508a7d0 aws: Update to AWS SDK 0.53/0.23
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1050>
2023-01-18 16:56:10 +02:00
Sebastian Dröge df3b90881f Update Cargo.lock
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1045>
2023-01-11 18:38:47 +02:00
Mathieu Duponchelle 53ae335d22 webrtcsink: fix panic on pre-bwe request error
We dispose of consumer pipelines asynchronously, potentially after the
session objects have been disposed of.

As session objects are the owner of the cc element, it is entirely
possible for the bwe-request signal to get emitted after cc has been
disposed of, as the closure only takes a weak reference to it.

Fix by simply checking if cc is None

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1045>
2023-01-11 18:38:13 +02:00
Sebastian Dröge c8e8af3e81 deny: Ignore duplicated base64 dependency for now
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1045>
2023-01-11 18:38:13 +02:00
Sebastian Dröge 8e0fc8b063 Update Cargo.lock
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1042>
2023-01-10 20:20:56 +02:00
Nirbheek Chauhan cc8da54adb meson: Only enable cargo features when options are enabled
Fixes https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/issues/285

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1042>
2023-01-10 20:20:24 +02:00
Sebastian Dröge e213ba9618 deny: Remove duplicated windows dependencies
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1042>
2023-01-10 20:20:24 +02:00
Sebastian Dröge e8df0a0cb7 Update Cargo.lock
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1040>
2023-01-10 10:31:23 +02:00
Sebastian Dröge 408d439631 rav1e: Enable threading support
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1040>
2023-01-10 10:30:34 +02:00
Sebastian Dröge 2f623e15c2 Update Cargo.lock 2023-01-07 16:06:29 +02:00
Sebastian Dröge 2a8a90f76f Update versions to 0.9.5 2023-01-07 16:06:17 +02:00
Sebastian Dröge cd3e333a0c Update Cargo.lock
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1038>
2023-01-07 13:59:53 +02:00
Sebastian Dröge 1bfe6f9142 gtk4: Update dependencies to releases
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1038>
2023-01-07 13:59:26 +02:00
Sebastian Dröge db9ef0b2af gtk4: Propagate the GL display to the remainder of the pipeline
This allows sharing it with other parts of the pipeline and avoids
creating different, incompatible displays/contexts in different parts of
the pipeline.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1038>
2023-01-07 13:25:49 +02:00
Sebastian Dröge 85a03f5ff0 fmp4mux: Remove obsolete comment
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1038>
2023-01-07 13:25:44 +02:00
Sebastian Dröge 4b936950c2 aws: Update to test-with 0.9
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1038>
2023-01-07 13:25:37 +02:00
rajneeshksoni 698ab100b3 awss3hlssink: Add stats property.
application can monitor the progress of hls segment generation
and upload progress.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1038>
2023-01-07 13:25:31 +02:00
Philippe Normand 517dc286d0 rtpav1depay: Implement srcpad set_caps
Without this auto-pluggers such as decodebin or parsebin will be unable to
process AV1 RTP payloads.

Tested with: `videotestsrc num-buffers=50 ! videoconvert ! av1enc ! av1parse ! rtpav1pay ! queue ! decodebin3 ! videoconvert ! queue ! autovideosink`

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1038>
2023-01-07 13:25:15 +02:00
Guillaume Desmottes 514a8e48ef textahead: fix previous buffers
Actually implement a proper queue.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1038>
2023-01-07 13:25:07 +02:00
Sebastian Dröge ff1c99df98 gtk4: Rename a variable to make more sense
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1038>
2023-01-07 13:25:00 +02:00
Sebastian Dröge 696944c08e gtk4: Handle more GL context creation failures gracefully
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1038>
2023-01-07 13:24:53 +02:00
Sebastian Dröge 37dedfd4d0 gtk4: Reset app context and display if GL context creation fails
No need to keep them around and that way we either have all 3 values set
or none of them.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1038>
2023-01-07 13:24:48 +02:00
Sebastian Dröge bb2f632c9c gtk4: Reduce number of unwraps during GL context creation and query handling
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1038>
2023-01-07 13:24:42 +02:00
Jordan Petridis f6b092d2af video/gtk4: Fix typo in info logs
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1038>
2023-01-07 13:24:30 +02:00
Nirbheek Chauhan cd5a93dc09 meson: Enable gstreamer-gl-1.0 features in gtk4 plugin
Basically, if gstreamer-gl-1.0 is built with wayland / x11 / egl, use
those features in the gtk4 plugin.

MacOS always uses CGL, and it's always available. Windows version does
not use GL yet.

Requires https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/3654

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1038>
2023-01-07 13:24:24 +02:00
Nirbheek Chauhan a5a3c44951 cargo_wrapper: Write to log with line-buffering
So we get log output while cargo is running, not just when it completes

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1038>
2023-01-07 13:24:18 +02:00
Nirbheek Chauhan a0dbb94e01 gtk4: Remove 'gst' prefix from another debug category
Missed it last time. Caught all of them this time. Continuation from:

https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1029

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1038>
2023-01-07 13:24:12 +02:00
Nirbheek Chauhan 7013416a39 meson: Require gstreamer-gl-1.0 for gtkpaintablesink
This is required on macOS, and is also highly recommended on Linux.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1038>
2023-01-07 13:24:05 +02:00
Nirbheek Chauhan 9f8fa99089 gtk4: Use GL implicitly without the gst_gl feature on macOS
We already require gstreamer-gl as a dependency on macOS, so reflect
that in the code too.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1038>
2023-01-07 13:23:59 +02:00
Sebastian Dröge 34434bd877 gtk4: Add support for GL on macOS
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1038>
2023-01-07 13:23:53 +02:00
Nirbheek Chauhan d67baa7668 meson: Add an option to build examples
Required renaming threadshare/benchmark to threadshare/ts-benchmark
because 'benchmark' as a target name is reserved for meson's
`benchmark` target.

Disabled by default because cargo decides that it has to rebuild
everything, and is really slow because of that.

Also required adding --features for setting features required by the
examples.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1038>
2023-01-07 13:23:47 +02:00
Nirbheek Chauhan 8cdb30bd39 meson: Add options for all plugins
Required a slight rework of the build file, and how options are passed
to cargo_wrapper.py

Necessitated a bump of the required gstreamer version to 1.20, which
should be fine for the meson build since its primary function is to be
built as part of the gstreamer monorepo build to get a dev env.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1038>
2023-01-07 13:23:41 +02:00
Nirbheek Chauhan efc07cecf7 cargo_wrapper: Fix setting of PKG_CONFIG_PATH and CARGO_TARGET_DIR
Don't need to use an env var for the latter.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1038>
2023-01-07 13:23:36 +02:00
Nirbheek Chauhan 7db53aba22 meson: Require tomllib / tomli python modules explicitly
These are required by dependencies.py

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1038>
2023-01-07 13:23:29 +02:00
Nirbheek Chauhan 2045847bd6 gtk4: Remove 'gst' from gtksink debug category name
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1038>
2023-01-07 13:23:23 +02:00
Sebastian Dröge f4cb4b9da6 Update Cargo.lock 2022-12-27 13:15:11 +02:00
Sebastian Dröge b0bd55c4d2 Update versions to 0.9.4 2022-12-27 13:14:59 +02:00
Sebastian Dröge b9e6c817b7 Update Cargo.lock
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1027>
2022-12-27 12:53:56 +02:00
Sebastian Dröge e95a2c1016 gtk4: Release GStreamer GL context and display when going back to NULL state
And acquire it again next time when going to READY state.

Also clean up the whole GL context initialization.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1027>
2022-12-27 12:39:56 +02:00
Sebastian Dröge 31760b8f9a gtk4: Use glib::ThreadGuard instead of the fragile crate
Fixes https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/issues/272

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1027>
2022-12-27 12:39:56 +02:00
Sebastian Dröge 8d7ce380c4 gtk4: Don't try to use GL mapped video frames as raw RGB memory
This will fail badly because the memory pointers are actually GL texture
IDs, however this case can't really happen in practice so simply assert
on this.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1027>
2022-12-27 12:39:56 +02:00
Sebastian Dröge e8701652e2 gtk4: Don't error out when the main context channel does not exist anymore when rendering
But instead return flushing to shut down silently.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1027>
2022-12-27 12:39:56 +02:00
Sebastian Dröge 993619d654 gtk4: Flush frames from the paintable when shutting down the sink
Otherwise it will continue showing the last frames forever and keep
around the frames forever instead of rendering black.

Fixes https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/issues/281

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1027>
2022-12-27 12:39:56 +02:00
Johan Bjäreholt d9d5571641 fmp4mux: Only push fragment_offset if write_mfra is true
This is done so that the fragment_offset vector does not infinitely
build up when write_mfra is disabled.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1027>
2022-12-27 12:39:56 +02:00
Nirbheek Chauhan ba889c143c meson: Disable webp plugin on Windows and macOS
Known to be broken, should be kept disabled till the fix is in
a release: https://github.com/qnighy/libwebp-sys2-rs/pull/13

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1027>
2022-12-27 12:39:56 +02:00
Nirbheek Chauhan 4b95bde38f meson: Handle windows path separator correctly
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1027>
2022-12-27 12:39:56 +02:00
Nirbheek Chauhan 041f51c4bb cargo_wrapper: Handle windows paths for depfiles
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1027>
2022-12-27 12:39:56 +02:00
Sebastian Dröge 161c6db641 deny: Update
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1027>
2022-12-27 12:39:56 +02:00
Sebastian Dröge deeff67f94 aws: Update to AWS SDK 0.52/0.22
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1027>
2022-12-27 12:39:56 +02:00
Nirbheek Chauhan 568c2be582 meson: Fix pkgconfig detection when specified in machine file
When pkgconfig and pkg_config_path are specified in the machine file,
we need to parse those and pass them on to the cargo_wrapper.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1027>
2022-12-27 12:39:56 +02:00
Nirbheek Chauhan 548fe54ba9 meson: Do not serialize env, use env: kwarg
This is simpler, and more correct.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1027>
2022-12-27 12:39:56 +02:00
Sebastian Dröge a1afef2207 Update Cargo.lock
This time correctly.
2022-12-27 10:43:00 +02:00
Sebastian Dröge 778c4da27e Update Cargo.lock 2022-12-27 10:32:03 +02:00
Sebastian Dröge cbc99fb198 Revert "Revert "rav1e: Update to rav1e 0.6""
This reverts commit 5f6afce842.

It should be building fine on macOS too now.
2022-12-27 10:31:33 +02:00
Sebastian Dröge b701003352 livesync: Add missing version to the gst-plugin-gtk4 / gst-plugin-version-helper dependencies 2022-12-16 20:37:58 +02:00
Sebastian Dröge 5f9645bb74 Update Cargo.lock 2022-12-16 20:24:01 +02:00
Sebastian Dröge bae5294e8f Update versions to 0.9.3 2022-12-16 20:22:17 +02:00
Sebastian Dröge 19957d1d23 Update Cargo.lock
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1017>
2022-12-16 18:59:32 +02:00
Sebastian Dröge bc9408840f livesync: Use release versions of the GLib/GStreamer bindings
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1017>
2022-12-16 18:59:32 +02:00
Sebastian Dröge 08668a4bbb livesync: Fix version
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1017>
2022-12-16 18:59:32 +02:00
Sebastian Dröge fb745f077b fmp4mux: Skip gap buffers earlier to consider them for the sample durations and fragment start durations
Otherwise dropping the gap buffers would offset the timestamps of
following samples.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1017>
2022-12-16 18:59:32 +02:00
Sebastian Dröge 1eea2219c6 mp4mux: Adjust durations and possibly stream start time on encountering a gap buffer
If there was a previous sample in this stream then its duration needs to
be extended by the gap position, and if there was none then the start
time of the whole stream has to be shifted by the duration.

Not doing so causes timestamps to be offset wrongly by the duration of
the gap.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1017>
2022-12-16 18:52:03 +02:00
Sebastian Dröge 9b2d9ba4f9 mp4mux: Fix edit list shift for streams with initial DTS smaller earliest PTS but initial DTS positive
This would be a stream where the initial DTS is negative if the initial
PTS was zero, but it is offset so the initial DTS became positive now.
The edit list shift has to happen exactly the same way though.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1017>
2022-12-16 18:51:57 +02:00
Sebastian Dröge 00615ab478 mp4mux: Don't write gap edit lists if their duration would be zero
The track might start later than the earliest track by less than one
timescale units, in which case writing an empty gap edit list would be
useless and confusing.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1017>
2022-12-16 18:51:50 +02:00
Sebastian Dröge 6ceccac1be mp4mux: Don't write empty chunks at the end if the last buffer of a stream started a new chunk and happened to be a from a gap event
Empty chunks are not valid in MP4.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1017>
2022-12-16 18:51:30 +02:00
Jan Alexander Steffens (heftig) 6596b6cdd1 Add livesync plugin
It attempts to produce a (nearly) gapless live stream by synchronizing
its output to the running time and forwarding the next input buffer if
its start is (nearly) flush with the end of the last output buffer.

If the input buffer is missing or too far in the future, it duplicates
the last output buffer with adjusted timestamps. If it is operating on a
raw audio stream, it will fill duplicate buffers with silence.

If an input buffer arrives too late, it is thrown away. If the last
input buffer was accepted too long ago (according to `late-threshold`),
a late input buffer is accepted anyway, but immediately considered a
duplicate. Due to the silence-filling, this has no effect on audio, but
video gets a "slideshow" effect instead of freezing completely.

The "many-repeats" property will be notified when this element has
recently duplicated a lot of buffers or recovered from such a state.

Co-authored-by: Vivia Nikolaidou <vivia@ahiru.eu>
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1017>
2022-12-16 18:51:22 +02:00
Michiel Konstapel b5641d838e audiornnoise: Add debug output for voice activity to help you choose a threshold
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1017>
2022-12-16 18:51:15 +02:00
Mathieu Duponchelle fffd7dc542 webrtc/README: update command to run the signalling server
Fixes https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/issues/277

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1017>
2022-12-16 18:51:08 +02:00
Sebastian Dröge b4185134d1 Fix various new clippy warnings
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1017>
2022-12-16 18:51:00 +02:00
Sebastian Dröge dc7e3c9f28 Update Cargo.lock again 2022-12-12 20:04:13 +02:00
Sebastian Dröge 5f6afce842 Revert "rav1e: Update to rav1e 0.6"
Revert for the time being because it pulls in libgit2-sys, which fails
to build on macOS because of course it does. It regularly fails building
everywhere because of its brittle C code and build system, which is why
gst-plugin-version-helper moved away from it.

This reverts commit e6789fc338.
2022-12-12 19:01:40 +02:00
Sebastian Dröge 7b1ee9f948 webrtchttp: Remove unnecessary clippy warning override
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1008>
2022-12-12 14:31:33 +02:00
Sebastian Dröge b6c9c14ccf Update Cargo.lock
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1008>
2022-12-12 14:21:30 +02:00
Sebastian Dröge 3936211b55 threadshare: Update to concurrent-queue 2
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1008>
2022-12-12 13:47:45 +02:00
Sebastian Dröge 2a981132b4 gtk4: Only require GTK 4.6 if GL support is enabled
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1008>
2022-12-12 13:46:57 +02:00
Michiel Konstapel a1fd847f70 audiornnoise: add voice detection threshold
Add a property "voice-activity-threshold". Frames where the voice
detection score from the RNN is below the threshold will be completely
muted.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1008>
2022-12-12 13:40:01 +02:00
Sebastian Dröge 71558bd086 gtk4: Deactivate application GL context again after fill_info()
It does not need to be activate anymore, and keeping it active can cause
problems.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1008>
2022-12-12 13:39:55 +02:00
Jordan Petridis b689a0825e gtk4: Deactivate the context if we fail to fill_info
Avoid leaving the context activated if we end up erroring out.

Similar to https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/3492

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1008>
2022-12-12 13:39:50 +02:00
Guillaume Desmottes b3e33e329b textahead: add settings to display previous buffers
I'll use this in Karapulse to keep displaying the few previous lyrics
rather than having them disappear right away.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1008>
2022-12-12 13:39:44 +02:00
Sebastian Dröge 8c27aefe76 net: Update to async-tungstenite 0.19
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1008>
2022-12-12 13:39:38 +02:00
Sebastian Dröge 44ec9eba7f audiorrnoise: Use correct value range for the samples
The nnnoiseless crate wants all samples in the range [-32767,32767]
instead of the [-1,1] range we're using for floating point samples.

Scale before/after processing while (de)interleaving the samples.

Fixes https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/issues/276

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1008>
2022-12-12 13:39:34 +02:00
Sebastian Dröge fd5b31fb43 tttocea608: Don't fail if a gap event contains no duration
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1008>
2022-12-12 13:39:27 +02:00
Sebastian Dröge d79edce517 webrtchttp: Fix documentation JSON
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1008>
2022-12-12 13:39:21 +02:00
Sebastian Dröge 412c191fc2 whipsink: Handle offer creation errors more gracefully
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1008>
2022-12-12 13:39:16 +02:00
Sebastian Dröge e46d2dfa54 webrtchttp: Fix missing import for docs build
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1008>
2022-12-12 13:39:10 +02:00
Sebastian Dröge e4788662b9 webrtchttp: Don't use let-else for now
We still support Rust 1.63.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1008>
2022-12-12 13:39:04 +02:00
Sebastian Dröge cab5410782 webrtchttp: Fix formatting
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1008>
2022-12-12 13:38:59 +02:00
Sanchayan Maity 8ac5632561 webrtchttp: Use tokio runtime for spawning thread used for candidate offer
While at it, we had a bug in whepsrc where for redirect we were
incorrectly calling initial_post_request instead of do_post. Fix
that.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1008>
2022-12-12 13:38:53 +02:00
Sanchayan Maity 4f67623c22 webrtchttp: Use a proper Rust type name for ICE transport policy
We don't need to namespace here but can just use the Rust namespaces.
Only the GType name has to stay like it is.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1008>
2022-12-12 13:38:48 +02:00
Sanchayan Maity 1d4d9b3bdb webrtchttp: Do not import element_imp_error
element_imp_error and such macros should not be imported but rather
only be accessed via gst namespace.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1008>
2022-12-12 13:38:41 +02:00
Sanchayan Maity 3202c4dc39 webrtchttp: Do not block webrtcbin signal handlers for sending candidates
While at it, drop the OPTIONS request in WHIP sink. This was not really
required. See section 4.4 of the spec
https://www.ietf.org/archive/id/draft-ietf-wish-whip-01.html#name-stun-turn-server-configurat

Also introduce a new error type and distinguish between a future being
aborted or returning an error.

We call abort only during shutdown and hence except for the DELETE
resource request being aborted, other waits on future should not
be fatal.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1008>
2022-12-12 13:38:36 +02:00
Alba Mendez 3bc9df7e71 webrtchttp: whipsink: construct TURN URL correctly
Right now the code manually pieces together the components
in a String for efficiency. When credentials contain special
characters this can result in invalid URLs, so do it the proper
way (with Url::parse + format) to make sure components are escaped
as needed.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1008>
2022-12-12 13:38:29 +02:00
Sanchayan Maity 929c48e19a webrtchttp: Drop unused dependencies
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1008>
2022-12-12 13:38:20 +02:00
Sanchayan Maity 4e9ec324e1 webrtchttp: Implement timeout for waiting on futures
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1008>
2022-12-12 13:37:47 +02:00
Sanchayan Maity 3fc0326084 webrtchttp: whipsink: Add candidates when sending the offer
WHIP endpoint providers like Cloudflare do not support Trickle ICE
and need candidates to be send along with the initial offer. Instead
of sending the offer in create-offer promise, send it once the ICE
candidates have been gathered.

While at it add properties to set STUN and TURN server along with the
ICE transport policy as at least when testing the Cloudflare WHIP
endpoint seems unreachable without it. This has also been observed
with Cloudflare provided demos.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1008>
2022-12-12 13:37:43 +02:00
Sanchayan Maity 420716fb63 webrtchttp: whipsink: Miscellaneous clean up
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1008>
2022-12-12 13:37:35 +02:00
Sanchayan Maity baf3da86cc webrtchttp: Factor out the common bits for WHIP and WHEP
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1008>
2022-12-12 13:37:30 +02:00
Sanchayan Maity 2e529fa152 Add a WebRTC WHEP source element
This implements WHEP specification based on
https://datatracker.ietf.org/doc/html/draft-murillo-whep-00

and has been tested with Cloudflare.

Server offers are likely to be removed from the WHEP specification
in upcoming revisions, to avoid compatibility issues. None of the
commercial services implementing WHEP support server initiated offers.
So we only support client side initiated offers.

Follows session setup and tear down as covered in Figure 1, Section 3
of the specification.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1008>
2022-12-12 13:37:13 +02:00
Sebastian Dröge 60772d2c06 ci: Disable gst-build job for now
See https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/issues/262

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1008>
2022-12-12 13:37:04 +02:00
Raphael Dürscheid 184f879bf7 webrtcsink: Support nvv4l2vp9enc
Naive support for nvv4l2vp9enc by assuming configuration is equivalent
to existing nvv4l2vp8enc. Validated to have relevant properties.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1008>
2022-12-12 13:37:00 +02:00
Sebastian Dröge b2ad89cf06 deny: Remove another dependency that is not duplicated anymore
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1008>
2022-12-12 13:36:50 +02:00
Seungha Yang 506c96e8aa dav1ddec: Lower rank to primary
The rank of AOM av1dec was demoted as secondary, and thus
primary rank is sufficient.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1008>
2022-12-12 13:36:43 +02:00
Sebastian Dröge 2227b41342 deny: Remove dependencies that are not duplicated anymore
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1008>
2022-12-12 13:36:38 +02:00
Sebastian Dröge e6789fc338 rav1e: Update to rav1e 0.6
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1008>
2022-12-12 13:36:26 +02:00
Sebastian Dröge b7534643be gtk4: example: Use a bin with a videoconvert in the non-GL case
The sink only supports RGB formats in that case, which decoders rarely
would output.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1008>
2022-12-12 13:35:52 +02:00
Sebastian Dröge 0b2aa2646f gtk4: Make GL support fully optional
Don't depend on gstreamer-gl if it's not enabled, and don't try doing
anything with the GDK GL context at all.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1008>
2022-12-12 13:35:07 +02:00
Jordan Petridis 507377c052 video/gtk4: Implement support for GLTextures when possible.
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1008>
2022-12-12 13:34:22 +02:00
Jordan Petridis f590b7e62f video/gtk4: Restrict visibility of struct related to the Frame
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1008>
2022-12-12 13:32:32 +02:00
Jordan Petridis 9fa3d88a63 video/gtk4: Rename Object types and struct to something simpler
Avoid the confusion caused by SinkPaintable and PaintableSink,
and instead refer to the objects as Paintable for the GdkPaintable
subclass or PaintalbeSink for the gst element.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1008>
2022-12-12 13:32:25 +02:00
Jordan Petridis b8d2d98027 mux/{mp4, fmp4}: Hard depend on feature v1_18
Else --no-default-features was failing to compile.

v1_18 is needed to for the aggregator code.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1008>
2022-12-12 13:32:02 +02:00
Jordan Petridis bfe62488f4 net/ndi: fix build with --no-default-features
doc_show_default() is only available with gst/v1_18

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1008>
2022-12-12 13:30:57 +02:00
Jordan Petridis 922f14ea19 meson: Fix build of static plugins
While we were correctly skipping the plugins that couldn't be
built statically, we were still adding their names to the list
and the .pc list causing them to still get built.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1008>
2022-12-12 13:30:48 +02:00
Jordan Petridis 204e9af663 meson: Fix build of static plugins
While we were correctly skipping the plugins that couldn't be
built statically, we were still adding their names to the list
and the .pc list causing them to still get built.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1008>
2022-12-12 13:30:42 +02:00
Sebastian Dröge a5f48507c4 textwrap: Don't panic on empty buffers
Simply don't calculate with any duration per word for this buffer.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1008>
2022-12-12 13:30:34 +02:00
Sebastian Dröge 7df114e0e9 deny: Update
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1008>
2022-12-12 13:30:23 +02:00
Sebastian Dröge a7c75f8066 fmp4mux: Crank clock for the first fragment in more tests
Due to how aggregator works, it depends on how buffers are pulled
whether aggregate() is called again or it is waiting for a timeout or EOS:

works:
  - pad 1: 4 buffers, pad 2: 4 buffers
  - aggregate ready: take all 4/4 buffers
  - pad 1: 1 buffers, pad 2: 1 buffer
  - aggregate ready: take all 1/1 buffers

waits:
  - pad 1: 5 buffers, pad 2: 4 buffers
  - aggregate ready: take all 5/4 buffers
  - pad 1: 0 buffers, pad 2: 1 buffer
  - aggregate not ready: waiting for timeout or EOS

Also don't manually set the clock time as that's unnecessary.

Fixes https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/issues/274

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/999>
2022-12-05 00:47:51 +00:00
Tim-Philipp Müller 08799d242c Update Cargo.lock
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/999>
2022-12-05 00:30:48 +00:00
Vivia Nikolaidou a59a0340cf ndisrc: Use actual number of channels in positions_from_mask
Otherwise it fails for mono and stereo

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/992>
2022-11-29 12:19:26 +02:00
Vivia Nikolaidou cadf36ff01 ndisrc: Use default channel mask for audio output
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/989>
2022-11-28 17:10:08 +02:00
Sebastian Dröge 6a05b7f56a mp4: Add version to gst-plugin-version-helper dependency 2022-11-28 11:46:29 +02:00
Sebastian Dröge 1f4a035dc0 Update versions to 0.9.2 2022-11-28 11:44:33 +02:00
Sebastian Dröge b41d1e3f34 Update Cargo.lock
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/987>
2022-11-28 10:47:14 +02:00
Sebastian Dröge 649434bd04 mp4mux: For video with N/1001 framerates use N as timescale
See https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/3049

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/987>
2022-11-28 10:47:02 +02:00
Sebastian Dröge 10813ed621 mp4mux: Factor out running time to UTC time calculation into a function
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/987>
2022-11-28 10:47:02 +02:00
Sebastian Dröge ea0d5751a2 mp4mux: Remove unnecessary error case of negative PTS when doing the ONVIF UTC time calculations
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/987>
2022-11-28 10:47:02 +02:00
Sebastian Dröge c771c86631 mp4mux: Skip gap buffers instead of writing empty samples
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/987>
2022-11-28 10:47:02 +02:00
Sebastian Dröge 516b561191 mp4: Add ONVIF non-fragmented MP4 muxer
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/987>
2022-11-28 10:47:02 +02:00
Sebastian Dröge 23e8fea170 mp4: Remove unneeded cast in tests
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/987>
2022-11-28 10:47:02 +02:00
Sebastian Dröge 81a46ee33d mp4: Update to url 2
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/987>
2022-11-28 10:47:02 +02:00
Sebastian Dröge e7f5e73e3f gst-plugin-mp4: Add new MP4 plugin with a non-fragmented MP4 muxer
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/987>
2022-11-28 10:47:02 +02:00
Sebastian Dröge 969be7ab52 Update Cargo.lock
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/986>
2022-11-27 20:53:40 +02:00
Sebastian Dröge 931917e559 aws: Update to env_logger 0.10 for the tests
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/986>
2022-11-27 20:53:13 +02:00
Sebastian Dröge 1fb0062059 deny: Update
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/986>
2022-11-27 20:52:39 +02:00
Sebastian Dröge 93ba677b18 fmp4mux: Handle EOS correctly if it happens before a fragment start time was determined
Whatever earliest time we have at that point is going to be the start
time.

Also handle the case correctly where all inputs are EOS before any
buffers were received at all.

Fixes https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/issues/270

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/986>
2022-11-27 20:52:30 +02:00
Sebastian Dröge 9491c77540 fmp4mux: For video with N/1001 framerates use N as timescale
See https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/3049

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/986>
2022-11-27 20:51:48 +02:00
Sebastian Dröge 6c15bba592 fmp4mux: Re-work buffer dequeueing and calculations of timestamps
Especially simplify calculation of ONVIF UTC times. As a side-effect
this reduces the number of times the running times of a buffer are
calculated, and also causes streams to be interleaved correctly in ONVIF
mode if there is a non-constant UTC-to-running-time difference.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/986>
2022-11-27 20:51:48 +02:00
Sebastian Dröge 2b287bcd61 gif: Update to gif 0.12
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/986>
2022-11-27 20:45:18 +02:00
Sebastian Dröge 582cc34895 Provide explicit type to Iterator::sum() calls to avoid ambiguity
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/986>
2022-11-27 20:44:44 +02:00
Guillaume Desmottes 8bd9de8d48 spotify: fix "start a runtime from within a runtime" with static link
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/986>
2022-11-27 20:44:23 +02:00
Arun Raghavan b015688447 aws: s3sink: Treat stopping without EOS as an error for multipart upload
This allows us to try to clean up based on configuration (abort /
complete / do nothing) if the pipeline is shut down without an EOS.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/986>
2022-11-27 20:44:15 +02:00
Sebastian Dröge 274e57a536 Update Cargo.lock 2022-11-13 20:26:21 +02:00
Sebastian Dröge e434fd19ca Update versions to 0.9.1 2022-11-13 20:23:47 +02:00
Sebastian Dröge 28065de413 closedcaption: Update for deprecated chrono functions
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/977>
2022-11-12 18:41:58 +02:00
Sebastian Dröge a6f64b5b20 version-helper: Update for deprecated chrono functions
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/977>
2022-11-12 18:24:02 +02:00
Sebastian Dröge 5295fe9e67 Update Cargo.lock
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/977>
2022-11-12 18:15:50 +02:00
Guillaume Desmottes 331d053516 webrtc: README: fix couple of links
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/977>
2022-11-12 15:52:50 +00:00
Mathieu Duponchelle 5c9bc03eab webrtcsink: improve debug
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/977>
2022-11-12 15:52:50 +00:00
Tim-Philipp Müller 8c454c5c37 ci: add trigger job and only run documentation job post-merge
- require manual trigger to run pipeline on branches and MRs
- require manual trigger to run pipeline post-merge (excl. docs)

https://gitlab.freedesktop.org/gstreamer/gstreamer-rs/-/issues/417

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/977>
2022-11-12 15:52:50 +00:00
Tim-Philipp Müller a9f3ff2925 ci: add integration stage and move documentation job to that
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/977>
2022-11-12 15:52:50 +00:00
François Laignel cdf07dd860 ts/udpsink: handle items in the PadSinkHandler
... instead of forwarding them to a Task via a channel.

This improves CPU usage by 5% according to `udpsrc-benchmark-sender`
with the `tuning` feature using default audio test buffers and
400 streams on the same ts-context.

It is expected to improve latency significantly. This is inferred
from `ts-standalone`: latency shrinks from around 5ms to 1.5µs
using the `task` sink compared to the `async-mutex` sink.

The async Mutex is mandatory here as we need to hold the lock
across await points.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/977>
2022-11-12 15:52:50 +00:00
François Laignel 2e52fece61 ts: introduce ts-audiotestsrc
This makes it easy to generate "listenable" signals and to evaluate
discontinuities.

When the `tuning` feature is activated and the `main-elem` property
is set, the element can log the parked duration in %, which is an
image of the CPU usage for the ts-context.

This commit adds a test mode to `udpsrc-benchmark-sender` which
generates default audio buffers from `ts-audiotestsrc`. The `rtp`
mode is modified so that it uses `ts-audiotestsrc`.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/977>
2022-11-12 15:52:50 +00:00
François Laignel 01816e2a8a ts/standalone: add new Sinks
Contrary to the existing Task Sink, the Async and Sync Mutex Sinks
handle buffers in the `PadSinkHandler` directly. The Async Mutex
Sink uses an async Mutex for the `PadSinkHandlerInner` while the
Sync Mutex Sink uses... a sync Mutex.

All Sinks share the same settings and stats manager.

Use the `--sink` command line option to select the sink (default is
`sync-mutex` since it allows evaluating the framework with as little
overhead as possible.

Also apply various fixes:

- Only keep the segment start instead of the full `Segment`. This
  helps with cache locality (`Segment` is a plain struct with many
  fields) and avoids downcasting the generic `Segment` upon each
  buffer handling.
- Box the `Stat`s. This should improve cache locality a bit.
- Fix EOS handling which took ages for no benefits in this
  particular use case.
- Use a macro to raise log level in the main element.
- Move error handling during item processing in `handle_loop_error`.
  This function was precisely designed for this and it should reduce
  the `handle_item`'s Future size.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/977>
2022-11-12 15:52:50 +00:00
François Laignel ea82881e1c ts/standalone: move current sink under task_sink
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/977>
2022-11-12 15:52:50 +00:00
Sebastian Dröge 429e545e5c deny: Update
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/977>
2022-11-12 15:52:50 +00:00
Sebastian Dröge 2e3373647a Add missing doc features to WebRTC plugins
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/977>
2022-11-12 15:52:50 +00:00
Sebastian Dröge 43ac186e69 fmp4mux: Make media/trak timescales configurable
And refactor a bit of code for easier extensibility.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/977>
2022-11-12 15:52:50 +00:00
Sebastian Dröge 1ef9a46508 ci: Update to cargo-c 0.9.14
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/977>
2022-11-12 15:52:50 +00:00
Jan Beich b7891e77e5 meson: optionalize pango dependency used by net/onvif
Similar to -Dpango=<auto|enabled|disabled> in gst-plugins-base.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/977>
2022-11-12 15:52:49 +00:00
Jan Beich bdb423e2b9 ndi: provide Unix fallback after 3fe9e4a207
error[E0425]: cannot find value `LIBRARY_NAME` in this scope
   --> net/ndi/src/ndisys.rs:336:23
    |
336 |             path.push(LIBRARY_NAME);
    |                       ^^^^^^^^^^^^ not found in this scope

error[E0425]: cannot find value `LIBRARY_NAME` in this scope
   --> net/ndi/src/ndisys.rs:339:33
    |
339 |             path::PathBuf::from(LIBRARY_NAME)
    |                                 ^^^^^^^^^^^^ not found in this scope

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/977>
2022-11-12 15:52:49 +00:00
Sebastian Dröge 4556657602 fmp4mux: Don't allow VP9 for CMAF
This would require setting the correct compatible band for VP9 in CMAF,
which is not implemented yet.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/977>
2022-11-12 15:52:49 +00:00
Sebastian Dröge cb5a956ee7 fmp4mux: Add initial Opus support
Fixes https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/issues/239

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/977>
2022-11-12 15:52:49 +00:00
Sebastian Dröge d0228ed544 docs: Remove some stale entries of renamed elements
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/977>
2022-11-12 15:52:49 +00:00
Arun Raghavan 6b3f0f764e aws: Skip s3 test on Windows until we figure out why it times out
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/977>
2022-11-12 15:52:49 +00:00
Sebastian Dröge 07f3b0f504 Fix various new clippy warnings
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/977>
2022-11-12 15:52:49 +00:00
Sebastian Dröge 8dc22d3bf1 fmp4mux: For VP9, write resolution into the tkhd and include a stss box to signal that not all frames are sync samples
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/977>
2022-11-12 15:52:49 +00:00
Sebastian Dröge f2f0eb30e0 webrtc: Update to human_bytes 0.4
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/977>
2022-11-12 15:52:49 +00:00
Sebastian Dröge b596b407f6 aws: Update to aws 0.21/0.51
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/977>
2022-11-12 15:52:49 +00:00
Sebastian Dröge 2b6d87cf66 fmp4mux: Remove unused uuid dependency
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/977>
2022-11-12 15:52:49 +00:00
Sebastian Dröge 18fa678a5c Update Cargo.lock 2022-10-27 15:36:30 +03:00
Sebastian Dröge 8e2a6500aa fmp4mux: Clip negative PTS to zero/last PTS instead of erroring out
This can happen at the beginning of a stream if upstream is
rtpjitterbuffer and it has problems figuring out timestamps in the
beginning due to resetting / skew.
2022-10-27 15:35:17 +03:00
Sebastian Dröge e268577994 fmp4mux: Send force-keyunit events for now if the ideal position has already passed 2022-10-27 15:35:17 +03:00
Sebastian Dröge f2a6a8d3de fmp4mux: Add debug log when writing the mfra box 2022-10-27 15:35:17 +03:00
Sebastian Dröge 51ff099221 fmp4mux: Reset timing infos to None if a stream only contained gap events for a whole fragment 2022-10-27 15:35:17 +03:00
Sebastian Dröge eefa8540ba fmp4mux: If a stream is longer than the main stream at EOS, simply include all of its buffers in the last fragment nonetheless 2022-10-27 15:35:17 +03:00
Sebastian Dröge 790453364d whipsink: Add object to debug logs 2022-10-27 15:34:52 +03:00
Matthew Waters 32d2372e90 fmp4mux: don't require dts for predictive-only formats like vp9 2022-10-27 15:34:52 +03:00
Guillaume Desmottes 15955758b6 aws: fix title in README
The title was not matching the actual plugin name which was confusing.
2022-10-27 15:34:52 +03:00
Sebastian Dröge 54fe3f1c02 deny: Update 2022-10-27 15:34:32 +03:00
Matthew Waters a54318fbb4 fmp4: add support for muxing VP9 streams in cmaf, dash and iso fmp4
As specified in https://www.webmproject.org/vp9/mp4/
2022-10-27 15:34:32 +03:00
Sebastian Dröge 46152533ba Add Cargo.lock 2022-10-24 19:28:41 +03:00
Sebastian Dröge ba5270d30a Update to release versions of gtk-rs and gstreamer-rs 2022-10-24 19:28:41 +03:00
Sebastian Dröge 2ff40142db Update versions to 0.9.0 2022-10-24 18:25:05 +03:00
201 changed files with 27308 additions and 5526 deletions

View file

@ -6,7 +6,7 @@ include:
file: '/templates/debian.yml'
- project: 'gstreamer/gstreamer-rs'
ref: main
ref: '0.19'
file: '/ci/images_template.yml'
- project: 'gstreamer/gstreamer'
@ -38,10 +38,33 @@ default:
interruptible: true
stages:
- "trigger"
- "prep"
- "lint"
- "test"
- "extras"
- "integration"
# This is an empty job that is used to trigger the pipeline.
trigger:
image: alpine:latest
stage: 'trigger'
variables:
GIT_STRATEGY: none
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:11:
variables:
@ -49,30 +72,23 @@ stages:
before_script:
- source ./ci/env.sh
- mkdir .cargo && echo -e "[net]\ngit-fetch-with-cli = true" > .cargo/config
# If cargo exists assume we probably will want to update
# the lockfile
- |
if command -v cargo; then
cargo generate-lockfile
cargo update
fi
.debian:11-stable:
extends: .debian:11
variables:
FDO_DISTRIBUTION_TAG: '$GST_RS_STABLE-${GST_RS_IMG_TAG}_2022-09-07.0'
FDO_DISTRIBUTION_TAG: '$GST_RS_STABLE-${GST_RS_IMG_TAG}_2022-11-05.0'
FDO_BASE_IMAGE: "registry.freedesktop.org/gstreamer/gstreamer-rs/debian/bullseye-slim:$GST_RS_STABLE-$GST_RS_IMG_TAG"
.debian:11-msrv:
extends: .debian:11
variables:
FDO_DISTRIBUTION_TAG: '$GST_RS_MSRV-${GST_RS_IMG_TAG}_2022-09-07.0'
FDO_DISTRIBUTION_TAG: '$GST_RS_MSRV-${GST_RS_IMG_TAG}_2022-11-05.0'
FDO_BASE_IMAGE: "registry.freedesktop.org/gstreamer/gstreamer-rs/debian/bullseye-slim:$GST_RS_MSRV-$GST_RS_IMG_TAG"
.debian:11-nightly:
extends: .debian:11
variables:
FDO_DISTRIBUTION_TAG: 'nightly-${GST_RS_IMG_TAG}_2022-09-07.0'
FDO_DISTRIBUTION_TAG: 'nightly-${GST_RS_IMG_TAG}_2022-11-05.0'
FDO_BASE_IMAGE: "registry.freedesktop.org/gstreamer/gstreamer-rs/debian/bullseye-slim:nightly-$GST_RS_IMG_TAG"
.build-debian-container:
@ -86,6 +102,8 @@ stages:
apt clean &&
bash ./ci/install-rust-ext.sh &&
pip install tomli
needs:
- "trigger"
rules:
- if: '$UPDATE_IMG == null'
@ -230,7 +248,7 @@ meson shared:
meson static:
extends: .meson
script:
- meson build --default-library=static --prefix=$(pwd)/install -Dsodium=built-in
- meson build --default-library=static --prefix=$(pwd)/install -Dsodium-source=built-in
- ninja -C build install
- ./ci/generate-static-test.py test-static-link-all
- cd test-static-link-all
@ -246,6 +264,7 @@ meson static:
# Check that the gstreamer documentation keeps working
documentation:
image: $GSTREAMER_DOC_IMAGE
stage: 'integration'
variables:
MESON_ARGS: >
-Ddoc=enabled
@ -258,7 +277,7 @@ documentation:
-Dsharp=disabled
-Dgst-examples=disabled
-Drs=enabled
-Dgst-plugins-rs:sodium=system
-Dgst-plugins-rs:sodium-source=system
-Dgst-docs:fatal_warnings=true
-Dorc=disabled
script:
@ -279,54 +298,65 @@ documentation:
paths:
- documentation/
needs: []
rules:
# Run job if the MR is assigned to the Merge bot or it a post-merge pipeline on main branch
- if: '$CI_MERGE_REQUEST_ASSIGNEES == "gstreamer-merge-bot"'
when: 'always'
- if: '$CI_PROJECT_NAMESPACE == "gstreamer" && $CI_COMMIT_BRANCH == "main"'
when: 'always'
# Require explicit action to trigger otherwise
- if: '$CI_PROJECT_NAMESPACE != "gstreamer" || $CI_COMMIT_BRANCH != "main"'
when: 'manual'
# build gst-plugins-rs as a gst-build subproject
gst-build:
extends: .meson
rules:
- if: '$CI_PIPELINE_SOURCE == "schedule"'
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
when: 'manual'
allow_failure: true
variables:
MESON_ARGS: >
-Domx=disabled
-Dpython=disabled
-Dlibav=disabled
-Dlibnice=disabled
-Dugly=disabled
-Dbad=disabled
-Ddevtools=disabled
-Dges=disabled
-Drtsp_server=disabled
-Dvaapi=disabled
-Dsharp=disabled
-Dgst-examples=disabled
-Drs=enabled
-Dgst-plugins-rs:sodium=system
script:
- P=$(pwd)
- cd ..
- rm -rf gstreamer
- git clone --depth 1 https://gitlab.freedesktop.org/gstreamer/gstreamer.git --branch main
- cd gstreamer
- ln -s $P subprojects/gst-plugins-rs
- meson build $MESON_ARGS
- ninja -C build
# Check static Rust plugins can be linked into gst-full
- meson build-gst-full --default-library=static $MESON_ARGS
- ninja -C build-gst-full
- meson devenv -C build-gst-full ./gst-inspect-1.0 rsaudiofx
artifacts:
expire_in: '7 days'
when: always
paths:
- 'build/meson-logs/'
- 'build-gst-full/meson-logs/'
# Disabled because of https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/issues/262
#gst-build:
# extends: .meson
# rules:
# - if: '$CI_PIPELINE_SOURCE == "schedule"'
# - if: $CI_PIPELINE_SOURCE == "merge_request_event"
# when: 'manual'
# allow_failure: true
# variables:
# MESON_ARGS: >
# -Domx=disabled
# -Dpython=disabled
# -Dlibav=disabled
# -Dlibnice=disabled
# -Dugly=disabled
# -Dbad=disabled
# -Ddevtools=disabled
# -Dges=disabled
# -Drtsp_server=disabled
# -Dvaapi=disabled
# -Dsharp=disabled
# -Dgst-examples=disabled
# -Drs=enabled
# -Dgst-plugins-rs:sodium-source=system
# script:
# - P=$(pwd)
# - cd ..
# - rm -rf gstreamer
# - git clone --depth 1 https://gitlab.freedesktop.org/gstreamer/gstreamer.git --branch main
# - cd gstreamer
# - ln -s $P subprojects/gst-plugins-rs
# - meson build $MESON_ARGS
# - ninja -C build
# # Check static Rust plugins can be linked into gst-full
# - meson build-gst-full --default-library=static $MESON_ARGS
# - ninja -C build-gst-full
# - meson devenv -C build-gst-full ./gst-inspect-1.0 rsaudiofx
# artifacts:
# expire_in: '7 days'
# when: always
# paths:
# - 'build/meson-logs/'
# - 'build-gst-full/meson-logs/'
.msvc2019 build:
stage: 'test'
needs: []
needs:
- 'trigger'
tags:
- 'docker'
- 'windows'

6379
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -16,6 +16,7 @@ members = [
"mux/flavors",
"mux/fmp4",
"mux/mp4",
"net/aws",
"net/hlssink3",
@ -35,6 +36,7 @@ members = [
"text/wrap",
"utils/fallbackswitch",
"utils/livesync",
"utils/togglerecord",
"utils/tracers",
"utils/uriplaylistbin",
@ -63,6 +65,7 @@ default-members = [
"generic/threadshare",
"mux/fmp4",
"mux/mp4",
"net/aws",
"net/hlssink3",
@ -82,6 +85,7 @@ default-members = [
"text/wrap",
"utils/fallbackswitch",
"utils/livesync",
"utils/togglerecord",
"utils/tracers",
"utils/uriplaylistbin",

View file

@ -110,6 +110,8 @@ You will find the following plugins in this repository:
- `fmp4`: A fragmented MP4/ISOBMFF/CMAF muxer for generating e.g. DASH/HLS media fragments.
- `mp4`: A non-fragmented MP4 muxer for generating MP4 files.
* `text`
- `ahead`: A plugin to display upcoming text buffers ahead.
@ -127,6 +129,9 @@ You will find the following plugins in this repository:
configuring a fallback audio/video if there are problems with the main
source.
- `livesync`: Element to maintain a continuous live stream from a
potentially unstable source.
- `togglerecord`: Element to enable starting and stopping multiple streams together.
- `tracers`: Plugin with multiple tracers:

View file

@ -1,6 +1,6 @@
[package]
name = "gst-plugin-audiofx"
version = "0.9.0-alpha.1"
version = "0.9.10"
authors = ["Sebastian Dröge <sebastian@centricular.com>"]
repository = "https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs"
license = "MPL-2.0"
@ -9,9 +9,9 @@ edition = "2021"
rust-version = "1.63"
[dependencies]
gst = { package = "gstreamer", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", features = ["v1_16"] }
gst-base = { package = "gstreamer-base", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", features = ["v1_16"] }
gst-audio = { package = "gstreamer-audio", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", features = ["v1_16"] }
gst = { package = "gstreamer", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19.1", features = ["v1_16"] }
gst-base = { package = "gstreamer-base", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19", features = ["v1_16"] }
gst-audio = { package = "gstreamer-audio", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19", features = ["v1_16"] }
anyhow = "1"
byte-slice-cast = "1.0"
num-traits = "0.2"
@ -29,11 +29,11 @@ crate-type = ["cdylib", "rlib"]
path = "src/lib.rs"
[dev-dependencies]
gst-check = { package = "gstreamer-check", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", features = ["v1_18"] }
gst-app = { package = "gstreamer-app", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
gst-check = { package = "gstreamer-check", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19", features = ["v1_18"] }
gst-app = { package = "gstreamer-app", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19" }
[build-dependencies]
gst-plugin-version-helper = { path="../../version-helper" }
gst-plugin-version-helper = { version = "0.7", path="../../version-helper" }
[features]
static = []

View file

@ -90,10 +90,10 @@ fn run() -> Result<(), Error> {
let new_z = -x * f32::sin(ROTATION) + z * f32::cos(ROTATION);
let objs = [gst::Structure::builder("application/spatial-object")
.field("x", &new_x)
.field("y", &y)
.field("z", &new_z)
.field("distance-gain", &gain)
.field("x", new_x)
.field("y", y)
.field("z", new_z)
.field("distance-gain", gain)
.build()];
hrtf.set_property("spatial-objects", gst::Array::new(objs));

View file

@ -15,8 +15,8 @@ pub struct RingBuffer {
impl RingBuffer {
pub fn new(size: usize) -> Self {
let mut buffer = Vec::with_capacity(size as usize);
buffer.extend(iter::repeat(0.0).take(size as usize));
let mut buffer = Vec::with_capacity(size);
buffer.extend(iter::repeat(0.0).take(size));
Self {
buffer: buffer.into_boxed_slice(),

View file

@ -611,10 +611,9 @@ impl State {
// the position where we have to start writing the next 100ms in the next
// iteration.
let mut outbuf = gst::Buffer::with_size(
self.current_samples_per_frame as usize * self.info.bpf() as usize,
)
.map_err(|_| gst::FlowError::Error)?;
let mut outbuf =
gst::Buffer::with_size(self.current_samples_per_frame * self.info.bpf() as usize)
.map_err(|_| gst::FlowError::Error)?;
{
let outbuf = outbuf.get_mut().unwrap();
let mut dst = outbuf.map_writable().map_err(|_| gst::FlowError::Error)?;
@ -819,7 +818,7 @@ impl State {
// adjustment. frame_type should only ever be set to Final at the end if we ended up in
// Inner state before.
if self.frame_type == FrameType::First
&& (src.len() / self.info.channels() as usize) < self.current_samples_per_frame as usize
&& (src.len() / self.info.channels() as usize) < self.current_samples_per_frame
{
self.process_first_frame_is_last(imp)?;
}
@ -1560,7 +1559,7 @@ impl AudioLoudNorm {
}
// Need to reset the state now
*state = State::new(&*self.settings.lock().unwrap(), state.info.clone());
*state = State::new(&self.settings.lock().unwrap(), state.info.clone());
}
state.adapter.push(buffer);
@ -1602,7 +1601,7 @@ impl AudioLoudNorm {
Err(_) => return false,
};
}
*state = Some(State::new(&*self.settings.lock().unwrap(), info));
*state = Some(State::new(&self.settings.lock().unwrap(), info));
drop(state);
if let Some(outbuf) = outbuf {
@ -1623,7 +1622,7 @@ impl AudioLoudNorm {
Err(gst::FlowError::Eos) => None,
Err(_) => return false,
};
*state = State::new(&*self.settings.lock().unwrap(), state.info.clone());
*state = State::new(&self.settings.lock().unwrap(), state.info.clone());
}
drop(state);

View file

@ -7,6 +7,8 @@
//
// SPDX-License-Identifier: MPL-2.0
use std::sync::Mutex;
use gst::glib;
use gst::prelude::*;
use gst::subclass::prelude::*;
@ -31,8 +33,22 @@ static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
)
});
const DEFAULT_VOICE_ACTIVITY_THRESHOLD: f32 = 0.0;
const FRAME_SIZE: usize = DenoiseState::FRAME_SIZE;
#[derive(Debug, Clone, Copy)]
struct Settings {
vad_threshold: f32,
}
impl Default for Settings {
fn default() -> Self {
Settings {
vad_threshold: DEFAULT_VOICE_ACTIVITY_THRESHOLD,
}
}
}
struct ChannelDenoiser {
denoiser: Box<DenoiseState<'static>>,
frame_chunk: Box<[f32; FRAME_SIZE]>,
@ -47,6 +63,7 @@ struct State {
#[derive(Default)]
pub struct AudioRNNoise {
settings: Mutex<Settings>,
state: AtomicRefCell<Option<State>>,
}
@ -82,43 +99,6 @@ impl State {
fn needs_more_data(&self) -> bool {
self.adapter.available() < (FRAME_SIZE * self.in_info.bpf() as usize)
}
fn process(&mut self, input_plane: &[f32], output_plane: &mut [f32]) {
let channels = self.in_info.channels() as usize;
let size = FRAME_SIZE * channels;
for (out_frame, in_frame) in output_plane.chunks_mut(size).zip(input_plane.chunks(size)) {
for (index, item) in in_frame.iter().enumerate() {
let channel_index = index % channels;
let channel_denoiser = &mut self.denoisers[channel_index];
let pos = index / channels;
channel_denoiser.frame_chunk[pos] = *item;
}
for i in (in_frame.len() / channels)..(size / channels) {
for c in 0..channels {
let channel_denoiser = &mut self.denoisers[c];
channel_denoiser.frame_chunk[i] = 0.0;
}
}
// FIXME: The first chunks coming out of the denoisers contains some
// fade-in artifacts. We might want to discard those.
for channel_denoiser in &mut self.denoisers {
channel_denoiser.denoiser.process_frame(
&mut channel_denoiser.out_chunk[..],
&channel_denoiser.frame_chunk[..],
);
}
for (index, item) in out_frame.iter_mut().enumerate() {
let channel_index = index % channels;
let channel_denoiser = &self.denoisers[channel_index];
let pos = index / channels;
*item = channel_denoiser.out_chunk[pos];
}
}
}
}
impl AudioRNNoise {
@ -131,6 +111,7 @@ impl AudioRNNoise {
return Ok(gst::FlowSuccess::Ok);
}
let settings = *self.settings.lock().unwrap();
let mut buffer = gst::Buffer::with_size(available).map_err(|e| {
gst::error!(CAT, imp: self, "Failed to allocate buffer at EOS {:?}", e);
gst::FlowError::Flushing
@ -151,7 +132,7 @@ impl AudioRNNoise {
let mut out_map = buffer.map_writable().map_err(|_| gst::FlowError::Error)?;
let out_data = out_map.as_mut_slice_of::<f32>().unwrap();
state.process(in_data, out_data);
self.process(state, &settings, in_data, out_data);
}
self.obj().src_pad().push(buffer)
@ -164,6 +145,7 @@ impl AudioRNNoise {
let duration = state.buffer_duration(output_size as _);
let pts = state.current_pts();
let settings = *self.settings.lock().unwrap();
let mut buffer = gst::Buffer::with_size(output_size).map_err(|_| gst::FlowError::Error)?;
{
@ -181,11 +163,64 @@ impl AudioRNNoise {
let mut out_map = buffer.map_writable().map_err(|_| gst::FlowError::Error)?;
let out_data = out_map.as_mut_slice_of::<f32>().unwrap();
state.process(in_data, out_data);
self.process(state, &settings, in_data, out_data);
}
Ok(GenerateOutputSuccess::Buffer(buffer))
}
fn process(
&self,
state: &mut State,
settings: &Settings,
input_plane: &[f32],
output_plane: &mut [f32],
) {
let channels = state.in_info.channels() as usize;
let size = FRAME_SIZE * channels;
for (out_frame, in_frame) in output_plane.chunks_mut(size).zip(input_plane.chunks(size)) {
for (index, item) in in_frame.iter().enumerate() {
let channel_index = index % channels;
let channel_denoiser = &mut state.denoisers[channel_index];
let pos = index / channels;
channel_denoiser.frame_chunk[pos] = *item * 32767.0;
}
for i in (in_frame.len() / channels)..(size / channels) {
for c in 0..channels {
let channel_denoiser = &mut state.denoisers[c];
channel_denoiser.frame_chunk[i] = 0.0;
}
}
// FIXME: The first chunks coming out of the denoisers contains some
// fade-in artifacts. We might want to discard those.
let mut vad: f32 = 0.0;
for channel_denoiser in &mut state.denoisers {
vad = f32::max(
vad,
channel_denoiser.denoiser.process_frame(
&mut channel_denoiser.out_chunk[..],
&channel_denoiser.frame_chunk[..],
),
);
}
gst::debug!(CAT, imp: self, "Voice activity: {}", vad);
if vad < settings.vad_threshold {
out_frame.fill(0.0);
} else {
for (index, item) in out_frame.iter_mut().enumerate() {
let channel_index = index % channels;
let channel_denoiser = &state.denoisers[channel_index];
let pos = index / channels;
*item = channel_denoiser.out_chunk[pos] / 32767.0;
}
}
}
}
}
#[glib::object_subclass]
@ -195,7 +230,42 @@ impl ObjectSubclass for AudioRNNoise {
type ParentType = gst_base::BaseTransform;
}
impl ObjectImpl for AudioRNNoise {}
impl ObjectImpl for AudioRNNoise {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![glib::ParamSpecFloat::builder("voice-activity-threshold")
.nick("Voice activity threshold")
.blurb("Threshold of the voice activity detector below which to mute the output")
.minimum(0.0)
.maximum(1.0)
.default_value(DEFAULT_VOICE_ACTIVITY_THRESHOLD)
.mutable_playing()
.build()]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
match pspec.name() {
"voice-activity-threshold" => {
let mut settings = self.settings.lock().unwrap();
settings.vad_threshold = value.get().expect("type checked upstream");
}
_ => unimplemented!(),
}
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
"voice-activity-threshold" => {
let settings = self.settings.lock().unwrap();
settings.vad_threshold.to_value()
}
_ => unimplemented!(),
}
}
}
impl GstObjectImpl for AudioRNNoise {}

View file

@ -138,7 +138,7 @@ impl ObjectImpl for EbuR128Level {
.build()]
});
&*SIGNALS
&SIGNALS
}
fn properties() -> &'static [glib::ParamSpec] {
@ -482,7 +482,7 @@ impl BaseTransformImpl for EbuR128Level {
if state.ebur128.mode().contains(ebur128::Mode::M) {
match state.ebur128.loudness_momentary() {
Ok(loudness) => s.set("momentary-loudness", &loudness),
Ok(loudness) => s.set("momentary-loudness", loudness),
Err(err) => gst::error!(
CAT,
imp: self,
@ -494,7 +494,7 @@ impl BaseTransformImpl for EbuR128Level {
if state.ebur128.mode().contains(ebur128::Mode::S) {
match state.ebur128.loudness_shortterm() {
Ok(loudness) => s.set("shortterm-loudness", &loudness),
Ok(loudness) => s.set("shortterm-loudness", loudness),
Err(err) => gst::error!(
CAT,
imp: self,
@ -506,7 +506,7 @@ impl BaseTransformImpl for EbuR128Level {
if state.ebur128.mode().contains(ebur128::Mode::I) {
match state.ebur128.loudness_global() {
Ok(loudness) => s.set("global-loudness", &loudness),
Ok(loudness) => s.set("global-loudness", loudness),
Err(err) => gst::error!(
CAT,
imp: self,
@ -516,7 +516,7 @@ impl BaseTransformImpl for EbuR128Level {
}
match state.ebur128.relative_threshold() {
Ok(threshold) => s.set("relative-threshold", &threshold),
Ok(threshold) => s.set("relative-threshold", threshold),
Err(err) => gst::error!(
CAT,
imp: self,
@ -528,7 +528,7 @@ impl BaseTransformImpl for EbuR128Level {
if state.ebur128.mode().contains(ebur128::Mode::LRA) {
match state.ebur128.loudness_range() {
Ok(range) => s.set("loudness-range", &range),
Ok(range) => s.set("loudness-range", range),
Err(err) => {
gst::error!(CAT, imp: self, "Failed to get loudness range: {}", err)
}

View file

@ -373,7 +373,7 @@ impl HrtfRender {
let (prev_offset, _) = state.adapter.prev_offset();
let offset = prev_offset.checked_add(distance_samples).unwrap_or(0);
let duration_samples = outputsz / outbpf as usize;
let duration_samples = outputsz / outbpf;
let duration = samples_to_time(duration_samples as u64);
(pts, offset, duration)

View file

@ -1,6 +1,6 @@
[package]
name = "gst-plugin-claxon"
version = "0.9.0-alpha.1"
version = "0.9.10"
authors = ["Ruben Gonzalez <rgonzalez@fluendo.com>"]
repository = "https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs"
license = "MIT OR Apache-2.0"
@ -9,15 +9,15 @@ edition = "2021"
rust-version = "1.63"
[dependencies]
gst = { package = "gstreamer", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
gst-audio = { package = "gstreamer-audio", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
gst = { package = "gstreamer", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19.1" }
gst-audio = { package = "gstreamer-audio", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19" }
claxon = { version = "0.4" }
byte-slice-cast = "1.0"
atomic_refcell = "0.1"
once_cell = "1"
[dev-dependencies]
gst-check = { package = "gstreamer-check", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
gst-check = { package = "gstreamer-check", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19" }
[lib]
name = "gstclaxon"
@ -25,7 +25,7 @@ crate-type = ["cdylib", "rlib"]
path = "src/lib.rs"
[build-dependencies]
gst-plugin-version-helper = { path="../../version-helper" }
gst-plugin-version-helper = { version = "0.7", path="../../version-helper" }
[features]
static = []

View file

@ -1,6 +1,6 @@
[package]
name = "gst-plugin-csound"
version = "0.9.0-alpha.1"
version = "0.9.10"
authors = ["Natanael Mojica <neithanmo@gmail.com>"]
repository = "https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs"
license = "MPL-2.0"
@ -9,15 +9,15 @@ rust-version = "1.63"
description = "GStreamer Audio Filter plugin based on Csound"
[dependencies]
gst = { package = "gstreamer", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
gst-base = { package = "gstreamer-base", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
gst-audio = { package = "gstreamer-audio", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
gst = { package = "gstreamer", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19.1" }
gst-base = { package = "gstreamer-base", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19" }
gst-audio = { package = "gstreamer-audio", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19" }
csound = "0.1.8"
once_cell = "1.0"
byte-slice-cast = "1.0"
[dev-dependencies]
gst-check = { package = "gstreamer-check", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
gst-check = { package = "gstreamer-check", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19" }
[lib]
name = "gstcsound"
@ -29,7 +29,7 @@ name = "csound-effect"
path = "examples/effect_example.rs"
[build-dependencies]
gst-plugin-version-helper = { path = "../../version-helper" }
gst-plugin-version-helper = { version = "0.7", path = "../../version-helper" }
[features]
static = []

View file

@ -80,7 +80,7 @@ fn create_pipeline() -> Result<gst::Pipeline, Box<dyn Error>> {
let audio_sink = gst::parse_bin_from_description(AUDIO_SINK, true)?.upcast();
let csoundfilter = gst::ElementFactory::make("csoundfilter")
.property("csd-text", &CSD)
.property("csd-text", CSD)
.build()
.unwrap();

View file

@ -517,15 +517,15 @@ impl BaseTransformImpl for CsoundFilter {
let ichannels = csound.input_channels() as i32;
let ochannels = csound.output_channels() as i32;
for s in new_caps.make_mut().iter_mut() {
s.set("format", &gst_audio::AUDIO_FORMAT_F64.to_str());
s.set("rate", &sr);
s.set("format", gst_audio::AUDIO_FORMAT_F64.to_str());
s.set("rate", sr);
// replace the channel property with our values,
// if they are not supported, the negotiation will fail.
if direction == gst::PadDirection::Src {
s.set("channels", &ichannels);
s.set("channels", ichannels);
} else {
s.set("channels", &ochannels);
s.set("channels", ochannels);
}
// Csound does not have a concept of channel-mask
s.remove_field("channel-mask");

View file

@ -57,7 +57,7 @@ fn init() {
fn build_harness(src_caps: gst::Caps, sink_caps: gst::Caps, csd: &str) -> gst_check::Harness {
let filter = gst::ElementFactory::make("csoundfilter")
.property("csd-text", &csd)
.property("csd-text", csd)
.build()
.unwrap();
@ -260,10 +260,7 @@ fn csound_filter_underflow() {
}
assert_eq!(num_buffers, UNDERFLOW_NUM_BUFFERS / 2);
assert_eq!(
num_samples as usize,
UNDERFLOW_NUM_SAMPLES * UNDERFLOW_NUM_BUFFERS
);
assert_eq!(num_samples, UNDERFLOW_NUM_SAMPLES * UNDERFLOW_NUM_BUFFERS);
}
// Verifies that the caps negotiation is properly done, by pushing buffers whose caps

View file

@ -1,6 +1,6 @@
[package]
name = "gst-plugin-lewton"
version = "0.9.0-alpha.1"
version = "0.9.10"
authors = ["Sebastian Dröge <sebastian@centricular.com>"]
repository = "https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs"
license = "MIT OR Apache-2.0"
@ -9,15 +9,15 @@ edition = "2021"
rust-version = "1.63"
[dependencies]
gst = { package = "gstreamer", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
gst-audio = { package = "gstreamer-audio", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
gst = { package = "gstreamer", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19.1" }
gst-audio = { package = "gstreamer-audio", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19" }
lewton = { version = "0.10", default-features = false }
byte-slice-cast = "1.0"
atomic_refcell = "0.1"
once_cell = "1.0"
[dev-dependencies]
gst-check = { package = "gstreamer-check", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
gst-check = { package = "gstreamer-check", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19" }
[lib]
name = "gstlewton"
@ -25,7 +25,7 @@ crate-type = ["cdylib", "rlib"]
path = "src/lib.rs"
[build-dependencies]
gst-plugin-version-helper = { path="../../version-helper" }
gst-plugin-version-helper = { version = "0.7", path="../../version-helper" }
[features]
static = []

View file

@ -405,7 +405,7 @@ impl LewtonDec {
let outbuf = if let Some(ref reorder_map) = state.reorder_map {
let mut outbuf = self
.obj()
.allocate_output_buffer(sample_count as usize * audio_info.bpf() as usize);
.allocate_output_buffer(sample_count * audio_info.bpf() as usize);
{
// And copy the decoded data into our output buffer while reordering the channels to the
// GStreamer channel order

View file

@ -1,6 +1,6 @@
[package]
name = "gst-plugin-spotify"
version = "0.9.0-alpha.1"
version = "0.9.10"
authors = ["Guillaume Desmottes <guillaume@desmottes.be>"]
repository = "https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs"
license = "MPL-2.0"
@ -9,8 +9,8 @@ edition = "2021"
rust-version = "1.63"
[dependencies]
gst = { package = "gstreamer", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
gst-base = { package = "gstreamer-base", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
gst = { package = "gstreamer", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19.1" }
gst-base = { package = "gstreamer-base", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19" }
once_cell = "1.0"
librespot = { version = "0.4", default-features = false }
tokio = "1.0"
@ -24,7 +24,7 @@ crate-type = ["cdylib", "rlib"]
path = "src/lib.rs"
[build-dependencies]
gst-plugin-version-helper = { path="../../version-helper" }
gst-plugin-version-helper = { version = "0.7", path="../../version-helper" }
[features]
static = []

View file

@ -6,9 +6,10 @@
//
// SPDX-License-Identifier: MPL-2.0
use std::sync::{mpsc, Arc, Mutex};
use std::sync::{mpsc, Arc, Mutex, MutexGuard};
use anyhow::bail;
use futures::future::{AbortHandle, Abortable, Aborted};
use once_cell::sync::Lazy;
use tokio::{runtime, task::JoinHandle};
@ -30,6 +31,8 @@ use librespot::playback::{
player::{Player, PlayerEvent},
};
use super::Bitrate;
static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
gst::DebugCategory::new(
"spotifyaudiosrc",
@ -70,14 +73,21 @@ struct Settings {
cache_files: String,
cache_max_size: u64,
track: String,
bitrate: Bitrate,
}
#[derive(Default)]
pub struct SpotifyAudioSrc {
setup_thread: Mutex<Option<SetupThread>>,
state: Arc<Mutex<Option<State>>>,
settings: Mutex<Settings>,
}
struct SetupThread {
thread_handle: std::thread::JoinHandle<Result<anyhow::Result<()>, Aborted>>,
abort_handle: AbortHandle,
}
#[glib::object_subclass]
impl ObjectSubclass for SpotifyAudioSrc {
const NAME: &'static str = "GstSpotifyAudioSrc";
@ -125,6 +135,11 @@ impl ObjectImpl for SpotifyAudioSrc {
.default_value(Some(""))
.mutable_ready()
.build(),
glib::ParamSpecEnum::builder("bitrate", Bitrate::default())
.nick("Spotify bitrate")
.blurb("Spotify audio bitrate in kbit/s")
.mutable_ready()
.build()
]
});
@ -157,6 +172,10 @@ impl ObjectImpl for SpotifyAudioSrc {
let mut settings = self.settings.lock().unwrap();
settings.track = value.get().expect("type checked upstream");
}
"bitrate" => {
let mut settings = self.settings.lock().unwrap();
settings.bitrate = value.get().expect("type checked upstream");
}
_ => unimplemented!(),
}
}
@ -187,6 +206,10 @@ impl ObjectImpl for SpotifyAudioSrc {
let settings = self.settings.lock().unwrap();
settings.track.to_value()
}
"bitrate" => {
let settings = self.settings.lock().unwrap();
settings.bitrate.to_value()
}
_ => unimplemented!(),
}
}
@ -237,17 +260,22 @@ impl BaseSrcImpl for SpotifyAudioSrc {
}
}
if let Err(err) = RUNTIME.block_on(async move { self.setup().await }) {
let details = format!("{:?}", err);
gst::error!(CAT, imp: self, "failed to start: {}", details);
gst::element_imp_error!(self, gst::ResourceError::Settings, [&details]);
return Err(gst::error_msg!(gst::ResourceError::Settings, [&details]));
{
let setup_thread = self.setup_thread.lock().unwrap();
if setup_thread.is_some() {
// already starting
return Ok(());
}
self.start_setup(setup_thread);
}
Ok(())
}
fn stop(&self) -> Result<(), gst::ErrorMessage> {
// stop the setup if it's not completed yet
self.cancel_setup();
if let Some(state) = self.state.lock().unwrap().take() {
gst::debug!(CAT, imp: self, "stopping");
state.player.stop();
@ -258,6 +286,12 @@ impl BaseSrcImpl for SpotifyAudioSrc {
Ok(())
}
fn unlock(&self) -> Result<(), gst::ErrorMessage> {
self.cancel_setup();
self.parent_unlock()
}
}
impl PushSrcImpl for SpotifyAudioSrc {
@ -265,6 +299,41 @@ impl PushSrcImpl for SpotifyAudioSrc {
&self,
_buffer: Option<&mut gst::BufferRef>,
) -> Result<CreateSuccess, gst::FlowError> {
let state_set = {
let state = self.state.lock().unwrap();
state.is_some()
};
if !state_set {
let setup_thread = self.setup_thread.lock().unwrap();
if setup_thread.is_none() {
// unlock() could potentially cancel the setup, and create() can be called after unlock() without going through start() again.
self.start_setup(setup_thread);
}
}
{
// wait for the setup to be completed
let mut setup_thread = self.setup_thread.lock().unwrap();
if let Some(setup) = setup_thread.take() {
let res = setup.thread_handle.join().unwrap();
match res {
Err(_aborted) => {
gst::debug!(CAT, imp: self, "setup has been cancelled");
return Err(gst::FlowError::Flushing);
}
Ok(Err(err)) => {
let details = format!("{:?}", err);
gst::error!(CAT, imp: self, "failed to start: {}", details);
gst::element_imp_error!(self, gst::ResourceError::Settings, [&details]);
return Err(gst::FlowError::Error);
}
Ok(Ok(_)) => {}
}
}
}
let state = self.state.lock().unwrap();
let state = state.as_ref().unwrap();
@ -290,112 +359,6 @@ impl PushSrcImpl for SpotifyAudioSrc {
}
}
impl SpotifyAudioSrc {
async fn setup(&self) -> anyhow::Result<()> {
let (credentials, cache, track) = {
let settings = self.settings.lock().unwrap();
let credentials_cache = if settings.cache_credentials.is_empty() {
None
} else {
Some(&settings.cache_credentials)
};
let files_cache = if settings.cache_files.is_empty() {
None
} else {
Some(&settings.cache_files)
};
let max_size = if settings.cache_max_size != 0 {
Some(settings.cache_max_size)
} else {
None
};
let cache = Cache::new(credentials_cache, None, files_cache, max_size)?;
let credentials = match cache.credentials() {
Some(cached_cred) => {
gst::debug!(CAT, imp: self, "reuse credentials from cache",);
cached_cred
}
None => {
gst::debug!(CAT, imp: self, "credentials not in cache",);
if settings.username.is_empty() {
bail!("username is not set and credentials are not in cache");
}
if settings.password.is_empty() {
bail!("password is not set and credentials are not in cache");
}
let cred = Credentials::with_password(&settings.username, &settings.password);
cache.save_credentials(&cred);
cred
}
};
if settings.track.is_empty() {
bail!("track is not set")
}
(credentials, cache, settings.track.clone())
};
let state = self.state.clone();
let (session, _credentials) =
Session::connect(SessionConfig::default(), credentials, Some(cache), false).await?;
let player_config = PlayerConfig {
passthrough: true,
..Default::default()
};
// use a sync channel to prevent buffering the whole track inside the channel
let (sender, receiver) = mpsc::sync_channel(2);
let sender_clone = sender.clone();
let (mut player, mut player_event_channel) =
Player::new(player_config, session, Box::new(NoOpVolume), || {
Box::new(BufferSink { sender })
});
let track = match SpotifyId::from_uri(&track) {
Ok(track) => track,
Err(_) => bail!("Failed to create Spotify URI from track"),
};
player.load(track, true, 0);
let player_channel_handle = RUNTIME.spawn(async move {
let sender = sender_clone;
while let Some(event) = player_event_channel.recv().await {
match event {
PlayerEvent::EndOfTrack { .. } => {
let _ = sender.send(Message::Eos);
}
PlayerEvent::Unavailable { .. } => {
let _ = sender.send(Message::Unavailable);
}
_ => {}
}
}
});
let mut state = state.lock().unwrap();
state.replace(State {
player,
receiver,
player_channel_handle,
});
Ok(())
}
}
struct BufferSink {
sender: mpsc::SyncSender<Message>,
}
@ -456,3 +419,148 @@ impl URIHandlerImpl for SpotifyAudioSrc {
Ok(())
}
}
impl SpotifyAudioSrc {
fn start_setup(&self, mut setup_thread: MutexGuard<Option<SetupThread>>) {
let self_ = self.to_owned();
// run the runtime from another thread to prevent the "start a runtime from within a runtime" panic
// when the plugin is statically linked.
let (abort_handle, abort_registration) = AbortHandle::new_pair();
let thread_handle = std::thread::spawn(move || {
RUNTIME.block_on(async move {
let future = Abortable::new(self_.setup(), abort_registration);
future.await
})
});
setup_thread.replace(SetupThread {
thread_handle,
abort_handle,
});
}
async fn setup(&self) -> anyhow::Result<()> {
{
let state = self.state.lock().unwrap();
if state.is_some() {
// already setup
return Ok(());
}
}
let (credentials, cache, track, bitrate) = {
let settings = self.settings.lock().unwrap();
let credentials_cache = if settings.cache_credentials.is_empty() {
None
} else {
Some(&settings.cache_credentials)
};
let files_cache = if settings.cache_files.is_empty() {
None
} else {
Some(&settings.cache_files)
};
let max_size = if settings.cache_max_size != 0 {
Some(settings.cache_max_size)
} else {
None
};
let cache = Cache::new(credentials_cache, None, files_cache, max_size)?;
let credentials = match cache.credentials() {
Some(cached_cred) => {
gst::debug!(CAT, imp: self, "reuse credentials from cache",);
cached_cred
}
None => {
gst::debug!(CAT, imp: self, "credentials not in cache",);
if settings.username.is_empty() {
bail!("username is not set and credentials are not in cache");
}
if settings.password.is_empty() {
bail!("password is not set and credentials are not in cache");
}
let cred = Credentials::with_password(&settings.username, &settings.password);
cache.save_credentials(&cred);
cred
}
};
if settings.track.is_empty() {
bail!("track is not set")
}
let bitrate = settings.bitrate.into();
gst::debug!(CAT, imp: self, "Requesting bitrate {:?}", bitrate);
(credentials, cache, settings.track.clone(), bitrate)
};
let (session, _credentials) =
Session::connect(SessionConfig::default(), credentials, Some(cache), false).await?;
let player_config = PlayerConfig {
passthrough: true,
bitrate,
..Default::default()
};
// use a sync channel to prevent buffering the whole track inside the channel
let (sender, receiver) = mpsc::sync_channel(2);
let sender_clone = sender.clone();
let (mut player, mut player_event_channel) =
Player::new(player_config, session, Box::new(NoOpVolume), || {
Box::new(BufferSink { sender })
});
let track = match SpotifyId::from_uri(&track) {
Ok(track) => track,
Err(_) => bail!("Failed to create Spotify URI from track"),
};
player.load(track, true, 0);
let player_channel_handle = RUNTIME.spawn(async move {
let sender = sender_clone;
while let Some(event) = player_event_channel.recv().await {
match event {
PlayerEvent::EndOfTrack { .. } => {
let _ = sender.send(Message::Eos);
}
PlayerEvent::Unavailable { .. } => {
let _ = sender.send(Message::Unavailable);
}
_ => {}
}
}
});
let mut state = self.state.lock().unwrap();
state.replace(State {
player,
receiver,
player_channel_handle,
});
Ok(())
}
fn cancel_setup(&self) {
let mut setup_thread = self.setup_thread.lock().unwrap();
if let Some(setup) = setup_thread.take() {
setup.abort_handle.abort();
}
}
}

View file

@ -11,11 +11,42 @@ use gst::prelude::*;
mod imp;
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Clone, Copy, glib::Enum)]
#[repr(u32)]
#[enum_type(name = "GstRsSpotifyBitrate")]
enum Bitrate {
#[enum_value(name = "96 kbit/s", nick = "96")]
B96,
#[enum_value(name = "160 kbit/s", nick = "160")]
B160,
#[enum_value(name = "320 kbit/s", nick = "320")]
B320,
}
impl Default for Bitrate {
fn default() -> Self {
Self::B160
}
}
impl From<Bitrate> for librespot::playback::config::Bitrate {
fn from(value: Bitrate) -> Self {
match value {
Bitrate::B96 => Self::Bitrate96,
Bitrate::B160 => Self::Bitrate160,
Bitrate::B320 => Self::Bitrate320,
}
}
}
glib::wrapper! {
pub struct SpotifyAudioSrc(ObjectSubclass<imp::SpotifyAudioSrc>) @extends gst_base::PushSrc, gst_base::BaseSrc, gst::Element, gst::Object, @implements gst::URIHandler;
}
pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
#[cfg(feature = "doc")]
Bitrate::static_type().mark_as_plugin_api(gst::PluginAPIFlags::empty());
gst::Element::register(
Some(plugin),
"spotifyaudiosrc",

View file

@ -16,13 +16,15 @@ PARSER.add_argument('build_dir', type=P)
PARSER.add_argument('src_dir', type=P)
PARSER.add_argument('root_dir', type=P)
PARSER.add_argument('target', choices=['release', 'debug'])
PARSER.add_argument('include')
PARSER.add_argument('extra_env')
PARSER.add_argument('prefix', type=P)
PARSER.add_argument('libdir', type=P)
PARSER.add_argument('--version', default=None)
PARSER.add_argument('--bin', default=None, type=P)
PARSER.add_argument('--exts', nargs="+", default=[])
PARSER.add_argument('--features', nargs="+", default=[])
PARSER.add_argument('--packages', nargs="+", default=[])
PARSER.add_argument('--examples', nargs="+", default=[])
PARSER.add_argument('--lib-suffixes', nargs="+", default=[])
PARSER.add_argument('--exe-suffix')
PARSER.add_argument('--depfile')
PARSER.add_argument('--disable-doc', action="store_true", default=False)
@ -33,7 +35,19 @@ def generate_depfile_for(fpath):
with open(f"{file_stem}.d", 'r') as depfile:
for l in depfile.readlines():
if l.startswith(str(file_stem)):
output, srcs = l.split(":", maxsplit=2)
# We can't blindly split on `:` because on Windows that's part
# of the drive letter. Lucky for us, the format of the dep file
# is one of:
#
# /path/to/output: /path/to/src1 /path/to/src2
# /path/to/output:
#
# So we parse these two cases specifically
if l.endswith(':'):
output = l[:-1]
srcs = ''
else:
output, srcs = l.split(": ", maxsplit=2)
all_deps = []
for src in srcs.split(" "):
@ -53,33 +67,30 @@ def generate_depfile_for(fpath):
if __name__ == "__main__":
opts = PARSER.parse_args()
logfile = open(opts.root_dir / 'meson-logs' /
f'{opts.src_dir.name}-cargo-wrapper.log', 'w')
logdir = opts.root_dir / 'meson-logs'
logfile_path = logdir / f'{opts.src_dir.name}-cargo-wrapper.log'
logfile = open(logfile_path, mode='w', buffering=1)
print(opts, file=logfile)
cargo_target_dir = opts.build_dir / 'target'
env = os.environ.copy()
env['CARGO_TARGET_DIR'] = str(cargo_target_dir)
pkg_config_path = env.get('PKG_CONFIG_PATH', '').split(':')
if 'PKG_CONFIG_PATH' in env:
pkg_config_path = env['PKG_CONFIG_PATH'].split(os.pathsep)
else:
pkg_config_path = []
pkg_config_path.append(str(opts.root_dir / 'meson-uninstalled'))
env['PKG_CONFIG_PATH'] = ':'.join(pkg_config_path)
if opts.extra_env:
for e in opts.extra_env.split(','):
k, v = e.split(':')
env[k] = v
env['PKG_CONFIG_PATH'] = os.pathsep.join(pkg_config_path)
features = opts.features
if opts.command == 'build':
cargo_cmd = ['cargo']
if opts.bin:
if opts.bin or opts.examples:
cargo_cmd += ['build']
else:
cargo_cmd += ['cbuild']
if not opts.disable_doc:
cargo_cmd += ['--features', "doc"]
features += ['doc']
if opts.target == 'release':
cargo_cmd.append('--release')
elif opts.command == 'test':
@ -89,40 +100,41 @@ if __name__ == "__main__":
print("Unknown command:", opts.command, file=logfile)
sys.exit(1)
cwd = None
if not opts.bin:
cargo_cmd.extend(['--manifest-path', opts.src_dir / 'Cargo.toml'])
cargo_cmd.extend(['--prefix', opts.prefix, '--libdir',
opts.prefix / opts.libdir])
for p in opts.include.split(','):
cargo_cmd.extend(['-p', p])
else:
if features:
cargo_cmd += ['--features', ','.join(features)]
cargo_cmd += ['--target-dir', cargo_target_dir]
cargo_cmd += ['--manifest-path', opts.src_dir / 'Cargo.toml']
if opts.bin:
cargo_cmd.extend(['--bin', opts.bin.name])
cwd = opts.src_dir
else:
if not opts.examples:
cargo_cmd.extend(['--prefix', opts.prefix, '--libdir',
opts.prefix / opts.libdir])
for p in opts.packages:
cargo_cmd.extend(['-p', p])
for e in opts.examples:
cargo_cmd.extend(['--example', e])
def run(cargo_cmd, env, cwd=cwd):
def run(cargo_cmd, env):
print(cargo_cmd, env, file=logfile)
try:
subprocess.run(cargo_cmd, env=env, check=True, cwd=cwd)
subprocess.run(cargo_cmd, env=env, cwd=opts.src_dir, check=True)
except subprocess.SubprocessError:
sys.exit(1)
run(cargo_cmd, env, cwd)
run(cargo_cmd, env)
if opts.command == 'build':
target_dir = cargo_target_dir / '**' / opts.target
if opts.bin:
if opts.exts[0]:
ext = f'.{opts.exts[0]}'
else:
ext = ''
exe = glob.glob(str(target_dir / opts.bin) + ext, recursive=True)[0]
exe = glob.glob(str(target_dir / opts.bin) + opts.exe_suffix, recursive=True)[0]
shutil.copy2(exe, opts.build_dir)
depfile_content = generate_depfile_for(P(exe))
else:
# Copy so files to build dir
depfile_content = ""
for ext in opts.exts:
for f in glob.glob(str(target_dir / f'*.{ext}'), recursive=True):
for suffix in opts.lib_suffixes:
for f in glob.glob(str(target_dir / f'*.{suffix}'), recursive=True):
libfile = P(f)
depfile_content += generate_depfile_for(libfile)
@ -137,6 +149,12 @@ if __name__ == "__main__":
print(f"Copying {copied_file}", file=logfile)
shutil.copy2(f, opts.build_dir)
# Copy examples to builddir
for example in opts.examples:
example_glob = str(target_dir / 'examples' / example) + opts.exe_suffix
exe = glob.glob(example_glob, recursive=True)[0]
shutil.copy2(exe, opts.build_dir)
depfile_content += generate_depfile_for(P(exe))
with open(opts.depfile, 'w') as depfile:
depfile.write(depfile_content)

View file

@ -7,7 +7,8 @@ import os
from utils import iterate_plugins
# the csound version used on ci does not ship a .pc file
IGNORE = ['csound']
# threadshare we skip in meson static build as well
IGNORE = ['csound', 'threadshare', 'gtk4']
outdir = sys.argv[1]

View file

@ -3,4 +3,4 @@ source ./ci/env.sh
set -e
export CARGO_HOME='/usr/local/cargo'
cargo install cargo-c --version 0.9.12+cargo-0.64
cargo install cargo-c --version 0.9.14+cargo-0.66

View file

@ -17,11 +17,18 @@ function Run-Tests {
param (
$Features
)
$local_exclude = $exclude_crates;
# In this case the plugin will pull x11/wayland features
# which will fail to build on windows.
if (($Features -eq '--all-features') -or ($Features -eq '')) {
$local_exclude += @("--exclude", "gst-plugin-gtk4")
}
Write-Host "Features: $Features"
Write-Host "Exclude string: $exclude_crates"
Write-Host "Exclude string: $local_exclude"
cargo build --color=always --workspace $exclude_crates --all-targets $Features
cargo build --color=always --workspace $local_exclude --all-targets $Features
if (!$?) {
Write-Host "Build failed"
@ -29,7 +36,7 @@ function Run-Tests {
}
$env:G_DEBUG="fatal_warnings"
cargo test --no-fail-fast --color=always --workspace $exclude_crates --all-targets $Features
cargo test --no-fail-fast --color=always --workspace $local_exclude --all-targets $Features
if (!$?) {
Write-Host "Tests failed"

View file

@ -9,10 +9,13 @@ ignore = [
"RUSTSEC-2021-0059",
"RUSTSEC-2021-0060",
"RUSTSEC-2021-0061",
"RUSTSEC-2021-0145",
# https://github.com/chronotope/chrono/issues/499
"RUSTSEC-2020-0071",
# sodiumoxide is deprecated
"RUSTSEC-2021-0137",
# https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/issues/256
"RUSTSEC-2022-0048",
]
[licenses]
@ -55,12 +58,6 @@ wildcards = "allow"
name = "time"
version = "0.1"
# ignore duplicated textwrap dependency because clap depends on an old version
# https://github.com/clap-rs/clap/pull/1994
[[bans.skip]]
name = "textwrap"
version = "0.11"
# ignore duplicated rustc_version dependency because rav1e depends on an old version
[[bans.skip]]
name = "rustc_version"
@ -69,31 +66,13 @@ version = "0.3"
name = "semver"
version = "0.11"
# ignore duplicated system-deps dependency because dav1d depends on an old version
[[bans.skip]]
name = "system-deps"
version = "3"
[[bans.skip]]
name = "version-compare"
version = "0.0"
[[bans.skip]]
name = "cfg-expr"
version = "0.7"
# ignore duplicated crc dependency because ffv1 depends on an old version
# https://github.com/rust-av/ffv1/issues/21
[[bans.skip]]
name = "crc"
version = "1.8"
# ignore duplicated heck dependency because various crates depend on an old version
[[bans.skip]]
name = "heck"
version = "0.3"
# ignore duplicated sha-1/digest/block-buffer dependencies because librespot depends on an old version
# Ignore various duplicated dependencies because librespot depends on an old versions
[[bans.skip]]
name = "block-buffer"
version = "0.9"
@ -103,6 +82,12 @@ version = "0.9"
[[bans.skip]]
name = "sha-1"
version = "0.9"
[[bans.skip]]
name = "env_logger"
version = "0.9"
[[bans.skip]]
name = "hmac"
version = "0.11"
# ignore duplicated wasi dependency because various crates depends on an old version
[[bans.skip]]
@ -114,6 +99,48 @@ version = "0.10"
name = "spin"
version = "0.5"
# cookie_store depends on older idna
# https://github.com/pfernie/cookie_store/commit/b9c710f45550c5c8997f18a83e6fcc5998cf1726
[[bans.skip]]
name = "idna"
version = "0.2"
# image depends on older gif
# https://github.com/image-rs/image/pull/1826
[[bans.skip]]
name = "gif"
version = "0.11"
# field-offset and nix depend on an older memoffset
# https://github.com/Diggsey/rust-field-offset/pull/23
# https://github.com/nix-rust/nix/pull/1885
[[bans.skip]]
name = "memoffset"
version = "0.6"
# Various crates depend on an older version of hermit-abi
[[bans.skip]]
name = "hermit-abi"
version = "0.1"
[[bans.skip]]
name = "hermit-abi"
version = "0.2"
# Various crates depend on an older version of base64
[[bans.skip]]
name = "base64"
version = "0.13"
# Various crates depend on an older version of windows-sys
[[bans.skip]]
name = "windows-sys"
version = "0.42"
# Various crates depend on an older version of socket2
[[bans.skip]]
name = "socket2"
version = "0.4"
[sources]
unknown-registry = "deny"
unknown-git = "deny"

View file

@ -9,7 +9,7 @@ if meson.is_cross_build()
subdir_done()
endif
if static_build
if default_library == 'static'
if get_option('doc').enabled()
error('Documentation enabled but not supported when building statically.')
endif
@ -99,9 +99,17 @@ foreach plugin_name: list_plugin_res.stdout().split(':')
gst_index: 'plugins/index.md',
include_paths: join_paths(meson.current_source_dir(), '..'),
gst_smart_index: true,
gst_c_source_filters: [
'../target/*/*.rs',
'../target/*/*/*.rs',
'../target/*/*/*/*.rs',
'../target/*/*/*/*/*.rs',
'../target/*/*/*/*/*/*.rs',
],
gst_c_sources: [
'../*/*/*/*.rs',
'../*/*/*/*/*.rs',
'../*/*/*/*/*/*.rs',
],
dependencies: [gst_dep],
gst_order_generated_subpages: true,

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[package]
name = "gst-plugin-file"
version = "0.9.0-alpha.1"
version = "0.9.10"
authors = ["Sebastian Dröge <sebastian@centricular.com>"]
repository = "https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs"
license = "MIT OR Apache-2.0"
@ -10,8 +10,8 @@ rust-version = "1.63"
[dependencies]
url = "2"
gst = { package = "gstreamer", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
gst-base = { package = "gstreamer-base", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
gst = { package = "gstreamer", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19.1" }
gst-base = { package = "gstreamer-base", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19" }
once_cell = "1.0"
[lib]
@ -20,7 +20,7 @@ crate-type = ["cdylib", "rlib"]
path = "src/lib.rs"
[build-dependencies]
gst-plugin-version-helper = { path="../../version-helper" }
gst-plugin-version-helper = { version = "0.7", path="../../version-helper" }
[features]
static = []

View file

@ -74,7 +74,7 @@ impl FileLocation {
.parent()
.expect("FileSink::set_location `location` with filename but without a parent")
.to_owned();
if parent_dir.is_relative() && parent_dir.components().next() == None {
if parent_dir.is_relative() && parent_dir.components().next().is_none() {
// `location` only contains the filename
// need to specify "." for `canonicalize` to resolve the actual path
parent_dir = PathBuf::from(".");

View file

@ -1,6 +1,6 @@
[package]
name = "gst-plugin-sodium"
version = "0.9.0-alpha.1"
version = "0.9.10"
authors = ["Jordan Petridis <jordan@centricular.com>"]
repository = "https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs"
description = "GStreamer plugin for libsodium-based file encryption and decryption"
@ -9,8 +9,8 @@ edition = "2021"
rust-version = "1.63"
[dependencies]
gst = { package="gstreamer", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
gst-base = { package = "gstreamer-base", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
gst = { package="gstreamer", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19" }
gst-base = { package = "gstreamer-base", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19" }
sodiumoxide = "0.2.1"
once_cell = "1.3.0"
hex = "0.4"
@ -27,10 +27,14 @@ rand = "0.8"
[dev-dependencies.gst-check]
git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs"
branch = "0.19"
version = "0.19"
package="gstreamer-check"
[dev-dependencies.gst-app]
git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs"
branch = "0.19"
version = "0.19"
package="gstreamer-app"
[lib]
@ -54,7 +58,7 @@ path = "examples/decrypt_example.rs"
required-features = ["serde", "serde_json", "clap"]
[build-dependencies]
gst-plugin-version-helper = { path="../../version-helper" }
gst-plugin-version-helper = { version = "0.7", path="../../version-helper" }
[features]
static = []

View file

@ -53,7 +53,7 @@ struct Keys {
impl Keys {
fn from_file(file: &Path) -> Result<Self, Box<dyn Error>> {
let f = File::open(&file)?;
let f = File::open(file)?;
serde_json::from_reader(f).map_err(From::from)
}
}

View file

@ -57,7 +57,7 @@ struct Keys {
impl Keys {
fn from_file(file: &Path) -> Result<Self, Box<dyn Error>> {
let f = File::open(&file)?;
let f = File::open(file)?;
serde_json::from_reader(f).map_err(From::from)
}
}

View file

@ -498,7 +498,7 @@ impl Decrypter {
gst::debug!(CAT, obj: pad, "Requested offset: {}", offset);
gst::debug!(CAT, obj: pad, "Requested size: {}", requested_size);
let chunk_index = offset as u64 / block_size as u64;
let chunk_index = offset / block_size as u64;
gst::debug!(CAT, obj: pad, "Stream Block index: {}", chunk_index);
let pull_offset = offset - (chunk_index * block_size as u64);

View file

@ -75,7 +75,7 @@ fn test_pipeline() {
};
let filesrc = gst::ElementFactory::make("filesrc")
.property("location", &input_path.to_str().unwrap())
.property("location", input_path.to_str().unwrap())
.build()
.unwrap();

View file

@ -1,6 +1,6 @@
[package]
name = "gst-plugin-threadshare"
version = "0.9.0-alpha.1"
version = "0.9.10"
authors = ["Sebastian Dröge <sebastian@centricular.com>"]
license = "LGPL-2.1-or-later"
description = "GStreamer Threadshare Plugin"
@ -10,20 +10,21 @@ rust-version = "1.63"
[dependencies]
async-task = "4.3.0"
concurrent-queue = "1.2.2"
concurrent-queue = "2"
flume = "0.10.13"
futures = "0.3.21"
libc = "0.2"
gio = { git = "https://github.com/gtk-rs/gtk-rs-core" }
gst = { package = "gstreamer", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
gst-net = { package = "gstreamer-net", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
gst-rtp = { package = "gstreamer-rtp", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
gio = { git = "https://github.com/gtk-rs/gtk-rs-core", branch = "0.16", version = "0.16" }
gst = { package = "gstreamer", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19.1" }
gst-audio = { package = "gstreamer-audio", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19" }
gst-net = { package = "gstreamer-net", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19" }
gst-rtp = { package = "gstreamer-rtp", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19" }
once_cell = "1"
pin-project-lite = "0.2.0"
polling = "2.2.0"
rand = "0.8"
slab = "0.4.7"
socket2 = {features = ["all"], version = "0.4"}
socket2 = {features = ["all"], version = "0.5"}
waker-fn = "1.1"
# Used by examples
@ -33,8 +34,8 @@ clap = { version = "4", features = ["derive"], optional = true }
winapi = { version = "0.3.9", features = ["winsock2", "processthreadsapi"] }
[dev-dependencies]
gst-check = { package = "gstreamer-check", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
gst-app = { package = "gstreamer-app", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
gst-check = { package = "gstreamer-check", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19" }
gst-app = { package = "gstreamer-app", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19" }
[lib]
name = "gstthreadshare"
@ -42,7 +43,7 @@ crate-type = ["cdylib", "rlib"]
path = "src/lib.rs"
[[example]]
name = "benchmark"
name = "ts-benchmark"
path = "examples/benchmark.rs"
[[example]]
@ -58,7 +59,7 @@ name = "ts-standalone"
path = "examples/standalone/main.rs"
[build-dependencies]
gst-plugin-version-helper = { path="../../version-helper" }
gst-plugin-version-helper = { version = "0.7", path="../../version-helper" }
cc = "1.0.38"
pkg-config = "0.3.15"

View file

@ -63,7 +63,7 @@ fn main() {
};
let is_rtp = args.len() > 6 && (args[6] == "rtp");
let rtp_caps = gst::Caps::builder("audio/x-rtp")
let rtp_caps = gst::Caps::builder("application/x-rtp")
.field("media", "audio")
.field("payload", 8i32)
.field("clock-rate", 8000)
@ -97,7 +97,7 @@ fn main() {
"udpsrc" => {
let source = gst::ElementFactory::make("udpsrc")
.name(format!("source-{}", i).as_str())
.property("port", 40000i32 + i as i32)
.property("port", 5004i32 + i as i32)
.property("retrieve-sender-address", false)
.build()
.unwrap();
@ -108,7 +108,7 @@ fn main() {
let context = build_context();
let source = gst::ElementFactory::make("ts-udpsrc")
.name(format!("source-{}", i).as_str())
.property("port", 40000i32 + i as i32)
.property("port", 5004i32 + i as i32)
.property("context", &context)
.property("context-wait", wait)
.build()
@ -158,7 +158,7 @@ fn main() {
let context = build_context();
let source = gst::ElementFactory::make("ts-tonesrc")
.name(format!("source-{}", i).as_str())
.property("samples-per-buffer", (wait as u32) * 8000 / 1000)
.property("samples-per-buffer", wait * 8000 / 1000)
.property("context", &context)
.property("context-wait", wait)
.build()
@ -184,22 +184,7 @@ fn main() {
pipeline.add_many(elements).unwrap();
gst::Element::link_many(elements).unwrap();
} else {
let queue = if let Some(context) = context {
let queue = gst::ElementFactory::make("ts-queue")
.name(format!("queue-{}", i).as_str())
.property("context", &context)
.property("context-wait", wait)
.build()
.unwrap();
queue
} else {
gst::ElementFactory::make("queue")
.name(format!("queue-{}", i).as_str())
.build()
.unwrap()
};
let elements = &[&source, &queue, &sink];
let elements = &[&source, &sink];
pipeline.add_many(elements).unwrap();
gst::Element::link_many(elements).unwrap();
}
@ -268,14 +253,14 @@ fn main() {
let elapsed = init.elapsed();
gst::info!(
CAT,
"{:>6.2} / s / stream",
"Thrpt: {:>6.2}",
total_count * 1_000.0 / elapsed.as_millis() as f32
);
#[cfg(feature = "tuning")]
gst::info!(
CAT,
"{:>6.2}% parked",
"Parked: {:>6.2}%",
(ctx_0.parked_duration() - parked_init).as_nanos() as f32 * 100.0
/ elapsed.as_nanos() as f32
);

View file

@ -0,0 +1,66 @@
use super::super::CAT;
use clap::Parser;
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum)]
pub enum Sink {
/// Item handling in PadHandler with async Mutex
AsyncMutex,
/// Item handling in PadHandler with sync Mutex
SyncMutex,
/// Item handling in runtime::Task
Task,
}
impl Sink {
pub fn element_name(self) -> &'static str {
use super::super::sink;
use Sink::*;
match self {
AsyncMutex => sink::ASYNC_MUTEX_ELEMENT_NAME,
SyncMutex => sink::SYNC_MUTEX_ELEMENT_NAME,
Task => sink::TASK_ELEMENT_NAME,
}
}
}
#[derive(Parser, Debug)]
#[clap(version)]
#[clap(
about = "Standalone pipeline threadshare runtime test. Use `GST_DEBUG=ts-standalone*:4` for stats"
)]
pub struct Args {
/// Parallel streams to process.
#[clap(short, long, default_value_t = 5000)]
pub streams: u32,
/// Threadshare groups.
#[clap(short, long, default_value_t = 2)]
pub groups: u32,
/// Threadshare Context wait in ms (max throttling duration).
#[clap(short, long, default_value_t = 20)]
pub wait: u32,
/// Buffer push period in ms.
#[clap(short, long, default_value_t = 20)]
pub push_period: u32,
/// Number of buffers per stream to output before sending EOS (-1 = unlimited).
#[clap(short, long, default_value_t = 5000)]
pub num_buffers: i32,
/// The Sink variant to use.
#[clap(long, value_enum, default_value_t = Sink::SyncMutex)]
pub sink: Sink,
/// Disables statistics logging.
#[clap(short, long)]
pub disable_stats_log: bool,
}
pub fn args() -> Args {
let args = Args::parse();
gst::info!(CAT, "{:?}", args);
args
}

View file

@ -0,0 +1,47 @@
use super::super::CAT;
#[derive(Copy, Clone, Debug)]
pub struct SyncMutexSink;
impl SyncMutexSink {
pub fn element_name(self) -> &'static str {
super::super::sink::SYNC_MUTEX_ELEMENT_NAME
}
}
#[derive(Debug)]
pub struct Args {
pub streams: u32,
pub groups: u32,
pub wait: u32,
pub push_period: u32,
pub num_buffers: i32,
pub sink: SyncMutexSink,
pub disable_stats_log: bool,
}
impl Default for Args {
fn default() -> Self {
Args {
streams: 5000,
groups: 2,
wait: 20,
push_period: 20,
num_buffers: 5000,
sink: SyncMutexSink,
disable_stats_log: false,
}
}
}
pub fn args() -> Args {
if std::env::args().len() > 1 {
gst::warning!(CAT, "Ignoring command line arguments");
gst::warning!(CAT, "Build with `--features=clap`");
}
let args = Args::default();
gst::warning!(CAT, "{:?}", args);
args
}

View file

@ -0,0 +1,9 @@
#[cfg(not(feature = "clap"))]
mod default_args;
#[cfg(not(feature = "clap"))]
pub use default_args::*;
#[cfg(feature = "clap")]
mod clap_args;
#[cfg(feature = "clap")]
pub use clap_args::*;

View file

@ -0,0 +1,19 @@
macro_rules! debug_or_trace {
($cat:expr, $raise_log_level:expr, $qual:ident: $obj:expr, $rest:tt $(,)?) => {
if $raise_log_level {
gst::debug!($cat, $qual: $obj, $rest);
} else {
gst::trace!($cat, $qual: $obj, $rest);
}
};
}
macro_rules! log_or_trace {
($cat:expr, $raise_log_level:expr, $qual:ident: $obj:expr, $rest:tt $(,)?) => {
if $raise_log_level {
gst::log!($cat, $qual: $obj, $rest);
} else {
gst::trace!($cat, $qual: $obj, $rest);
}
};
}

View file

@ -1,12 +1,21 @@
use gst::glib;
use once_cell::sync::Lazy;
mod args;
use args::*;
#[macro_use]
mod macros;
mod sink;
mod src;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
gst::DebugCategory::new(
"ts-standalone-test-main",
"ts-standalone-main",
gst::DebugColorFlags::empty(),
Some("Thread-sharing standalone test main"),
)
@ -14,7 +23,9 @@ static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
src::register(plugin)?;
sink::register(plugin)?;
sink::async_mutex::register(plugin)?;
sink::sync_mutex::register(plugin)?;
sink::task::register(plugin)?;
Ok(())
}
@ -31,91 +42,6 @@ gst::plugin_define!(
env!("BUILD_REL_DATE")
);
#[cfg(feature = "clap")]
use clap::Parser;
#[cfg(feature = "clap")]
#[derive(Parser, Debug)]
#[clap(version)]
#[clap(
about = "Standalone pipeline threadshare runtime test. Use `GST_DEBUG=ts-standalone*:4` for stats"
)]
struct Args {
/// Parallel streams to process.
#[clap(short, long, default_value_t = 5000)]
streams: u32,
/// Threadshare groups.
#[clap(short, long, default_value_t = 2)]
groups: u32,
/// Threadshare Context wait in ms (max throttling duration).
#[clap(short, long, default_value_t = 20)]
wait: u32,
/// Buffer push period in ms.
#[clap(short, long, default_value_t = 20)]
push_period: u32,
/// Number of buffers per stream to output before sending EOS (-1 = unlimited).
#[clap(short, long, default_value_t = 5000)]
num_buffers: i32,
/// Disables statistics logging.
#[clap(short, long)]
disable_stats_log: bool,
}
#[cfg(not(feature = "clap"))]
#[derive(Debug)]
struct Args {
streams: u32,
groups: u32,
wait: u32,
push_period: u32,
num_buffers: i32,
disable_stats_log: bool,
}
#[cfg(not(feature = "clap"))]
impl Default for Args {
fn default() -> Self {
Args {
streams: 5000,
groups: 2,
wait: 20,
push_period: 20,
num_buffers: 5000,
disable_stats_log: false,
}
}
}
fn args() -> Args {
#[cfg(feature = "clap")]
let args = {
let args = Args::parse();
gst::info!(CAT, "{:?}", args);
args
};
#[cfg(not(feature = "clap"))]
let args = {
if std::env::args().len() > 1 {
gst::warning!(CAT, "Ignoring command line arguments");
gst::warning!(CAT, "Build with `--features=clap`");
}
let args = Args::default();
gst::warning!(CAT, "{:?}", args);
args
};
args
}
fn main() {
use gst::prelude::*;
use std::time::Instant;
@ -133,8 +59,8 @@ fn main() {
for i in 0..args.streams {
let ctx_name = format!("standalone {}", i % args.groups);
let src = gst::ElementFactory::make("ts-standalone-test-src")
.name(format!("src-{}", i).as_str())
let src = gst::ElementFactory::make(src::ELEMENT_NAME)
.name(format!("src-{i}").as_str())
.property("context", &ctx_name)
.property("context-wait", args.wait)
.property("push-period", args.push_period)
@ -142,16 +68,16 @@ fn main() {
.build()
.unwrap();
let sink = gst::ElementFactory::make("ts-standalone-test-sink")
.name(format!("sink-{}", i).as_str())
let sink = gst::ElementFactory::make(args.sink.element_name())
.name(format!("sink-{i}").as_str())
.property("context", &ctx_name)
.property("context-wait", args.wait)
.build()
.unwrap();
if i == 0 {
src.set_property("raise-log-level", true);
sink.set_property("raise-log-level", true);
src.set_property("main-elem", true);
sink.set_property("main-elem", true);
if !args.disable_stats_log {
// Don't use the last 5 secs in stats
@ -179,30 +105,46 @@ fn main() {
let l = glib::MainLoop::new(None, false);
let bus = pipeline.bus().unwrap();
let terminated_count = Arc::new(AtomicU32::new(0));
let pipeline_clone = pipeline.clone();
let l_clone = l.clone();
bus.add_watch(move |_, msg| {
use gst::MessageView;
match msg.view() {
MessageView::Eos(_) => {
// Actually, we don't post EOS (see sinks impl).
gst::info!(CAT, "Received eos");
l_clone.quit();
glib::Continue(false)
}
MessageView::Error(err) => {
MessageView::Error(msg) => {
if let gst::MessageView::Error(msg) = msg.message().view() {
if msg.error().matches(gst::LibraryError::Shutdown) {
if terminated_count.fetch_add(1, Ordering::SeqCst) == args.streams - 1 {
gst::info!(CAT, "Received all shutdown requests");
l_clone.quit();
return glib::Continue(false);
} else {
return glib::Continue(true);
}
}
}
gst::error!(
CAT,
"Error from {:?}: {} ({:?})",
err.src().map(|s| s.path_string()),
err.error(),
err.debug()
msg.src().map(|s| s.path_string()),
msg.error(),
msg.debug()
);
l_clone.quit();
}
_ => (),
};
glib::Continue(true)
glib::Continue(false)
}
_ => glib::Continue(true),
}
})
.expect("Failed to add bus watch");

View file

@ -0,0 +1,334 @@
// Copyright (C) 2022 François Laignel <fengalin@free.fr>
//
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at
// <https://mozilla.org/MPL/2.0/>.
//
// SPDX-License-Identifier: MPL-2.0
use futures::future::BoxFuture;
use futures::prelude::*;
use gst::glib;
use gst::prelude::*;
use gst::subclass::prelude::*;
use gst::EventView;
use once_cell::sync::Lazy;
use gstthreadshare::runtime::executor::block_on_or_add_sub_task;
use gstthreadshare::runtime::{prelude::*, PadSink};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use super::super::{Settings, Stats, CAT};
#[derive(Debug, Default)]
struct PadSinkHandlerInner {
is_flushing: bool,
is_main_elem: bool,
last_dts: Option<gst::ClockTime>,
segment_start: Option<gst::ClockTime>,
stats: Option<Box<Stats>>,
}
impl PadSinkHandlerInner {
fn handle_buffer(
&mut self,
elem: &super::AsyncMutexSink,
buffer: gst::Buffer,
) -> Result<(), gst::FlowError> {
if self.is_flushing {
log_or_trace!(
CAT,
self.is_main_elem,
obj: elem,
"Discarding {buffer:?} (flushing)"
);
return Err(gst::FlowError::Flushing);
}
debug_or_trace!(CAT, self.is_main_elem, obj: elem, "Received {buffer:?}");
let dts = buffer
.dts()
.expect("Buffer without dts")
.checked_sub(self.segment_start.expect("Buffer without Time Segment"))
.expect("dts before Segment start");
if let Some(last_dts) = self.last_dts {
let cur_ts = elem.current_running_time().unwrap();
let latency: Duration = (cur_ts - dts).into();
let interval: Duration = (dts - last_dts).into();
if let Some(stats) = self.stats.as_mut() {
stats.add_buffer(latency, interval);
}
debug_or_trace!(CAT, self.is_main_elem, obj: elem, "o latency {latency:.2?}");
debug_or_trace!(
CAT,
self.is_main_elem,
obj: elem,
"o interval {interval:.2?}",
);
}
self.last_dts = Some(dts);
log_or_trace!(CAT, self.is_main_elem, obj: elem, "Buffer processed");
Ok(())
}
}
#[derive(Clone, Debug, Default)]
struct AsyncPadSinkHandler(Arc<futures::lock::Mutex<PadSinkHandlerInner>>);
impl PadSinkHandler for AsyncPadSinkHandler {
type ElementImpl = AsyncMutexSink;
fn sink_chain(
self,
_pad: gst::Pad,
elem: super::AsyncMutexSink,
buffer: gst::Buffer,
) -> BoxFuture<'static, Result<gst::FlowSuccess, gst::FlowError>> {
async move {
if self.0.lock().await.handle_buffer(&elem, buffer).is_err() {
return Err(gst::FlowError::Flushing);
}
Ok(gst::FlowSuccess::Ok)
}
.boxed()
}
fn sink_event_serialized(
self,
_pad: gst::Pad,
elem: super::AsyncMutexSink,
event: gst::Event,
) -> BoxFuture<'static, bool> {
async move {
match event.view() {
EventView::Eos(_) => {
{
let mut inner = self.0.lock().await;
debug_or_trace!(CAT, inner.is_main_elem, obj: elem, "EOS");
inner.is_flushing = true;
}
// When each element sends its own EOS message,
// it takes ages for the pipeline to process all of them.
// Let's just post an error message and let main shuts down
// after all streams have posted this message.
let _ = elem
.post_message(gst::message::Error::new(gst::LibraryError::Shutdown, "EOS"));
}
EventView::FlushStop(_) => {
self.0.lock().await.is_flushing = false;
}
EventView::Segment(evt) => {
if let Some(time_seg) = evt.segment().downcast_ref::<gst::ClockTime>() {
self.0.lock().await.segment_start = time_seg.start();
}
}
EventView::SinkMessage(evt) => {
let _ = elem.post_message(evt.message());
}
_ => (),
}
true
}
.boxed()
}
fn sink_event(self, _pad: &gst::Pad, _imp: &AsyncMutexSink, event: gst::Event) -> bool {
if let EventView::FlushStart(..) = event.view() {
block_on_or_add_sub_task(async move { self.0.lock().await.is_flushing = true });
}
true
}
}
impl AsyncPadSinkHandler {
fn prepare(&self, is_main_elem: bool, stats: Option<Stats>) {
futures::executor::block_on(async move {
let mut inner = self.0.lock().await;
inner.is_main_elem = is_main_elem;
inner.stats = stats.map(Box::new);
});
}
fn start(&self) {
futures::executor::block_on(async move {
let mut inner = self.0.lock().await;
inner.is_flushing = false;
inner.last_dts = None;
if let Some(stats) = inner.stats.as_mut() {
stats.start();
}
});
}
fn stop(&self) {
futures::executor::block_on(async move {
let mut inner = self.0.lock().await;
inner.is_flushing = true;
});
}
}
#[derive(Debug)]
pub struct AsyncMutexSink {
sink_pad: PadSink,
sink_pad_handler: AsyncPadSinkHandler,
settings: Mutex<Settings>,
}
impl AsyncMutexSink {
fn prepare(&self) -> Result<(), gst::ErrorMessage> {
let settings = self.settings.lock().unwrap();
debug_or_trace!(CAT, settings.is_main_elem, imp: self, "Preparing");
let stats = if settings.logs_stats {
Some(Stats::new(
settings.max_buffers,
settings.push_period + settings.context_wait / 2,
))
} else {
None
};
self.sink_pad_handler.prepare(settings.is_main_elem, stats);
debug_or_trace!(CAT, settings.is_main_elem, imp: self, "Prepared");
Ok(())
}
fn stop(&self) -> Result<(), gst::ErrorMessage> {
let is_main_elem = self.settings.lock().unwrap().is_main_elem;
debug_or_trace!(CAT, is_main_elem, imp: self, "Stopping");
self.sink_pad_handler.stop();
debug_or_trace!(CAT, is_main_elem, imp: self, "Stopped");
Ok(())
}
fn start(&self) -> Result<(), gst::ErrorMessage> {
let is_main_elem = self.settings.lock().unwrap().is_main_elem;
debug_or_trace!(CAT, is_main_elem, imp: self, "Starting");
self.sink_pad_handler.start();
debug_or_trace!(CAT, is_main_elem, imp: self, "Started");
Ok(())
}
}
#[glib::object_subclass]
impl ObjectSubclass for AsyncMutexSink {
const NAME: &'static str = "TsStandaloneAsyncMutexSink";
type Type = super::AsyncMutexSink;
type ParentType = gst::Element;
fn with_class(klass: &Self::Class) -> Self {
let sink_pad_handler = AsyncPadSinkHandler::default();
Self {
sink_pad: PadSink::new(
gst::Pad::from_template(&klass.pad_template("sink").unwrap(), Some("sink")),
sink_pad_handler.clone(),
),
sink_pad_handler,
settings: Default::default(),
}
}
}
impl ObjectImpl for AsyncMutexSink {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(Settings::properties);
PROPERTIES.as_ref()
}
fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
self.settings.lock().unwrap().set_property(id, value, pspec);
}
fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value {
self.settings.lock().unwrap().property(id, pspec)
}
fn constructed(&self) {
self.parent_constructed();
let obj = self.obj();
obj.add_pad(self.sink_pad.gst_pad()).unwrap();
obj.set_element_flags(gst::ElementFlags::SINK);
}
}
impl GstObjectImpl for AsyncMutexSink {}
impl ElementImpl for AsyncMutexSink {
fn metadata() -> Option<&'static gst::subclass::ElementMetadata> {
static ELEMENT_METADATA: Lazy<gst::subclass::ElementMetadata> = Lazy::new(|| {
gst::subclass::ElementMetadata::new(
"Thread-sharing standalone test async mutex sink",
"Sink/Test",
"Thread-sharing standalone test async mutex sink",
"François Laignel <fengalin@free.fr>",
)
});
Some(&*ELEMENT_METADATA)
}
fn pad_templates() -> &'static [gst::PadTemplate] {
static PAD_TEMPLATES: Lazy<Vec<gst::PadTemplate>> = Lazy::new(|| {
let caps = gst::Caps::new_any();
let sink_pad_template = gst::PadTemplate::new(
"sink",
gst::PadDirection::Sink,
gst::PadPresence::Always,
&caps,
)
.unwrap();
vec![sink_pad_template]
});
PAD_TEMPLATES.as_ref()
}
fn change_state(
&self,
transition: gst::StateChange,
) -> Result<gst::StateChangeSuccess, gst::StateChangeError> {
gst::trace!(CAT, imp: self, "Changing state {transition:?}");
match transition {
gst::StateChange::NullToReady => {
self.prepare().map_err(|err| {
self.post_error_message(err);
gst::StateChangeError
})?;
}
gst::StateChange::ReadyToPaused => {
self.start().map_err(|_| gst::StateChangeError)?;
}
gst::StateChange::PausedToReady => {
self.stop().map_err(|_| gst::StateChangeError)?;
}
_ => (),
}
self.parent_change_state(transition)
}
}

View file

@ -0,0 +1,17 @@
use gst::glib;
use gst::prelude::*;
mod imp;
glib::wrapper! {
pub struct AsyncMutexSink(ObjectSubclass<imp::AsyncMutexSink>) @extends gst::Element, gst::Object;
}
pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
gst::Element::register(
Some(plugin),
super::ASYNC_MUTEX_ELEMENT_NAME,
gst::Rank::None,
AsyncMutexSink::static_type(),
)
}

View file

@ -1,864 +0,0 @@
// Copyright (C) 2022 François Laignel <fengalin@free.fr>
//
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at
// <https://mozilla.org/MPL/2.0/>.
//
// SPDX-License-Identifier: MPL-2.0
use futures::future::BoxFuture;
use futures::prelude::*;
use futures::stream::Peekable;
use gst::error_msg;
use gst::glib;
use gst::prelude::*;
use gst::subclass::prelude::*;
use gst::EventView;
use once_cell::sync::Lazy;
use gstthreadshare::runtime::prelude::*;
use gstthreadshare::runtime::{Context, PadSink, Task};
use std::sync::Mutex;
use std::task::Poll;
use std::time::{Duration, Instant};
static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
gst::DebugCategory::new(
"ts-standalone-test-sink",
gst::DebugColorFlags::empty(),
Some("Thread-sharing standalone test sink"),
)
});
const DEFAULT_CONTEXT: &str = "";
const DEFAULT_CONTEXT_WAIT: Duration = Duration::from_millis(20);
const DEFAULT_PUSH_PERIOD: Duration = Duration::from_millis(20);
const DEFAULT_MAX_BUFFERS: i32 = 50 * (100 - 25);
const LOG_PERIOD: Duration = Duration::from_secs(20);
#[derive(Debug, Clone)]
struct Settings {
context: String,
context_wait: Duration,
raise_log_level: bool,
logs_stats: bool,
push_period: Duration,
max_buffers: Option<u32>,
}
impl Default for Settings {
fn default() -> Self {
Settings {
context: DEFAULT_CONTEXT.into(),
context_wait: DEFAULT_CONTEXT_WAIT,
raise_log_level: false,
logs_stats: false,
push_period: DEFAULT_PUSH_PERIOD,
max_buffers: Some(DEFAULT_MAX_BUFFERS as u32),
}
}
}
#[derive(Debug)]
enum StreamItem {
Buffer(gst::Buffer),
Event(gst::Event),
}
#[derive(Clone, Debug)]
struct TestSinkPadHandler;
impl PadSinkHandler for TestSinkPadHandler {
type ElementImpl = TestSink;
fn sink_chain(
self,
_pad: gst::Pad,
elem: super::TestSink,
buffer: gst::Buffer,
) -> BoxFuture<'static, Result<gst::FlowSuccess, gst::FlowError>> {
let sender = elem.imp().clone_item_sender();
async move {
if sender.send_async(StreamItem::Buffer(buffer)).await.is_err() {
gst::debug!(CAT, obj: elem, "Flushing");
return Err(gst::FlowError::Flushing);
}
Ok(gst::FlowSuccess::Ok)
}
.boxed()
}
fn sink_chain_list(
self,
_pad: gst::Pad,
elem: super::TestSink,
list: gst::BufferList,
) -> BoxFuture<'static, Result<gst::FlowSuccess, gst::FlowError>> {
let sender = elem.imp().clone_item_sender();
async move {
for buffer in list.iter_owned() {
if sender.send_async(StreamItem::Buffer(buffer)).await.is_err() {
gst::debug!(CAT, obj: elem, "Flushing");
return Err(gst::FlowError::Flushing);
}
}
Ok(gst::FlowSuccess::Ok)
}
.boxed()
}
fn sink_event_serialized(
self,
_pad: gst::Pad,
elem: super::TestSink,
event: gst::Event,
) -> BoxFuture<'static, bool> {
let sender = elem.imp().clone_item_sender();
async move {
if let EventView::FlushStop(_) = event.view() {
let imp = elem.imp();
return imp.task.flush_stop().await_maybe_on_context().is_ok();
} else if sender.send_async(StreamItem::Event(event)).await.is_err() {
gst::debug!(CAT, obj: elem, "Flushing");
}
true
}
.boxed()
}
fn sink_event(self, _pad: &gst::Pad, imp: &TestSink, event: gst::Event) -> bool {
if let EventView::FlushStart(..) = event.view() {
return imp.task.flush_start().await_maybe_on_context().is_ok();
}
true
}
}
#[derive(Default)]
struct Stats {
must_log: bool,
ramp_up_instant: Option<Instant>,
log_start_instant: Option<Instant>,
last_delta_instant: Option<Instant>,
max_buffers: Option<f32>,
buffer_count: f32,
buffer_count_delta: f32,
latency_sum: f32,
latency_square_sum: f32,
latency_sum_delta: f32,
latency_square_sum_delta: f32,
latency_min: Duration,
latency_min_delta: Duration,
latency_max: Duration,
latency_max_delta: Duration,
interval_sum: f32,
interval_square_sum: f32,
interval_sum_delta: f32,
interval_square_sum_delta: f32,
interval_min: Duration,
interval_min_delta: Duration,
interval_max: Duration,
interval_max_delta: Duration,
interval_late_warn: Duration,
interval_late_count: f32,
interval_late_count_delta: f32,
#[cfg(feature = "tuning")]
parked_duration_init: Duration,
}
impl Stats {
fn start(&mut self) {
if !self.must_log {
return;
}
self.buffer_count = 0.0;
self.buffer_count_delta = 0.0;
self.latency_sum = 0.0;
self.latency_square_sum = 0.0;
self.latency_sum_delta = 0.0;
self.latency_square_sum_delta = 0.0;
self.latency_min = Duration::MAX;
self.latency_min_delta = Duration::MAX;
self.latency_max = Duration::ZERO;
self.latency_max_delta = Duration::ZERO;
self.interval_sum = 0.0;
self.interval_square_sum = 0.0;
self.interval_sum_delta = 0.0;
self.interval_square_sum_delta = 0.0;
self.interval_min = Duration::MAX;
self.interval_min_delta = Duration::MAX;
self.interval_max = Duration::ZERO;
self.interval_max_delta = Duration::ZERO;
self.interval_late_count = 0.0;
self.interval_late_count_delta = 0.0;
self.last_delta_instant = None;
self.log_start_instant = None;
self.ramp_up_instant = Some(Instant::now());
gst::info!(CAT, "First stats logs in {:2?}", 2 * LOG_PERIOD);
}
fn is_active(&mut self) -> bool {
if !self.must_log {
return false;
}
if let Some(ramp_up_instant) = self.ramp_up_instant {
if ramp_up_instant.elapsed() < LOG_PERIOD {
return false;
}
self.ramp_up_instant = None;
gst::info!(CAT, "Ramp up complete. Stats logs in {:2?}", LOG_PERIOD);
self.log_start_instant = Some(Instant::now());
self.last_delta_instant = self.log_start_instant;
#[cfg(feature = "tuning")]
{
self.parked_duration_init = Context::current().unwrap().parked_duration();
}
}
use std::cmp::Ordering::*;
match self.max_buffers.opt_cmp(self.buffer_count) {
Some(Equal) => {
self.log_global();
self.buffer_count += 1.0;
false
}
Some(Less) => false,
_ => true,
}
}
fn add_buffer(&mut self, latency: Duration, interval: Duration) {
if !self.is_active() {
return;
}
self.buffer_count += 1.0;
self.buffer_count_delta += 1.0;
// Latency
let latency_f32 = latency.as_nanos() as f32;
let latency_square = latency_f32.powi(2);
self.latency_sum += latency_f32;
self.latency_square_sum += latency_square;
self.latency_min = self.latency_min.min(latency);
self.latency_max = self.latency_max.max(latency);
self.latency_sum_delta += latency_f32;
self.latency_square_sum_delta += latency_square;
self.latency_min_delta = self.latency_min_delta.min(latency);
self.latency_max_delta = self.latency_max_delta.max(latency);
// Interval
let interval_f32 = interval.as_nanos() as f32;
let interval_square = interval_f32.powi(2);
self.interval_sum += interval_f32;
self.interval_square_sum += interval_square;
self.interval_min = self.interval_min.min(interval);
self.interval_max = self.interval_max.max(interval);
self.interval_sum_delta += interval_f32;
self.interval_square_sum_delta += interval_square;
self.interval_min_delta = self.interval_min_delta.min(interval);
self.interval_max_delta = self.interval_max_delta.max(interval);
if interval > self.interval_late_warn {
self.interval_late_count += 1.0;
self.interval_late_count_delta += 1.0;
}
let delta_duration = match self.last_delta_instant {
Some(last_delta) => last_delta.elapsed(),
None => return,
};
if delta_duration < LOG_PERIOD {
return;
}
self.last_delta_instant = Some(Instant::now());
gst::info!(CAT, "Delta stats:");
let interval_mean = self.interval_sum_delta / self.buffer_count_delta;
let interval_std_dev = f32::sqrt(
self.interval_square_sum_delta / self.buffer_count_delta - interval_mean.powi(2),
);
gst::info!(
CAT,
"o interval: mean {:4.2?} σ {:4.1?} [{:4.1?}, {:4.1?}]",
Duration::from_nanos(interval_mean as u64),
Duration::from_nanos(interval_std_dev as u64),
self.interval_min_delta,
self.interval_max_delta,
);
if self.interval_late_count_delta > f32::EPSILON {
gst::warning!(
CAT,
"o {:5.2}% late buffers",
100f32 * self.interval_late_count_delta / self.buffer_count_delta
);
}
self.interval_sum_delta = 0.0;
self.interval_square_sum_delta = 0.0;
self.interval_min_delta = Duration::MAX;
self.interval_max_delta = Duration::ZERO;
self.interval_late_count_delta = 0.0;
let latency_mean = self.latency_sum_delta / self.buffer_count_delta;
let latency_std_dev = f32::sqrt(
self.latency_square_sum_delta / self.buffer_count_delta - latency_mean.powi(2),
);
gst::info!(
CAT,
"o latency: mean {:4.2?} σ {:4.1?} [{:4.1?}, {:4.1?}]",
Duration::from_nanos(latency_mean as u64),
Duration::from_nanos(latency_std_dev as u64),
self.latency_min_delta,
self.latency_max_delta,
);
self.latency_sum_delta = 0.0;
self.latency_square_sum_delta = 0.0;
self.latency_min_delta = Duration::MAX;
self.latency_max_delta = Duration::ZERO;
self.buffer_count_delta = 0.0;
}
fn log_global(&mut self) {
if self.buffer_count < 1.0 {
return;
}
let _log_start = if let Some(log_start) = self.log_start_instant {
log_start
} else {
return;
};
gst::info!(CAT, "Global stats:");
#[cfg(feature = "tuning")]
{
let duration = _log_start.elapsed();
let parked_duration =
Context::current().unwrap().parked_duration() - self.parked_duration_init;
gst::info!(
CAT,
"o parked: {parked_duration:4.2?} ({:5.2?}%)",
(parked_duration.as_nanos() as f32 * 100.0 / duration.as_nanos() as f32)
);
}
let interval_mean = self.interval_sum / self.buffer_count;
let interval_std_dev =
f32::sqrt(self.interval_square_sum / self.buffer_count - interval_mean.powi(2));
gst::info!(
CAT,
"o interval: mean {:4.2?} σ {:4.1?} [{:4.1?}, {:4.1?}]",
Duration::from_nanos(interval_mean as u64),
Duration::from_nanos(interval_std_dev as u64),
self.interval_min,
self.interval_max,
);
if self.interval_late_count > f32::EPSILON {
gst::warning!(
CAT,
"o {:5.2}% late buffers",
100f32 * self.interval_late_count / self.buffer_count
);
}
let latency_mean = self.latency_sum / self.buffer_count;
let latency_std_dev =
f32::sqrt(self.latency_square_sum / self.buffer_count - latency_mean.powi(2));
gst::info!(
CAT,
"o latency: mean {:4.2?} σ {:4.1?} [{:4.1?}, {:4.1?}]",
Duration::from_nanos(latency_mean as u64),
Duration::from_nanos(latency_std_dev as u64),
self.latency_min,
self.latency_max,
);
}
}
struct TestSinkTask {
element: super::TestSink,
raise_log_level: bool,
last_dts: Option<gst::ClockTime>,
item_receiver: Peekable<flume::r#async::RecvStream<'static, StreamItem>>,
stats: Stats,
segment: Option<gst::Segment>,
}
impl TestSinkTask {
fn new(element: &super::TestSink, item_receiver: flume::Receiver<StreamItem>) -> Self {
TestSinkTask {
element: element.clone(),
raise_log_level: false,
last_dts: None,
item_receiver: item_receiver.into_stream().peekable(),
stats: Stats::default(),
segment: None,
}
}
async fn flush(&mut self) {
// Purge the channel
while let Poll::Ready(Some(_item)) = futures::poll!(self.item_receiver.next()) {}
}
}
impl TaskImpl for TestSinkTask {
type Item = StreamItem;
fn prepare(&mut self) -> BoxFuture<'_, Result<(), gst::ErrorMessage>> {
async move {
let sink = self.element.imp();
let settings = sink.settings.lock().unwrap();
self.raise_log_level = settings.raise_log_level;
if self.raise_log_level {
gst::log!(CAT, obj: self.element, "Preparing Task");
} else {
gst::trace!(CAT, obj: self.element, "Preparing Task");
}
self.stats.must_log = settings.logs_stats;
self.stats.max_buffers = settings.max_buffers.map(|max_buffers| max_buffers as f32);
self.stats.interval_late_warn = settings.push_period + settings.context_wait / 2;
Ok(())
}
.boxed()
}
fn start(&mut self) -> BoxFuture<'_, Result<(), gst::ErrorMessage>> {
async {
if self.raise_log_level {
gst::log!(CAT, obj: self.element, "Starting Task");
} else {
gst::trace!(CAT, obj: self.element, "Starting Task");
}
self.last_dts = None;
self.stats.start();
Ok(())
}
.boxed()
}
fn stop(&mut self) -> BoxFuture<'_, Result<(), gst::ErrorMessage>> {
async {
if self.raise_log_level {
gst::log!(CAT, obj: self.element, "Stopping Task");
} else {
gst::trace!(CAT, obj: self.element, "Stopping Task");
}
self.flush().await;
Ok(())
}
.boxed()
}
fn try_next(&mut self) -> BoxFuture<'_, Result<StreamItem, gst::FlowError>> {
async move {
let item = self.item_receiver.next().await.unwrap();
if self.raise_log_level {
gst::log!(CAT, obj: self.element, "Popped item");
} else {
gst::trace!(CAT, obj: self.element, "Popped item");
}
Ok(item)
}
.boxed()
}
fn handle_item(&mut self, item: StreamItem) -> BoxFuture<'_, Result<(), gst::FlowError>> {
async move {
if self.raise_log_level {
gst::debug!(CAT, obj: self.element, "Received {:?}", item);
} else {
gst::trace!(CAT, obj: self.element, "Received {:?}", item);
}
match item {
StreamItem::Buffer(buffer) => {
let dts = self
.segment
.as_ref()
.and_then(|segment| {
segment
.downcast_ref::<gst::format::Time>()
.and_then(|segment| segment.to_running_time(buffer.dts()))
})
.unwrap();
if let Some(last_dts) = self.last_dts {
let cur_ts = self.element.current_running_time().unwrap();
let latency: Duration = (cur_ts - dts).into();
let interval: Duration = (dts - last_dts).into();
self.stats.add_buffer(latency, interval);
if self.raise_log_level {
gst::debug!(CAT, obj: self.element, "o latency {:.2?}", latency);
gst::debug!(CAT, obj: self.element, "o interval {:.2?}", interval);
} else {
gst::trace!(CAT, obj: self.element, "o latency {:.2?}", latency);
gst::trace!(CAT, obj: self.element, "o interval {:.2?}", interval);
}
}
self.last_dts = Some(dts);
if self.raise_log_level {
gst::log!(CAT, obj: self.element, "Buffer processed");
} else {
gst::trace!(CAT, obj: self.element, "Buffer processed");
}
}
StreamItem::Event(event) => match event.view() {
EventView::Eos(_) => {
if self.raise_log_level {
gst::debug!(CAT, obj: self.element, "EOS");
} else {
gst::trace!(CAT, obj: self.element, "EOS");
}
let elem = self.element.clone();
self.element.call_async(move |_| {
let _ =
elem.post_message(gst::message::Eos::builder().src(&elem).build());
});
return Err(gst::FlowError::Eos);
}
EventView::Segment(e) => {
self.segment = Some(e.segment().clone());
}
EventView::SinkMessage(e) => {
let _ = self.element.post_message(e.message());
}
_ => (),
},
}
Ok(())
}
.boxed()
}
}
#[derive(Debug)]
pub struct TestSink {
sink_pad: PadSink,
task: Task,
item_sender: Mutex<Option<flume::Sender<StreamItem>>>,
settings: Mutex<Settings>,
}
impl TestSink {
#[track_caller]
fn clone_item_sender(&self) -> flume::Sender<StreamItem> {
self.item_sender.lock().unwrap().as_ref().unwrap().clone()
}
fn prepare(&self) -> Result<(), gst::ErrorMessage> {
let raise_log_level = self.settings.lock().unwrap().raise_log_level;
if raise_log_level {
gst::debug!(CAT, imp: self, "Preparing");
} else {
gst::trace!(CAT, imp: self, "Preparing");
}
let context = {
let settings = self.settings.lock().unwrap();
Context::acquire(&settings.context, settings.context_wait).map_err(|err| {
error_msg!(
gst::ResourceError::OpenWrite,
["Failed to acquire Context: {}", err]
)
})?
};
// Enable backpressure for items
let (item_sender, item_receiver) = flume::bounded(0);
let task_impl = TestSinkTask::new(&*self.obj(), item_receiver);
self.task.prepare(task_impl, context).block_on()?;
*self.item_sender.lock().unwrap() = Some(item_sender);
if raise_log_level {
gst::debug!(CAT, imp: self, "Prepared");
} else {
gst::trace!(CAT, imp: self, "Prepared");
}
Ok(())
}
fn unprepare(&self) {
let raise_log_level = self.settings.lock().unwrap().raise_log_level;
if raise_log_level {
gst::debug!(CAT, imp: self, "Unpreparing");
} else {
gst::trace!(CAT, imp: self, "Unpreparing");
}
self.task.unprepare().block_on().unwrap();
if raise_log_level {
gst::debug!(CAT, imp: self, "Unprepared");
} else {
gst::trace!(CAT, imp: self, "Unprepared");
}
}
fn stop(&self) -> Result<(), gst::ErrorMessage> {
let raise_log_level = self.settings.lock().unwrap().raise_log_level;
if raise_log_level {
gst::debug!(CAT, imp: self, "Stopping");
} else {
gst::trace!(CAT, imp: self, "Stopping");
}
self.task.stop().block_on()?;
if raise_log_level {
gst::debug!(CAT, imp: self, "Stopped");
} else {
gst::trace!(CAT, imp: self, "Stopped");
}
Ok(())
}
fn start(&self) -> Result<(), gst::ErrorMessage> {
let raise_log_level = self.settings.lock().unwrap().raise_log_level;
if raise_log_level {
gst::debug!(CAT, imp: self, "Starting");
} else {
gst::trace!(CAT, imp: self, "Starting");
}
self.task.start().block_on()?;
if raise_log_level {
gst::debug!(CAT, imp: self, "Started");
} else {
gst::trace!(CAT, imp: self, "Started");
}
Ok(())
}
}
#[glib::object_subclass]
impl ObjectSubclass for TestSink {
const NAME: &'static str = "StandaloneTestSink";
type Type = super::TestSink;
type ParentType = gst::Element;
fn with_class(klass: &Self::Class) -> Self {
Self {
sink_pad: PadSink::new(
gst::Pad::from_template(&klass.pad_template("sink").unwrap(), Some("sink")),
TestSinkPadHandler,
),
task: Task::default(),
item_sender: Default::default(),
settings: Default::default(),
}
}
}
impl ObjectImpl for TestSink {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecString::builder("context")
.nick("Context")
.blurb("Context name to share threads with")
.default_value(Some(DEFAULT_CONTEXT))
.build(),
glib::ParamSpecUInt::builder("context-wait")
.nick("Context Wait")
.blurb("Throttle poll loop to run at most once every this many ms")
.maximum(1000)
.default_value(DEFAULT_CONTEXT_WAIT.as_millis() as u32)
.build(),
glib::ParamSpecBoolean::builder("raise-log-level")
.nick("Raise log level")
.blurb("Raises the log level so that this element stands out")
.write_only()
.build(),
glib::ParamSpecBoolean::builder("logs-stats")
.nick("Logs Stats")
.blurb("Whether statistics should be logged")
.write_only()
.build(),
glib::ParamSpecUInt::builder("push-period")
.nick("Src buffer Push Period")
.blurb("Push period used by `src` element (used for stats warnings)")
.default_value(DEFAULT_PUSH_PERIOD.as_millis() as u32)
.build(),
glib::ParamSpecInt::builder("max-buffers")
.nick("Max Buffers")
.blurb("Number of buffers to count before stopping stats (-1 = unlimited)")
.minimum(-1i32)
.default_value(DEFAULT_MAX_BUFFERS)
.build(),
]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
let mut settings = self.settings.lock().unwrap();
match pspec.name() {
"context" => {
settings.context = value
.get::<Option<String>>()
.expect("type checked upstream")
.unwrap_or_else(|| DEFAULT_CONTEXT.into());
}
"context-wait" => {
settings.context_wait = Duration::from_millis(
value.get::<u32>().expect("type checked upstream").into(),
);
}
"raise-log-level" => {
settings.raise_log_level = value.get::<bool>().expect("type checked upstream");
}
"logs-stats" => {
let logs_stats = value.get().expect("type checked upstream");
settings.logs_stats = logs_stats;
}
"push-period" => {
settings.push_period = Duration::from_millis(
value.get::<u32>().expect("type checked upstream").into(),
);
}
"max-buffers" => {
let value = value.get::<i32>().expect("type checked upstream");
settings.max_buffers = if value > 0 { Some(value as u32) } else { None };
}
_ => unimplemented!(),
}
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
let settings = self.settings.lock().unwrap();
match pspec.name() {
"context" => settings.context.to_value(),
"context-wait" => (settings.context_wait.as_millis() as u32).to_value(),
"raise-log-level" => settings.raise_log_level.to_value(),
"push-period" => (settings.push_period.as_millis() as u32).to_value(),
"max-buffers" => settings
.max_buffers
.and_then(|val| val.try_into().ok())
.unwrap_or(-1i32)
.to_value(),
_ => unimplemented!(),
}
}
fn constructed(&self) {
self.parent_constructed();
let obj = self.obj();
obj.add_pad(self.sink_pad.gst_pad()).unwrap();
obj.set_element_flags(gst::ElementFlags::SINK);
}
}
impl GstObjectImpl for TestSink {}
impl ElementImpl for TestSink {
fn metadata() -> Option<&'static gst::subclass::ElementMetadata> {
static ELEMENT_METADATA: Lazy<gst::subclass::ElementMetadata> = Lazy::new(|| {
gst::subclass::ElementMetadata::new(
"Thread-sharing standalone test sink",
"Sink/Test",
"Thread-sharing standalone test sink",
"François Laignel <fengalin@free.fr>",
)
});
Some(&*ELEMENT_METADATA)
}
fn pad_templates() -> &'static [gst::PadTemplate] {
static PAD_TEMPLATES: Lazy<Vec<gst::PadTemplate>> = Lazy::new(|| {
let caps = gst::Caps::new_any();
let sink_pad_template = gst::PadTemplate::new(
"sink",
gst::PadDirection::Sink,
gst::PadPresence::Always,
&caps,
)
.unwrap();
vec![sink_pad_template]
});
PAD_TEMPLATES.as_ref()
}
fn change_state(
&self,
transition: gst::StateChange,
) -> Result<gst::StateChangeSuccess, gst::StateChangeError> {
gst::trace!(CAT, imp: self, "Changing state {:?}", transition);
match transition {
gst::StateChange::NullToReady => {
self.prepare().map_err(|err| {
self.post_error_message(err);
gst::StateChangeError
})?;
}
gst::StateChange::ReadyToPaused => {
self.start().map_err(|_| gst::StateChangeError)?;
}
gst::StateChange::PausedToReady => {
self.stop().map_err(|_| gst::StateChangeError)?;
}
gst::StateChange::ReadyToNull => {
self.unprepare();
}
_ => (),
}
self.parent_change_state(transition)
}
}

View file

@ -1,17 +1,22 @@
use gst::glib;
use gst::prelude::*;
pub mod async_mutex;
pub mod sync_mutex;
pub mod task;
mod imp;
mod settings;
pub use settings::Settings;
glib::wrapper! {
pub struct TestSink(ObjectSubclass<imp::TestSink>) @extends gst::Element, gst::Object;
}
mod stats;
pub use stats::Stats;
pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
gst::Element::register(
Some(plugin),
"ts-standalone-test-sink",
gst::Rank::None,
TestSink::static_type(),
pub const ASYNC_MUTEX_ELEMENT_NAME: &str = "ts-standalone-async-mutex-sink";
pub const SYNC_MUTEX_ELEMENT_NAME: &str = "ts-standalone-sync-mutex-sink";
pub const TASK_ELEMENT_NAME: &str = "ts-standalone-task-sink";
use once_cell::sync::Lazy;
static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
gst::DebugCategory::new(
"ts-standalone-sink",
gst::DebugColorFlags::empty(),
Some("Thread-sharing standalone test sink"),
)
}
});

View file

@ -0,0 +1,115 @@
use gst::glib;
use gst::prelude::*;
use std::time::Duration;
const DEFAULT_CONTEXT: &str = "";
const DEFAULT_CONTEXT_WAIT: Duration = Duration::from_millis(20);
const DEFAULT_PUSH_PERIOD: Duration = Duration::from_millis(20);
const DEFAULT_MAX_BUFFERS: i32 = 50 * (100 - 25);
#[derive(Debug, Clone)]
pub struct Settings {
pub context: String,
pub context_wait: Duration,
pub is_main_elem: bool,
pub logs_stats: bool,
pub push_period: Duration,
pub max_buffers: Option<u32>,
}
impl Default for Settings {
fn default() -> Self {
Settings {
context: DEFAULT_CONTEXT.into(),
context_wait: DEFAULT_CONTEXT_WAIT,
is_main_elem: false,
logs_stats: false,
push_period: DEFAULT_PUSH_PERIOD,
max_buffers: Some(DEFAULT_MAX_BUFFERS as u32),
}
}
}
impl Settings {
pub fn properties() -> Vec<glib::ParamSpec> {
vec![
glib::ParamSpecString::builder("context")
.nick("Context")
.blurb("Context name to share threads with")
.default_value(Some(DEFAULT_CONTEXT))
.build(),
glib::ParamSpecUInt::builder("context-wait")
.nick("Context Wait")
.blurb("Throttle poll loop to run at most once every this many ms")
.maximum(1000)
.default_value(DEFAULT_CONTEXT_WAIT.as_millis() as u32)
.build(),
glib::ParamSpecBoolean::builder("main-elem")
.nick("Main Element")
.blurb("Declare this element as the main one")
.write_only()
.build(),
glib::ParamSpecBoolean::builder("logs-stats")
.nick("Logs Stats")
.blurb("Whether statistics should be logged")
.write_only()
.build(),
glib::ParamSpecUInt::builder("push-period")
.nick("Src buffer Push Period")
.blurb("Push period used by `src` element (used for stats warnings)")
.default_value(DEFAULT_PUSH_PERIOD.as_millis() as u32)
.build(),
glib::ParamSpecInt::builder("max-buffers")
.nick("Max Buffers")
.blurb("Number of buffers to count before stopping stats (-1 = unlimited)")
.minimum(-1i32)
.default_value(DEFAULT_MAX_BUFFERS)
.build(),
]
}
pub fn set_property(&mut self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
match pspec.name() {
"context" => {
self.context = value
.get::<Option<String>>()
.unwrap()
.unwrap_or_else(|| DEFAULT_CONTEXT.into());
}
"context-wait" => {
self.context_wait = Duration::from_millis(value.get::<u32>().unwrap().into());
}
"main-elem" => {
self.is_main_elem = value.get::<bool>().unwrap();
}
"logs-stats" => {
let logs_stats = value.get().unwrap();
self.logs_stats = logs_stats;
}
"push-period" => {
self.push_period = Duration::from_millis(value.get::<u32>().unwrap().into());
}
"max-buffers" => {
let value = value.get::<i32>().unwrap();
self.max_buffers = if value > 0 { Some(value as u32) } else { None };
}
_ => unimplemented!(),
}
}
pub fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
"context" => self.context.to_value(),
"context-wait" => (self.context_wait.as_millis() as u32).to_value(),
"main-elem" => self.is_main_elem.to_value(),
"push-period" => (self.push_period.as_millis() as u32).to_value(),
"max-buffers" => self
.max_buffers
.and_then(|val| val.try_into().ok())
.unwrap_or(-1i32)
.to_value(),
_ => unimplemented!(),
}
}
}

View file

@ -0,0 +1,270 @@
use gst::prelude::*;
use std::time::{Duration, Instant};
#[cfg(feature = "tuning")]
use gstthreadshare::runtime::Context;
use super::CAT;
const LOG_PERIOD: Duration = Duration::from_secs(20);
#[derive(Debug, Default)]
pub struct Stats {
ramp_up_instant: Option<Instant>,
log_start_instant: Option<Instant>,
last_delta_instant: Option<Instant>,
max_buffers: Option<f32>,
buffer_count: f32,
buffer_count_delta: f32,
latency_sum: f32,
latency_square_sum: f32,
latency_sum_delta: f32,
latency_square_sum_delta: f32,
latency_min: Duration,
latency_min_delta: Duration,
latency_max: Duration,
latency_max_delta: Duration,
interval_sum: f32,
interval_square_sum: f32,
interval_sum_delta: f32,
interval_square_sum_delta: f32,
interval_min: Duration,
interval_min_delta: Duration,
interval_max: Duration,
interval_max_delta: Duration,
interval_late_warn: Duration,
interval_late_count: f32,
interval_late_count_delta: f32,
#[cfg(feature = "tuning")]
parked_duration_init: Duration,
}
impl Stats {
pub fn new(max_buffers: Option<u32>, interval_late_warn: Duration) -> Self {
Stats {
max_buffers: max_buffers.map(|max_buffers| max_buffers as f32),
interval_late_warn,
..Default::default()
}
}
pub fn start(&mut self) {
self.buffer_count = 0.0;
self.buffer_count_delta = 0.0;
self.latency_sum = 0.0;
self.latency_square_sum = 0.0;
self.latency_sum_delta = 0.0;
self.latency_square_sum_delta = 0.0;
self.latency_min = Duration::MAX;
self.latency_min_delta = Duration::MAX;
self.latency_max = Duration::ZERO;
self.latency_max_delta = Duration::ZERO;
self.interval_sum = 0.0;
self.interval_square_sum = 0.0;
self.interval_sum_delta = 0.0;
self.interval_square_sum_delta = 0.0;
self.interval_min = Duration::MAX;
self.interval_min_delta = Duration::MAX;
self.interval_max = Duration::ZERO;
self.interval_max_delta = Duration::ZERO;
self.interval_late_count = 0.0;
self.interval_late_count_delta = 0.0;
self.last_delta_instant = None;
self.log_start_instant = None;
self.ramp_up_instant = Some(Instant::now());
gst::info!(CAT, "First stats logs in {:2?}", 2 * LOG_PERIOD);
}
pub fn is_active(&mut self) -> bool {
if let Some(ramp_up_instant) = self.ramp_up_instant {
if ramp_up_instant.elapsed() < LOG_PERIOD {
return false;
}
self.ramp_up_instant = None;
gst::info!(CAT, "Ramp up complete. Stats logs in {:2?}", LOG_PERIOD);
self.log_start_instant = Some(Instant::now());
self.last_delta_instant = self.log_start_instant;
#[cfg(feature = "tuning")]
{
self.parked_duration_init = Context::current().unwrap().parked_duration();
}
}
use std::cmp::Ordering::*;
match self.max_buffers.opt_cmp(self.buffer_count) {
Some(Equal) => {
self.log_global();
self.buffer_count += 1.0;
false
}
Some(Less) => false,
_ => true,
}
}
pub fn add_buffer(&mut self, latency: Duration, interval: Duration) {
if !self.is_active() {
return;
}
self.buffer_count += 1.0;
self.buffer_count_delta += 1.0;
// Latency
let latency_f32 = latency.as_nanos() as f32;
let latency_square = latency_f32.powi(2);
self.latency_sum += latency_f32;
self.latency_square_sum += latency_square;
self.latency_min = self.latency_min.min(latency);
self.latency_max = self.latency_max.max(latency);
self.latency_sum_delta += latency_f32;
self.latency_square_sum_delta += latency_square;
self.latency_min_delta = self.latency_min_delta.min(latency);
self.latency_max_delta = self.latency_max_delta.max(latency);
// Interval
let interval_f32 = interval.as_nanos() as f32;
let interval_square = interval_f32.powi(2);
self.interval_sum += interval_f32;
self.interval_square_sum += interval_square;
self.interval_min = self.interval_min.min(interval);
self.interval_max = self.interval_max.max(interval);
self.interval_sum_delta += interval_f32;
self.interval_square_sum_delta += interval_square;
self.interval_min_delta = self.interval_min_delta.min(interval);
self.interval_max_delta = self.interval_max_delta.max(interval);
if interval > self.interval_late_warn {
self.interval_late_count += 1.0;
self.interval_late_count_delta += 1.0;
}
let delta_duration = match self.last_delta_instant {
Some(last_delta) => last_delta.elapsed(),
None => return,
};
if delta_duration < LOG_PERIOD {
return;
}
self.last_delta_instant = Some(Instant::now());
gst::info!(CAT, "Delta stats:");
let interval_mean = self.interval_sum_delta / self.buffer_count_delta;
let interval_std_dev = f32::sqrt(
self.interval_square_sum_delta / self.buffer_count_delta - interval_mean.powi(2),
);
gst::info!(
CAT,
"o interval: mean {:4.2?} σ {:4.1?} [{:4.1?}, {:4.1?}]",
Duration::from_nanos(interval_mean as u64),
Duration::from_nanos(interval_std_dev as u64),
self.interval_min_delta,
self.interval_max_delta,
);
if self.interval_late_count_delta > f32::EPSILON {
gst::warning!(
CAT,
"o {:5.2}% late buffers",
100f32 * self.interval_late_count_delta / self.buffer_count_delta
);
}
self.interval_sum_delta = 0.0;
self.interval_square_sum_delta = 0.0;
self.interval_min_delta = Duration::MAX;
self.interval_max_delta = Duration::ZERO;
self.interval_late_count_delta = 0.0;
let latency_mean = self.latency_sum_delta / self.buffer_count_delta;
let latency_std_dev = f32::sqrt(
self.latency_square_sum_delta / self.buffer_count_delta - latency_mean.powi(2),
);
gst::info!(
CAT,
"o latency: mean {:4.2?} σ {:4.1?} [{:4.1?}, {:4.1?}]",
Duration::from_nanos(latency_mean as u64),
Duration::from_nanos(latency_std_dev as u64),
self.latency_min_delta,
self.latency_max_delta,
);
self.latency_sum_delta = 0.0;
self.latency_square_sum_delta = 0.0;
self.latency_min_delta = Duration::MAX;
self.latency_max_delta = Duration::ZERO;
self.buffer_count_delta = 0.0;
}
pub fn log_global(&mut self) {
if self.buffer_count < 1.0 {
return;
}
let _log_start = if let Some(log_start) = self.log_start_instant {
log_start
} else {
return;
};
gst::info!(CAT, "Global stats:");
#[cfg(feature = "tuning")]
{
let duration = _log_start.elapsed();
let parked_duration =
Context::current().unwrap().parked_duration() - self.parked_duration_init;
gst::info!(
CAT,
"o parked: {parked_duration:4.2?} ({:5.2?}%)",
(parked_duration.as_nanos() as f32 * 100.0 / duration.as_nanos() as f32)
);
}
let interval_mean = self.interval_sum / self.buffer_count;
let interval_std_dev =
f32::sqrt(self.interval_square_sum / self.buffer_count - interval_mean.powi(2));
gst::info!(
CAT,
"o interval: mean {:4.2?} σ {:4.1?} [{:4.1?}, {:4.1?}]",
Duration::from_nanos(interval_mean as u64),
Duration::from_nanos(interval_std_dev as u64),
self.interval_min,
self.interval_max,
);
if self.interval_late_count > f32::EPSILON {
gst::warning!(
CAT,
"o {:5.2}% late buffers",
100f32 * self.interval_late_count / self.buffer_count
);
}
let latency_mean = self.latency_sum / self.buffer_count;
let latency_std_dev =
f32::sqrt(self.latency_square_sum / self.buffer_count - latency_mean.powi(2));
gst::info!(
CAT,
"o latency: mean {:4.2?} σ {:4.1?} [{:4.1?}, {:4.1?}]",
Duration::from_nanos(latency_mean as u64),
Duration::from_nanos(latency_std_dev as u64),
self.latency_min,
self.latency_max,
);
}
}

View file

@ -0,0 +1,327 @@
// Copyright (C) 2022 François Laignel <fengalin@free.fr>
//
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at
// <https://mozilla.org/MPL/2.0/>.
//
// SPDX-License-Identifier: MPL-2.0
use futures::future::BoxFuture;
use futures::prelude::*;
use gst::glib;
use gst::prelude::*;
use gst::subclass::prelude::*;
use gst::EventView;
use once_cell::sync::Lazy;
use gstthreadshare::runtime::{prelude::*, PadSink};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use super::super::{Settings, Stats, CAT};
#[derive(Debug, Default)]
struct PadSinkHandlerInner {
is_flushing: bool,
is_main_elem: bool,
last_dts: Option<gst::ClockTime>,
segment_start: Option<gst::ClockTime>,
stats: Option<Box<Stats>>,
}
impl PadSinkHandlerInner {
fn handle_buffer(
&mut self,
elem: &super::DirectSink,
buffer: gst::Buffer,
) -> Result<(), gst::FlowError> {
if self.is_flushing {
log_or_trace!(
CAT,
self.is_main_elem,
obj: elem,
"Discarding {buffer:?} (flushing)"
);
return Err(gst::FlowError::Flushing);
}
debug_or_trace!(CAT, self.is_main_elem, obj: elem, "Received {buffer:?}");
let dts = buffer
.dts()
.expect("Buffer without dts")
.checked_sub(self.segment_start.expect("Buffer without Time Segment"))
.expect("dts before Segment start");
if let Some(last_dts) = self.last_dts {
let cur_ts = elem.current_running_time().unwrap();
let latency: Duration = (cur_ts - dts).into();
let interval: Duration = (dts - last_dts).into();
if let Some(stats) = self.stats.as_mut() {
stats.add_buffer(latency, interval);
}
debug_or_trace!(CAT, self.is_main_elem, obj: elem, "o latency {latency:.2?}");
debug_or_trace!(
CAT,
self.is_main_elem,
obj: elem,
"o interval {interval:.2?}",
);
}
self.last_dts = Some(dts);
log_or_trace!(CAT, self.is_main_elem, obj: elem, "Buffer processed");
Ok(())
}
}
#[derive(Clone, Debug, Default)]
struct SyncPadSinkHandler(Arc<Mutex<PadSinkHandlerInner>>);
impl PadSinkHandler for SyncPadSinkHandler {
type ElementImpl = DirectSink;
fn sink_chain(
self,
_pad: gst::Pad,
elem: super::DirectSink,
buffer: gst::Buffer,
) -> BoxFuture<'static, Result<gst::FlowSuccess, gst::FlowError>> {
async move {
if self.0.lock().unwrap().handle_buffer(&elem, buffer).is_err() {
return Err(gst::FlowError::Flushing);
}
Ok(gst::FlowSuccess::Ok)
}
.boxed()
}
fn sink_event_serialized(
self,
_pad: gst::Pad,
elem: super::DirectSink,
event: gst::Event,
) -> BoxFuture<'static, bool> {
async move {
match event.view() {
EventView::Eos(_) => {
{
let mut inner = self.0.lock().unwrap();
debug_or_trace!(CAT, inner.is_main_elem, obj: elem, "EOS");
inner.is_flushing = true;
}
// When each element sends its own EOS message,
// it takes ages for the pipeline to process all of them.
// Let's just post an error message and let main shuts down
// after all streams have posted this message.
let _ = elem
.post_message(gst::message::Error::new(gst::LibraryError::Shutdown, "EOS"));
}
EventView::FlushStop(_) => {
self.0.lock().unwrap().is_flushing = false;
}
EventView::Segment(evt) => {
if let Some(time_seg) = evt.segment().downcast_ref::<gst::ClockTime>() {
self.0.lock().unwrap().segment_start = time_seg.start();
}
}
EventView::SinkMessage(evt) => {
let _ = elem.post_message(evt.message());
}
_ => (),
}
true
}
.boxed()
}
fn sink_event(self, _pad: &gst::Pad, _imp: &DirectSink, event: gst::Event) -> bool {
if let EventView::FlushStart(..) = event.view() {
self.0.lock().unwrap().is_flushing = true;
}
true
}
}
impl SyncPadSinkHandler {
fn prepare(&self, is_main_elem: bool, stats: Option<Stats>) {
let mut inner = self.0.lock().unwrap();
inner.is_main_elem = is_main_elem;
inner.stats = stats.map(Box::new);
}
fn start(&self) {
let mut inner = self.0.lock().unwrap();
inner.is_flushing = false;
inner.last_dts = None;
if let Some(stats) = inner.stats.as_mut() {
stats.start();
}
}
fn stop(&self) {
let mut inner = self.0.lock().unwrap();
inner.is_flushing = true;
}
}
#[derive(Debug)]
pub struct DirectSink {
sink_pad: PadSink,
sink_pad_handler: SyncPadSinkHandler,
settings: Mutex<Settings>,
}
impl DirectSink {
fn prepare(&self) -> Result<(), gst::ErrorMessage> {
let settings = self.settings.lock().unwrap();
debug_or_trace!(CAT, settings.is_main_elem, imp: self, "Preparing");
let stats = if settings.logs_stats {
Some(Stats::new(
settings.max_buffers,
settings.push_period + settings.context_wait / 2,
))
} else {
None
};
self.sink_pad_handler.prepare(settings.is_main_elem, stats);
debug_or_trace!(CAT, settings.is_main_elem, imp: self, "Prepared");
Ok(())
}
fn stop(&self) -> Result<(), gst::ErrorMessage> {
let is_main_elem = self.settings.lock().unwrap().is_main_elem;
debug_or_trace!(CAT, is_main_elem, imp: self, "Stopping");
self.sink_pad_handler.stop();
debug_or_trace!(CAT, is_main_elem, imp: self, "Stopped");
Ok(())
}
fn start(&self) -> Result<(), gst::ErrorMessage> {
let is_main_elem = self.settings.lock().unwrap().is_main_elem;
debug_or_trace!(CAT, is_main_elem, imp: self, "Starting");
self.sink_pad_handler.start();
debug_or_trace!(CAT, is_main_elem, imp: self, "Started");
Ok(())
}
}
#[glib::object_subclass]
impl ObjectSubclass for DirectSink {
const NAME: &'static str = "TsStandaloneDirectSink";
type Type = super::DirectSink;
type ParentType = gst::Element;
fn with_class(klass: &Self::Class) -> Self {
let sink_pad_handler = SyncPadSinkHandler::default();
Self {
sink_pad: PadSink::new(
gst::Pad::from_template(&klass.pad_template("sink").unwrap(), Some("sink")),
sink_pad_handler.clone(),
),
sink_pad_handler,
settings: Default::default(),
}
}
}
impl ObjectImpl for DirectSink {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(Settings::properties);
PROPERTIES.as_ref()
}
fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
self.settings.lock().unwrap().set_property(id, value, pspec);
}
fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value {
self.settings.lock().unwrap().property(id, pspec)
}
fn constructed(&self) {
self.parent_constructed();
let obj = self.obj();
obj.add_pad(self.sink_pad.gst_pad()).unwrap();
obj.set_element_flags(gst::ElementFlags::SINK);
}
}
impl GstObjectImpl for DirectSink {}
impl ElementImpl for DirectSink {
fn metadata() -> Option<&'static gst::subclass::ElementMetadata> {
static ELEMENT_METADATA: Lazy<gst::subclass::ElementMetadata> = Lazy::new(|| {
gst::subclass::ElementMetadata::new(
"Thread-sharing standalone test direct sink",
"Sink/Test",
"Thread-sharing standalone test direct sink",
"François Laignel <fengalin@free.fr>",
)
});
Some(&*ELEMENT_METADATA)
}
fn pad_templates() -> &'static [gst::PadTemplate] {
static PAD_TEMPLATES: Lazy<Vec<gst::PadTemplate>> = Lazy::new(|| {
let caps = gst::Caps::new_any();
let sink_pad_template = gst::PadTemplate::new(
"sink",
gst::PadDirection::Sink,
gst::PadPresence::Always,
&caps,
)
.unwrap();
vec![sink_pad_template]
});
PAD_TEMPLATES.as_ref()
}
fn change_state(
&self,
transition: gst::StateChange,
) -> Result<gst::StateChangeSuccess, gst::StateChangeError> {
gst::trace!(CAT, imp: self, "Changing state {transition:?}");
match transition {
gst::StateChange::NullToReady => {
self.prepare().map_err(|err| {
self.post_error_message(err);
gst::StateChangeError
})?;
}
gst::StateChange::ReadyToPaused => {
self.start().map_err(|_| gst::StateChangeError)?;
}
gst::StateChange::PausedToReady => {
self.stop().map_err(|_| gst::StateChangeError)?;
}
_ => (),
}
self.parent_change_state(transition)
}
}

View file

@ -0,0 +1,17 @@
use gst::glib;
use gst::prelude::*;
mod imp;
glib::wrapper! {
pub struct DirectSink(ObjectSubclass<imp::DirectSink>) @extends gst::Element, gst::Object;
}
pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
gst::Element::register(
Some(plugin),
super::SYNC_MUTEX_ELEMENT_NAME,
gst::Rank::None,
DirectSink::static_type(),
)
}

View file

@ -0,0 +1,402 @@
// Copyright (C) 2022 François Laignel <fengalin@free.fr>
//
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at
// <https://mozilla.org/MPL/2.0/>.
//
// SPDX-License-Identifier: MPL-2.0
use futures::future::BoxFuture;
use futures::prelude::*;
use gst::error_msg;
use gst::glib;
use gst::prelude::*;
use gst::subclass::prelude::*;
use gst::EventView;
use once_cell::sync::Lazy;
use gstthreadshare::runtime::prelude::*;
use gstthreadshare::runtime::{Context, PadSink, Task};
use std::sync::Mutex;
use std::time::Duration;
use super::super::{Settings, Stats, CAT};
#[derive(Debug)]
enum StreamItem {
Buffer(gst::Buffer),
Event(gst::Event),
}
#[derive(Clone, Debug)]
struct TaskPadSinkHandler;
impl PadSinkHandler for TaskPadSinkHandler {
type ElementImpl = TaskSink;
fn sink_chain(
self,
_pad: gst::Pad,
elem: super::TaskSink,
buffer: gst::Buffer,
) -> BoxFuture<'static, Result<gst::FlowSuccess, gst::FlowError>> {
let sender = elem.imp().clone_item_sender();
async move {
if sender.send_async(StreamItem::Buffer(buffer)).await.is_err() {
return Err(gst::FlowError::Flushing);
}
Ok(gst::FlowSuccess::Ok)
}
.boxed()
}
fn sink_event_serialized(
self,
_pad: gst::Pad,
elem: super::TaskSink,
event: gst::Event,
) -> BoxFuture<'static, bool> {
let sender = elem.imp().clone_item_sender();
async move {
match event.view() {
EventView::Segment(_) => {
let _ = sender.send_async(StreamItem::Event(event)).await;
}
EventView::Eos(_) => {
let is_main_elem = elem.imp().settings.lock().unwrap().is_main_elem;
debug_or_trace!(CAT, is_main_elem, obj: elem, "EOS");
// When each element sends its own EOS message,
// it takes ages for the pipeline to process all of them.
// Let's just post an error message and let main shuts down
// after all streams have posted this message.
let _ = elem
.post_message(gst::message::Error::new(gst::LibraryError::Shutdown, "EOS"));
}
EventView::FlushStop(_) => {
let imp = elem.imp();
return imp.task.flush_stop().await_maybe_on_context().is_ok();
}
EventView::SinkMessage(evt) => {
let _ = elem.post_message(evt.message());
}
_ => (),
}
true
}
.boxed()
}
fn sink_event(self, _pad: &gst::Pad, imp: &TaskSink, event: gst::Event) -> bool {
if let EventView::FlushStart(..) = event.view() {
return imp.task.flush_start().await_maybe_on_context().is_ok();
}
true
}
}
struct TaskSinkTask {
elem: super::TaskSink,
item_receiver: flume::Receiver<StreamItem>,
is_main_elem: bool,
last_dts: Option<gst::ClockTime>,
segment_start: Option<gst::ClockTime>,
stats: Option<Box<Stats>>,
}
impl TaskSinkTask {
fn new(
elem: &super::TaskSink,
item_receiver: flume::Receiver<StreamItem>,
is_main_elem: bool,
stats: Option<Box<Stats>>,
) -> Self {
TaskSinkTask {
elem: elem.clone(),
item_receiver,
is_main_elem,
last_dts: None,
stats,
segment_start: None,
}
}
fn flush(&mut self) {
// Purge the channel
while !self.item_receiver.is_empty() {}
}
}
impl TaskImpl for TaskSinkTask {
type Item = StreamItem;
fn prepare(&mut self) -> BoxFuture<'_, Result<(), gst::ErrorMessage>> {
log_or_trace!(CAT, self.is_main_elem, obj: self.elem, "Preparing Task");
future::ok(()).boxed()
}
fn start(&mut self) -> BoxFuture<'_, Result<(), gst::ErrorMessage>> {
async {
log_or_trace!(CAT, self.is_main_elem, obj: self.elem, "Starting Task");
self.last_dts = None;
if let Some(stats) = self.stats.as_mut() {
stats.start();
}
Ok(())
}
.boxed()
}
fn stop(&mut self) -> BoxFuture<'_, Result<(), gst::ErrorMessage>> {
async {
log_or_trace!(CAT, self.is_main_elem, obj: self.elem, "Stopping Task");
self.flush();
Ok(())
}
.boxed()
}
fn try_next(&mut self) -> BoxFuture<'_, Result<StreamItem, gst::FlowError>> {
self.item_receiver
.recv_async()
.map(|opt_item| Ok(opt_item.unwrap()))
.boxed()
}
fn handle_item(&mut self, item: StreamItem) -> BoxFuture<'_, Result<(), gst::FlowError>> {
async move {
debug_or_trace!(CAT, self.is_main_elem, obj: self.elem, "Received {item:?}");
match item {
StreamItem::Buffer(buffer) => {
let dts = buffer
.dts()
.expect("Buffer without dts")
.checked_sub(self.segment_start.expect("Buffer without Time Segment"))
.expect("dts before Segment start");
if let Some(last_dts) = self.last_dts {
let cur_ts = self.elem.current_running_time().unwrap();
let latency: Duration = (cur_ts - dts).into();
let interval: Duration = (dts - last_dts).into();
if let Some(stats) = self.stats.as_mut() {
stats.add_buffer(latency, interval);
}
debug_or_trace!(
CAT,
self.is_main_elem,
obj: self.elem,
"o latency {latency:.2?}",
);
debug_or_trace!(
CAT,
self.is_main_elem,
obj: self.elem,
"o interval {interval:.2?}",
);
}
self.last_dts = Some(dts);
log_or_trace!(CAT, self.is_main_elem, obj: self.elem, "Buffer processed");
}
StreamItem::Event(evt) => {
if let EventView::Segment(evt) = evt.view() {
if let Some(time_seg) = evt.segment().downcast_ref::<gst::ClockTime>() {
self.segment_start = time_seg.start();
}
}
}
}
Ok(())
}
.boxed()
}
}
#[derive(Debug)]
pub struct TaskSink {
sink_pad: PadSink,
task: Task,
item_sender: Mutex<Option<flume::Sender<StreamItem>>>,
settings: Mutex<Settings>,
}
impl TaskSink {
#[track_caller]
fn clone_item_sender(&self) -> flume::Sender<StreamItem> {
self.item_sender.lock().unwrap().as_ref().unwrap().clone()
}
fn prepare(&self) -> Result<(), gst::ErrorMessage> {
let settings = self.settings.lock().unwrap();
let stats = if settings.logs_stats {
Some(Box::new(Stats::new(
settings.max_buffers,
settings.push_period + settings.context_wait / 2,
)))
} else {
None
};
debug_or_trace!(CAT, settings.is_main_elem, imp: self, "Preparing");
let ts_ctx = Context::acquire(&settings.context, settings.context_wait).map_err(|err| {
error_msg!(
gst::ResourceError::OpenWrite,
["Failed to acquire Context: {}", err]
)
})?;
// Enable backpressure for items
let (item_sender, item_receiver) = flume::bounded(0);
let task_impl = TaskSinkTask::new(&self.obj(), item_receiver, settings.is_main_elem, stats);
self.task.prepare(task_impl, ts_ctx).block_on()?;
*self.item_sender.lock().unwrap() = Some(item_sender);
debug_or_trace!(CAT, settings.is_main_elem, imp: self, "Prepared");
Ok(())
}
fn unprepare(&self) {
let is_main_elem = self.settings.lock().unwrap().is_main_elem;
debug_or_trace!(CAT, is_main_elem, imp: self, "Unpreparing");
self.task.unprepare().block_on().unwrap();
debug_or_trace!(CAT, is_main_elem, imp: self, "Unprepared");
}
fn stop(&self) -> Result<(), gst::ErrorMessage> {
let is_main_elem = self.settings.lock().unwrap().is_main_elem;
debug_or_trace!(CAT, is_main_elem, imp: self, "Stopping");
self.task.stop().block_on()?;
debug_or_trace!(CAT, is_main_elem, imp: self, "Stopped");
Ok(())
}
fn start(&self) -> Result<(), gst::ErrorMessage> {
let is_main_elem = self.settings.lock().unwrap().is_main_elem;
debug_or_trace!(CAT, is_main_elem, imp: self, "Starting");
self.task.start().block_on()?;
debug_or_trace!(CAT, is_main_elem, imp: self, "Started");
Ok(())
}
}
#[glib::object_subclass]
impl ObjectSubclass for TaskSink {
const NAME: &'static str = "TsStandaloneTaskSink";
type Type = super::TaskSink;
type ParentType = gst::Element;
fn with_class(klass: &Self::Class) -> Self {
Self {
sink_pad: PadSink::new(
gst::Pad::from_template(&klass.pad_template("sink").unwrap(), Some("sink")),
TaskPadSinkHandler,
),
task: Task::default(),
item_sender: Default::default(),
settings: Default::default(),
}
}
}
impl ObjectImpl for TaskSink {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(Settings::properties);
PROPERTIES.as_ref()
}
fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
self.settings.lock().unwrap().set_property(id, value, pspec);
}
fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value {
self.settings.lock().unwrap().property(id, pspec)
}
fn constructed(&self) {
self.parent_constructed();
let obj = self.obj();
obj.add_pad(self.sink_pad.gst_pad()).unwrap();
obj.set_element_flags(gst::ElementFlags::SINK);
}
}
impl GstObjectImpl for TaskSink {}
impl ElementImpl for TaskSink {
fn metadata() -> Option<&'static gst::subclass::ElementMetadata> {
static ELEMENT_METADATA: Lazy<gst::subclass::ElementMetadata> = Lazy::new(|| {
gst::subclass::ElementMetadata::new(
"Thread-sharing standalone test task sink",
"Sink/Test",
"Thread-sharing standalone test task sink",
"François Laignel <fengalin@free.fr>",
)
});
Some(&*ELEMENT_METADATA)
}
fn pad_templates() -> &'static [gst::PadTemplate] {
static PAD_TEMPLATES: Lazy<Vec<gst::PadTemplate>> = Lazy::new(|| {
let caps = gst::Caps::new_any();
let sink_pad_template = gst::PadTemplate::new(
"sink",
gst::PadDirection::Sink,
gst::PadPresence::Always,
&caps,
)
.unwrap();
vec![sink_pad_template]
});
PAD_TEMPLATES.as_ref()
}
fn change_state(
&self,
transition: gst::StateChange,
) -> Result<gst::StateChangeSuccess, gst::StateChangeError> {
gst::trace!(CAT, imp: self, "Changing state {transition:?}");
match transition {
gst::StateChange::NullToReady => {
self.prepare().map_err(|err| {
self.post_error_message(err);
gst::StateChangeError
})?;
}
gst::StateChange::ReadyToPaused => {
self.start().map_err(|_| gst::StateChangeError)?;
}
gst::StateChange::PausedToReady => {
self.stop().map_err(|_| gst::StateChangeError)?;
}
gst::StateChange::ReadyToNull => {
self.unprepare();
}
_ => (),
}
self.parent_change_state(transition)
}
}

View file

@ -0,0 +1,17 @@
use gst::glib;
use gst::prelude::*;
mod imp;
glib::wrapper! {
pub struct TaskSink(ObjectSubclass<imp::TaskSink>) @extends gst::Element, gst::Object;
}
pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
gst::Element::register(
Some(plugin),
super::TASK_ELEMENT_NAME,
gst::Rank::None,
TaskSink::static_type(),
)
}

View file

@ -19,11 +19,11 @@ use std::sync::Mutex;
use std::time::Duration;
use gstthreadshare::runtime::prelude::*;
use gstthreadshare::runtime::{timer, Context, PadSrc, Task};
use gstthreadshare::runtime::{task, timer, Context, PadSrc, Task};
static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
gst::DebugCategory::new(
"ts-standalone-test-src",
super::ELEMENT_NAME,
gst::DebugColorFlags::empty(),
Some("Thread-sharing standalone test src"),
)
@ -39,7 +39,7 @@ struct Settings {
context: String,
context_wait: Duration,
push_period: gst::ClockTime,
raise_log_level: bool,
is_main_elem: bool,
num_buffers: Option<u32>,
}
@ -49,7 +49,7 @@ impl Default for Settings {
context: DEFAULT_CONTEXT.into(),
context_wait: DEFAULT_CONTEXT_WAIT,
push_period: DEFAULT_PUSH_PERIOD,
raise_log_level: false,
is_main_elem: false,
num_buffers: Some(DEFAULT_NUM_BUFFERS as u32),
}
}
@ -63,19 +63,18 @@ impl PadSrcHandler for TestSrcPadHandler {
#[derive(Debug)]
struct SrcTask {
element: super::TestSrc,
elem: super::TestSrc,
buffer_pool: gst::BufferPool,
timer: Option<timer::Interval>,
raise_log_level: bool,
is_main_elem: bool,
push_period: gst::ClockTime,
need_initial_events: bool,
need_segment: bool,
num_buffers: Option<u32>,
buffer_count: u32,
}
impl SrcTask {
fn new(element: super::TestSrc) -> Self {
fn new(elem: super::TestSrc) -> Self {
let buffer_pool = gst::BufferPool::new();
let mut pool_config = buffer_pool.config();
pool_config
@ -84,13 +83,12 @@ impl SrcTask {
buffer_pool.set_config(pool_config).unwrap();
SrcTask {
element,
elem,
buffer_pool,
timer: None,
raise_log_level: false,
is_main_elem: false,
push_period: gst::ClockTime::ZERO,
need_initial_events: true,
need_segment: true,
num_buffers: Some(DEFAULT_NUM_BUFFERS as u32),
buffer_count: 0,
}
@ -98,34 +96,48 @@ impl SrcTask {
}
impl TaskImpl for SrcTask {
type Item = gst::Buffer;
type Item = ();
fn prepare(&mut self) -> BoxFuture<'_, Result<(), gst::ErrorMessage>> {
async move {
let src = self.element.imp();
let settings = src.settings.lock().unwrap();
self.raise_log_level = settings.raise_log_level;
let imp = self.elem.imp();
let settings = imp.settings.lock().unwrap();
self.is_main_elem = settings.is_main_elem;
if self.raise_log_level {
gst::log!(CAT, obj: self.element, "Preparing Task");
} else {
gst::trace!(CAT, obj: self.element, "Preparing Task");
}
log_or_trace!(CAT, self.is_main_elem, imp: imp, "Preparing Task");
self.push_period = settings.push_period;
self.num_buffers = settings.num_buffers;
self.push_period = settings.push_period;
self.num_buffers = settings.num_buffers;
Ok(())
}
.boxed()
future::ok(()).boxed()
}
fn start(&mut self) -> BoxFuture<'_, Result<(), gst::ErrorMessage>> {
async {
if self.raise_log_level {
gst::log!(CAT, obj: self.element, "Starting Task");
} else {
gst::trace!(CAT, obj: self.element, "Starting Task");
async move {
log_or_trace!(CAT, self.is_main_elem, obj: self.elem, "Starting Task");
if self.need_initial_events {
let imp = self.elem.imp();
debug_or_trace!(CAT, self.is_main_elem, obj: self.elem, "Pushing initial events");
let stream_id =
format!("{:08x}{:08x}", rand::random::<u32>(), rand::random::<u32>());
let stream_start_evt = gst::event::StreamStart::builder(&stream_id)
.group_id(gst::GroupId::next())
.build();
imp.src_pad.push_event(stream_start_evt).await;
imp.src_pad
.push_event(gst::event::Caps::new(
&gst::Caps::builder("foo/bar").build(),
))
.await;
let segment_evt =
gst::event::Segment::new(&gst::FormattedSegment::<gst::format::Time>::new());
imp.src_pad.push_event(segment_evt).await;
self.need_initial_events = false;
}
self.timer = Some(
@ -138,178 +150,100 @@ impl TaskImpl for SrcTask {
);
self.buffer_count = 0;
self.buffer_pool.set_active(true).unwrap();
Ok(())
}
.boxed()
}
fn stop(&mut self) -> BoxFuture<'_, Result<(), gst::ErrorMessage>> {
async move {
if self.raise_log_level {
gst::log!(CAT, obj: self.element, "Stopping Task");
} else {
gst::trace!(CAT, obj: self.element, "Stopping Task");
}
log_or_trace!(CAT, self.is_main_elem, obj: self.elem, "Stopping Task");
self.buffer_pool.set_active(false).unwrap();
self.timer = None;
self.need_initial_events = true;
self.buffer_pool.set_active(false).unwrap();
self.timer = None;
self.need_initial_events = true;
self.need_segment = true;
future::ok(()).boxed()
}
fn try_next(&mut self) -> BoxFuture<'_, Result<(), gst::FlowError>> {
async move {
log_or_trace!(CAT, self.is_main_elem, obj: self.elem, "Awaiting timer");
self.timer.as_mut().unwrap().next().await;
log_or_trace!(CAT, self.is_main_elem, obj: self.elem, "Timer ticked");
Ok(())
}
.boxed()
}
fn try_next(&mut self) -> BoxFuture<'_, Result<gst::Buffer, gst::FlowError>> {
fn handle_item(&mut self, _: ()) -> BoxFuture<'_, Result<(), gst::FlowError>> {
async move {
if self.raise_log_level {
gst::log!(CAT, obj: self.element, "Awaiting timer");
} else {
gst::trace!(CAT, obj: self.element, "Awaiting timer");
}
self.timer.as_mut().unwrap().next().await;
if self.raise_log_level {
gst::log!(CAT, obj: self.element, "Timer ticked");
} else {
gst::trace!(CAT, obj: self.element, "Timer ticked");
}
self.buffer_pool
let buffer = self
.buffer_pool
.acquire_buffer(None)
.map(|mut buffer| {
{
let buffer = buffer.get_mut().unwrap();
let rtime = self.element.current_running_time().unwrap();
let rtime = self.elem.current_running_time().unwrap();
buffer.set_dts(rtime);
}
buffer
})
.map_err(|err| {
gst::error!(CAT, obj: self.element, "Failed to acquire buffer {}", err);
gst::error!(CAT, obj: self.elem, "Failed to acquire buffer {err}");
err
})
})?;
debug_or_trace!(CAT, self.is_main_elem, obj: self.elem, "Forwarding buffer");
self.elem.imp().src_pad.push(buffer).await?;
log_or_trace!(CAT, self.is_main_elem, obj: self.elem, "Successfully pushed buffer");
self.buffer_count += 1;
if self.num_buffers.opt_eq(self.buffer_count) == Some(true) {
return Err(gst::FlowError::Eos);
}
Ok(())
}
.boxed()
}
fn handle_item(&mut self, buffer: gst::Buffer) -> BoxFuture<'_, Result<(), gst::FlowError>> {
fn handle_loop_error(&mut self, err: gst::FlowError) -> BoxFuture<'_, task::Trigger> {
async move {
let res = self.push(buffer).await;
match res {
Ok(_) => {
if self.raise_log_level {
gst::log!(CAT, obj: self.element, "Successfully pushed buffer");
} else {
gst::trace!(CAT, obj: self.element, "Successfully pushed buffer");
}
}
Err(gst::FlowError::Eos) => {
if self.raise_log_level {
gst::debug!(CAT, obj: self.element, "EOS");
} else {
gst::trace!(CAT, obj: self.element, "EOS");
}
let test_src = self.element.imp();
test_src.src_pad.push_event(gst::event::Eos::new()).await;
match err {
gst::FlowError::Eos => {
debug_or_trace!(CAT, self.is_main_elem, obj: self.elem, "Pushing EOS");
return Err(gst::FlowError::Eos);
}
Err(gst::FlowError::Flushing) => {
if self.raise_log_level {
gst::debug!(CAT, obj: self.element, "Flushing");
} else {
gst::trace!(CAT, obj: self.element, "Flushing");
let imp = self.elem.imp();
if !imp.src_pad.push_event(gst::event::Eos::new()).await {
gst::error!(CAT, imp: imp, "Error pushing EOS");
}
task::Trigger::Stop
}
Err(err) => {
gst::error!(CAT, obj: self.element, "Got error {}", err);
gst::FlowError::Flushing => {
debug_or_trace!(CAT, self.is_main_elem, obj: self.elem, "Flushing");
task::Trigger::FlushStart
}
err => {
gst::error!(CAT, obj: self.elem, "Got error {err}");
gst::element_error!(
&self.element,
&self.elem,
gst::StreamError::Failed,
("Internal data stream error"),
["streaming stopped, reason {}", err]
);
task::Trigger::Error
}
}
res.map(drop)
}
.boxed()
}
}
impl SrcTask {
async fn push(&mut self, buffer: gst::Buffer) -> Result<gst::FlowSuccess, gst::FlowError> {
if self.raise_log_level {
gst::debug!(CAT, obj: self.element, "Pushing {:?}", buffer);
} else {
gst::trace!(CAT, obj: self.element, "Pushing {:?}", buffer);
}
let test_src = self.element.imp();
if self.need_initial_events {
if self.raise_log_level {
gst::debug!(CAT, obj: self.element, "Pushing initial events");
} else {
gst::trace!(CAT, obj: self.element, "Pushing initial events");
}
let stream_id = format!("{:08x}{:08x}", rand::random::<u32>(), rand::random::<u32>());
let stream_start_evt = gst::event::StreamStart::builder(&stream_id)
.group_id(gst::GroupId::next())
.build();
test_src.src_pad.push_event(stream_start_evt).await;
test_src
.src_pad
.push_event(gst::event::Caps::new(
&gst::Caps::builder("foo/bar").build(),
))
.await;
self.need_initial_events = false;
}
if self.need_segment {
let segment_evt =
gst::event::Segment::new(&gst::FormattedSegment::<gst::format::Time>::new());
test_src.src_pad.push_event(segment_evt).await;
self.need_segment = false;
}
if self.raise_log_level {
gst::debug!(CAT, obj: self.element, "Forwarding buffer");
} else {
gst::trace!(CAT, obj: self.element, "Forwarding buffer");
}
let ok = test_src.src_pad.push(buffer).await?;
self.buffer_count += 1;
if self.num_buffers.opt_eq(self.buffer_count).unwrap_or(false) {
if self.raise_log_level {
gst::debug!(CAT, obj: self.element, "Pushing EOS");
} else {
gst::trace!(CAT, obj: self.element, "Pushing EOS");
}
let test_src = self.element.imp();
if !test_src.src_pad.push_event(gst::event::Eos::new()).await {
gst::error!(CAT, obj: self.element, "Error pushing EOS");
}
return Err(gst::FlowError::Eos);
}
Ok(ok)
}
}
#[derive(Debug)]
pub struct TestSrc {
src_pad: PadSrc,
@ -319,106 +253,57 @@ pub struct TestSrc {
impl TestSrc {
fn prepare(&self) -> Result<(), gst::ErrorMessage> {
let raise_log_level = self.settings.lock().unwrap().raise_log_level;
if raise_log_level {
gst::debug!(CAT, imp: self, "Preparing");
} else {
gst::trace!(CAT, imp: self, "Preparing");
}
let is_main_elem = self.settings.lock().unwrap().is_main_elem;
debug_or_trace!(CAT, is_main_elem, imp: self, "Preparing");
let settings = self.settings.lock().unwrap();
let context =
Context::acquire(&settings.context, settings.context_wait).map_err(|err| {
gst::error_msg!(
gst::ResourceError::OpenRead,
["Failed to acquire Context: {}", err]
)
})?;
let ts_ctx = Context::acquire(&settings.context, settings.context_wait).map_err(|err| {
gst::error_msg!(
gst::ResourceError::OpenRead,
["Failed to acquire Context: {}", err]
)
})?;
drop(settings);
self.task
.prepare(SrcTask::new(self.obj().clone()), context)
.prepare(SrcTask::new(self.instance().clone()), ts_ctx)
.block_on()?;
if raise_log_level {
gst::debug!(CAT, imp: self, "Prepared");
} else {
gst::trace!(CAT, imp: self, "Prepared");
}
debug_or_trace!(CAT, is_main_elem, imp: self, "Prepared");
Ok(())
}
fn unprepare(&self) {
let raise_log_level = self.settings.lock().unwrap().raise_log_level;
if raise_log_level {
gst::debug!(CAT, imp: self, "Unpreparing");
} else {
gst::trace!(CAT, imp: self, "Unpreparing");
}
let is_main_elem = self.settings.lock().unwrap().is_main_elem;
debug_or_trace!(CAT, is_main_elem, imp: self, "Unpreparing");
self.task.unprepare().block_on().unwrap();
if raise_log_level {
gst::debug!(CAT, imp: self, "Unprepared");
} else {
gst::trace!(CAT, imp: self, "Unprepared");
}
debug_or_trace!(CAT, is_main_elem, imp: self, "Unprepared");
}
fn stop(&self) -> Result<(), gst::ErrorMessage> {
let raise_log_level = self.settings.lock().unwrap().raise_log_level;
if raise_log_level {
gst::debug!(CAT, imp: self, "Stopping");
} else {
gst::trace!(CAT, imp: self, "Stopping");
}
let is_main_elem = self.settings.lock().unwrap().is_main_elem;
debug_or_trace!(CAT, is_main_elem, imp: self, "Stopping");
self.task.stop().block_on()?;
if raise_log_level {
gst::debug!(CAT, imp: self, "Stopped");
} else {
gst::trace!(CAT, imp: self, "Stopped");
}
debug_or_trace!(CAT, is_main_elem, imp: self, "Stopped");
Ok(())
}
fn start(&self) -> Result<(), gst::ErrorMessage> {
let raise_log_level = self.settings.lock().unwrap().raise_log_level;
if raise_log_level {
gst::debug!(CAT, imp: self, "Starting");
} else {
gst::trace!(CAT, imp: self, "Starting");
}
let is_main_elem = self.settings.lock().unwrap().is_main_elem;
debug_or_trace!(CAT, is_main_elem, imp: self, "Starting");
self.task.start().block_on()?;
if raise_log_level {
gst::debug!(CAT, imp: self, "Started");
} else {
gst::trace!(CAT, imp: self, "Started");
}
debug_or_trace!(CAT, is_main_elem, imp: self, "Started");
Ok(())
}
fn pause(&self) -> Result<(), gst::ErrorMessage> {
let raise_log_level = self.settings.lock().unwrap().raise_log_level;
if raise_log_level {
gst::debug!(CAT, imp: self, "Pausing");
} else {
gst::trace!(CAT, imp: self, "Pausing");
}
let is_main_elem = self.settings.lock().unwrap().is_main_elem;
debug_or_trace!(CAT, is_main_elem, imp: self, "Pausing");
self.task.pause().block_on()?;
if raise_log_level {
gst::debug!(CAT, imp: self, "Paused");
} else {
gst::trace!(CAT, imp: self, "Paused");
}
debug_or_trace!(CAT, is_main_elem, imp: self, "Paused");
Ok(())
}
@ -462,9 +347,9 @@ impl ObjectImpl for TestSrc {
.blurb("Push a new buffer every this many ms")
.default_value(DEFAULT_PUSH_PERIOD.mseconds() as u32)
.build(),
glib::ParamSpecBoolean::builder("raise-log-level")
.nick("Raise log level")
.blurb("Raises the log level so that this element stands out")
glib::ParamSpecBoolean::builder("main-elem")
.nick("Main Element")
.blurb("Declare this element as the main one")
.write_only()
.build(),
glib::ParamSpecInt::builder("num-buffers")
@ -485,24 +370,21 @@ impl ObjectImpl for TestSrc {
"context" => {
settings.context = value
.get::<Option<String>>()
.expect("type checked upstream")
.unwrap()
.unwrap_or_else(|| DEFAULT_CONTEXT.into());
}
"context-wait" => {
settings.context_wait = Duration::from_millis(
value.get::<u32>().expect("type checked upstream").into(),
);
settings.context_wait = Duration::from_millis(value.get::<u32>().unwrap().into());
}
"push-period" => {
settings.push_period = gst::ClockTime::from_mseconds(
value.get::<u32>().expect("type checked upstream").into(),
);
let value: u64 = value.get::<u32>().unwrap().into();
settings.push_period = value.mseconds();
}
"raise-log-level" => {
settings.raise_log_level = value.get::<bool>().expect("type checked upstream");
"main-elem" => {
settings.is_main_elem = value.get::<bool>().unwrap();
}
"num-buffers" => {
let value = value.get::<i32>().expect("type checked upstream");
let value = value.get::<i32>().unwrap();
settings.num_buffers = if value > 0 { Some(value as u32) } else { None };
}
_ => unimplemented!(),
@ -515,7 +397,7 @@ impl ObjectImpl for TestSrc {
"context" => settings.context.to_value(),
"context-wait" => (settings.context_wait.as_millis() as u32).to_value(),
"push-period" => (settings.push_period.mseconds() as u32).to_value(),
"raise-log-level" => settings.raise_log_level.to_value(),
"main-elem" => settings.is_main_elem.to_value(),
"num-buffers" => settings
.num_buffers
.and_then(|val| val.try_into().ok())
@ -571,7 +453,7 @@ impl ElementImpl for TestSrc {
&self,
transition: gst::StateChange,
) -> Result<gst::StateChangeSuccess, gst::StateChangeError> {
gst::trace!(CAT, imp: self, "Changing state {:?}", transition);
gst::trace!(CAT, imp: self, "Changing state {transition:?}");
match transition {
gst::StateChange::NullToReady => {

View file

@ -3,6 +3,8 @@ use gst::prelude::*;
mod imp;
pub const ELEMENT_NAME: &str = "ts-standalone-src";
glib::wrapper! {
pub struct TestSrc(ObjectSubclass<imp::TestSrc>) @extends gst::Element, gst::Object;
}
@ -10,7 +12,7 @@ glib::wrapper! {
pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
gst::Element::register(
Some(plugin),
"ts-standalone-test-src",
"ts-standalone-src",
gst::Rank::None,
TestSrc::static_type(),
)

View file

@ -17,19 +17,45 @@
//
// SPDX-License-Identifier: LGPL-2.1-or-later
use gst::glib;
use gst::prelude::*;
use once_cell::sync::Lazy;
use std::net;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::{env, thread, time};
static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
gst::DebugCategory::new(
"ts-udpsrc-benchmark-sender",
gst::DebugColorFlags::empty(),
Some("Thread-sharing UDP src benchmark sender"),
)
});
fn main() {
gst::init().unwrap();
gstthreadshare::plugin_register_static().unwrap();
let args = env::args().collect::<Vec<_>>();
assert!(args.len() > 1);
let n_streams: u16 = args[1].parse().unwrap();
if args.len() > 2 && args[2] == "rtp" {
send_rtp_buffers(n_streams);
let num_buffers: Option<i32> = if args.len() > 3 {
args[3].parse().ok()
} else {
send_raw_buffers(n_streams);
None
};
if args.len() > 2 {
match args[2].as_str() {
"raw" => send_raw_buffers(n_streams),
"rtp" => send_rtp_buffers(n_streams, num_buffers),
_ => send_test_buffers(n_streams, num_buffers),
}
} else {
send_test_buffers(n_streams, num_buffers);
}
}
@ -38,7 +64,7 @@ fn send_raw_buffers(n_streams: u16) {
let socket = net::UdpSocket::bind("0.0.0.0:0").unwrap();
let ipaddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
let destinations = (40000..(40000 + n_streams))
let destinations = (5004..(5004 + n_streams))
.map(|port| SocketAddr::new(ipaddr, port))
.collect::<Vec<_>>();
@ -60,43 +86,60 @@ fn send_raw_buffers(n_streams: u16) {
}
}
fn send_rtp_buffers(n_streams: u16) {
use gst::glib;
use gst::prelude::*;
gst::init().unwrap();
#[cfg(debug_assertions)]
{
use std::path::Path;
let mut path = Path::new("target/debug");
if !path.exists() {
path = Path::new("../../target/debug");
}
gst::Registry::get().scan_path(path);
}
#[cfg(not(debug_assertions))]
{
use std::path::Path;
let mut path = Path::new("target/release");
if !path.exists() {
path = Path::new("../../target/release");
}
gst::Registry::get().scan_path(path);
}
let l = glib::MainLoop::new(None, false);
fn send_test_buffers(n_streams: u16, num_buffers: Option<i32>) {
let pipeline = gst::Pipeline::default();
for i in 0..n_streams {
let src = gst::ElementFactory::make("audiotestsrc")
.name(format!("audiotestsrc-{}", i).as_str())
let src = gst::ElementFactory::make("ts-audiotestsrc")
.name(format!("ts-audiotestsrc-{}", i).as_str())
.property("context-wait", 20u32)
.property("is-live", true)
.property("do-timestamp", true)
.build()
.unwrap();
src.set_property("is-live", true);
if let Some(num_buffers) = num_buffers {
src.set_property("num-buffers", num_buffers);
}
#[cfg(feature = "tuning")]
if i == 0 {
src.set_property("main-elem", true);
}
let sink = gst::ElementFactory::make("ts-udpsink")
.name(format!("udpsink-{}", i).as_str())
.property("clients", format!("127.0.0.1:{}", i + 5004))
.property("context-wait", 20u32)
.build()
.unwrap();
let elements = &[&src, &sink];
pipeline.add_many(elements).unwrap();
gst::Element::link_many(elements).unwrap();
}
run(pipeline);
}
fn send_rtp_buffers(n_streams: u16, num_buffers: Option<i32>) {
let pipeline = gst::Pipeline::default();
for i in 0..n_streams {
let src = gst::ElementFactory::make("ts-audiotestsrc")
.name(format!("ts-audiotestsrc-{}", i).as_str())
.property("context-wait", 20u32)
.property("is-live", true)
.property("do-timestamp", true)
.build()
.unwrap();
if let Some(num_buffers) = num_buffers {
src.set_property("num-buffers", num_buffers);
}
#[cfg(feature = "tuning")]
if i == 0 {
src.set_property("main-elem", true);
}
let enc = gst::ElementFactory::make("alawenc")
.name(format!("alawenc-{}", i).as_str())
@ -106,11 +149,11 @@ fn send_rtp_buffers(n_streams: u16) {
.name(format!("rtppcmapay-{}", i).as_str())
.build()
.unwrap();
let sink = gst::ElementFactory::make("ts-udpsink")
.name(format!("udpsink-{}", i).as_str())
.property("clients", format!("127.0.0.1:{}", i + 40000))
.property("context", "context-udpsink")
.property("context-wait", 20u32)
.property("clients", format!("127.0.0.1:{}", i + 5004))
.build()
.unwrap();
@ -119,6 +162,42 @@ fn send_rtp_buffers(n_streams: u16) {
gst::Element::link_many(elements).unwrap();
}
run(pipeline);
}
fn run(pipeline: gst::Pipeline) {
let l = glib::MainLoop::new(None, false);
let bus = pipeline.bus().unwrap();
let l_clone = l.clone();
bus.add_watch(move |_, msg| {
use gst::MessageView;
match msg.view() {
MessageView::Eos(_) => {
gst::info!(CAT, "Received eos");
l_clone.quit();
glib::Continue(false)
}
MessageView::Error(msg) => {
gst::error!(
CAT,
"Error from {:?}: {} ({:?})",
msg.src().map(|s| s.path_string()),
msg.error(),
msg.debug()
);
l_clone.quit();
glib::Continue(false)
}
_ => glib::Continue(true),
}
})
.expect("Failed to add bus watch");
pipeline.set_state(gst::State::Playing).unwrap();
l.run();
pipeline.set_state(gst::State::Null).unwrap();
}

View file

@ -0,0 +1,735 @@
// Copyright (C) 2022 François Laignel <fengalin@free.fr>
//
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at
// <https://mozilla.org/MPL/2.0/>.
//
// SPDX-License-Identifier: MPL-2.0
use futures::future::BoxFuture;
use futures::prelude::*;
use gst::glib;
use gst::prelude::*;
use gst::subclass::prelude::*;
use once_cell::sync::Lazy;
use std::mem::size_of;
use std::sync::Mutex;
use std::time::Duration;
#[cfg(feature = "tuning")]
use std::time::Instant;
use crate::runtime::prelude::*;
use crate::runtime::{self, task, timer, PadSrc, Task};
static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
gst::DebugCategory::new(
"ts-audiotestsrc",
gst::DebugColorFlags::empty(),
Some("Thread-sharing audio test src"),
)
});
const DEFAULT_CONTEXT: &str = "";
const DEFAULT_CONTEXT_WAIT: Duration = Duration::ZERO;
const DEFAULT_BUFFER_DURATION: gst::ClockTime = gst::ClockTime::from_mseconds(10);
const DEFAULT_DO_TIMESTAMP: bool = false;
const DEFAULT_IS_LIVE: bool = false;
const DEFAULT_NUM_BUFFERS: i32 = -1;
const DEFAULT_CHANNELS: usize = 1;
const DEFAULT_FREQ: f32 = 440.0;
const DEFAULT_VOLUME: f32 = 0.8;
const DEFAULT_RATE: u32 = 44_100;
#[cfg(feature = "tuning")]
const RAMPUP_BUFFER_COUNT: u32 = 500;
#[cfg(feature = "tuning")]
const LOG_BUFFER_INTERVAL: u32 = 2000;
static DEFAULT_CAPS: Lazy<gst::Caps> = Lazy::new(|| {
gst_audio::AudioCapsBuilder::new_interleaved()
.format(gst_audio::AUDIO_FORMAT_S16)
.rate_range(8_000..i32::MAX)
.channels_range(1..i32::MAX)
.build()
});
#[derive(Debug, Clone)]
struct Settings {
context: String,
context_wait: Duration,
do_timestamp: bool,
is_live: bool,
buffer_duration: gst::ClockTime,
num_buffers: Option<u32>,
#[cfg(feature = "tuning")]
is_main_elem: bool,
}
impl Default for Settings {
fn default() -> Self {
Settings {
context: DEFAULT_CONTEXT.into(),
context_wait: DEFAULT_CONTEXT_WAIT,
do_timestamp: DEFAULT_DO_TIMESTAMP,
is_live: DEFAULT_IS_LIVE,
buffer_duration: DEFAULT_BUFFER_DURATION,
num_buffers: None,
#[cfg(feature = "tuning")]
is_main_elem: false,
}
}
}
#[derive(Clone, Debug)]
struct AudioTestSrcPadHandler;
impl PadSrcHandler for AudioTestSrcPadHandler {
type ElementImpl = AudioTestSrc;
fn src_query(self, pad: &gst::Pad, imp: &Self::ElementImpl, query: &mut gst::QueryRef) -> bool {
gst::debug!(CAT, obj: pad, "Received {query:?}");
if let gst::QueryViewMut::Latency(q) = query.view_mut() {
let settings = imp.settings.lock().unwrap();
let min_latency = if settings.is_live {
settings.buffer_duration
} else {
gst::ClockTime::ZERO
};
q.set(
settings.is_live,
min_latency,
min_latency
+ runtime::Context::current().map_or(gst::ClockTime::ZERO, |ctx| {
gst::ClockTime::try_from(ctx.wait_duration()).unwrap()
}),
);
return true;
}
gst::Pad::query_default(pad, Some(&*imp.obj()), query)
}
}
#[derive(Debug, Copy, Clone)]
enum Negotiation {
Unchanged,
Changed,
}
impl Negotiation {
fn has_changed(self) -> bool {
matches!(self, Negotiation::Changed)
}
}
#[derive(Debug)]
struct AudioTestSrcTask {
elem: super::AudioTestSrc,
buffer_pool: gst::BufferPool,
rate: u32,
channels: usize,
do_timestamp: bool,
is_live: bool,
buffer_duration: gst::ClockTime,
need_initial_events: bool,
step: f32,
accumulator: f32,
last_buffer_end: Option<gst::ClockTime>,
caps: gst::Caps,
buffer_count: u32,
num_buffers: Option<u32>,
#[cfg(feature = "tuning")]
is_main_elem: bool,
#[cfg(feature = "tuning")]
parked_duration_init: Option<Duration>,
#[cfg(feature = "tuning")]
log_start: Instant,
}
impl AudioTestSrcTask {
fn new(elem: super::AudioTestSrc) -> Self {
AudioTestSrcTask {
elem,
buffer_pool: gst::BufferPool::new(),
rate: DEFAULT_RATE,
channels: DEFAULT_CHANNELS,
do_timestamp: DEFAULT_DO_TIMESTAMP,
is_live: DEFAULT_IS_LIVE,
buffer_duration: DEFAULT_BUFFER_DURATION,
need_initial_events: true,
step: 0.0,
accumulator: 0.0,
last_buffer_end: None,
caps: gst::Caps::new_empty(),
buffer_count: 0,
num_buffers: None,
#[cfg(feature = "tuning")]
is_main_elem: false,
#[cfg(feature = "tuning")]
parked_duration_init: None,
#[cfg(feature = "tuning")]
log_start: Instant::now(),
}
}
async fn negotiate(&mut self) -> Result<Negotiation, gst::ErrorMessage> {
let imp = self.elem.imp();
let pad = imp.src_pad.gst_pad();
if !pad.check_reconfigure() {
return Ok(Negotiation::Unchanged);
}
let mut caps = pad.peer_query_caps(Some(&DEFAULT_CAPS));
gst::debug!(CAT, imp: imp, "Peer returned {caps:?}");
if caps.is_empty() {
pad.mark_reconfigure();
let err = gst::error_msg!(gst::CoreError::Pad, ["No common Caps"]);
gst::error!(CAT, imp: imp, "{err}");
return Err(err);
}
if caps.is_any() {
gst::debug!(CAT, imp: imp, "Using our own Caps");
caps = DEFAULT_CAPS.clone();
}
{
let caps = caps.make_mut();
let s = caps.structure_mut(0).ok_or_else(|| {
let err = gst::error_msg!(gst::CoreError::Pad, ["Invalid peer Caps structure"]);
gst::error!(CAT, imp: imp, "{err}");
err
})?;
s.fixate_field_nearest_int("rate", DEFAULT_RATE as i32);
self.rate = s.get::<i32>("rate").unwrap() as u32;
self.step = 2.0 * std::f32::consts::PI * DEFAULT_FREQ / (self.rate as f32);
s.fixate_field_nearest_int("channels", DEFAULT_CHANNELS as i32);
self.channels = s.get::<i32>("channels").unwrap() as usize;
if self.channels > 2 {
s.set::<gst::Bitmask>(
"channel-mask",
gst_audio::AudioChannelPosition::fallback_mask(self.channels as u32).into(),
);
}
}
caps.fixate();
gst::debug!(CAT, imp: imp, "fixated to {caps:?}");
imp.src_pad.push_event(gst::event::Caps::new(&caps)).await;
self.caps = caps;
Ok(Negotiation::Changed)
}
}
impl TaskImpl for AudioTestSrcTask {
type Item = gst::Buffer;
fn prepare(&mut self) -> BoxFuture<'_, Result<(), gst::ErrorMessage>> {
gst::log!(CAT, obj: self.elem, "Preparing Task");
let imp = self.elem.imp();
let settings = imp.settings.lock().unwrap();
self.do_timestamp = settings.do_timestamp;
self.is_live = settings.is_live;
self.buffer_duration = settings.buffer_duration;
self.num_buffers = settings.num_buffers;
#[cfg(feature = "tuning")]
{
self.is_main_elem = settings.is_main_elem;
}
future::ok(()).boxed()
}
fn start(&mut self) -> BoxFuture<'_, Result<(), gst::ErrorMessage>> {
async move {
gst::log!(CAT, obj: self.elem, "Starting Task");
if self.need_initial_events {
gst::debug!(CAT, obj: self.elem, "Pushing initial events");
let stream_id =
format!("{:08x}{:08x}", rand::random::<u32>(), rand::random::<u32>());
let stream_start_evt = gst::event::StreamStart::builder(&stream_id)
.group_id(gst::GroupId::next())
.build();
self.elem.imp().src_pad.push_event(stream_start_evt).await;
}
if self.negotiate().await?.has_changed() {
let bytes_per_buffer = (self.rate as u64)
* self.buffer_duration.mseconds()
* self.channels as u64
* size_of::<i16>() as u64
/ 1_000;
let mut pool_config = self.buffer_pool.config();
pool_config
.as_mut()
.set_params(Some(&self.caps), bytes_per_buffer as u32, 2, 6);
self.buffer_pool.set_config(pool_config).unwrap();
}
assert!(!self.caps.is_empty());
self.buffer_pool.set_active(true).unwrap();
if self.need_initial_events {
let segment_evt =
gst::event::Segment::new(&gst::FormattedSegment::<gst::format::Time>::new());
self.elem.imp().src_pad.push_event(segment_evt).await;
self.need_initial_events = false;
}
self.buffer_count = 0;
#[cfg(feature = "tuning")]
if self.is_main_elem {
self.parked_duration_init = None;
}
Ok(())
}
.boxed()
}
fn pause(&mut self) -> BoxFuture<'_, Result<(), gst::ErrorMessage>> {
gst::log!(CAT, obj: self.elem, "Pausing Task");
self.buffer_pool.set_active(false).unwrap();
future::ok(()).boxed()
}
fn stop(&mut self) -> BoxFuture<'_, Result<(), gst::ErrorMessage>> {
gst::log!(CAT, obj: self.elem, "Stopping Task");
self.need_initial_events = true;
self.accumulator = 0.0;
self.last_buffer_end = None;
future::ok(()).boxed()
}
fn try_next(&mut self) -> BoxFuture<'_, Result<gst::Buffer, gst::FlowError>> {
let mut buffer = match self.buffer_pool.acquire_buffer(None) {
Ok(buffer) => buffer,
Err(err) => {
gst::error!(CAT, obj: self.elem, "Failed to acquire buffer {}", err);
return future::err(err).boxed();
}
};
let buffer_mut = buffer.get_mut().unwrap();
let start = if self.is_live | self.do_timestamp {
self.last_buffer_end
.or_else(|| self.elem.current_running_time())
} else {
None
};
{
use std::io::Write;
let mut mapped = buffer_mut.map_writable().unwrap();
let slice = mapped.as_mut_slice();
slice
.chunks_mut(self.channels * size_of::<i16>())
.for_each(|frame| {
let sample = ((self.accumulator.sin() * DEFAULT_VOLUME * (i16::MAX as f32))
as i16)
.to_ne_bytes();
frame.chunks_mut(size_of::<i16>()).for_each(|mut channel| {
let _ = channel.write(&sample).unwrap();
});
self.accumulator += self.step;
if self.accumulator >= 2.0 * std::f32::consts::PI {
self.accumulator = -2.0 * std::f32::consts::PI;
}
});
}
if self.do_timestamp {
buffer_mut.set_dts(start);
buffer_mut.set_duration(self.buffer_duration);
}
self.last_buffer_end = start.opt_add(self.buffer_duration);
async move {
if self.is_live {
if let Some(delay) = self
.last_buffer_end
.unwrap()
.checked_sub(self.elem.current_running_time().unwrap())
{
// Wait for all samples to fit in last time slice
timer::delay_for_at_least(delay.into()).await;
}
} else {
// Let the scheduler share time with other tasks
runtime::executor::yield_now().await;
}
Ok(buffer)
}
.boxed()
}
fn handle_item(&mut self, buffer: gst::Buffer) -> BoxFuture<'_, Result<(), gst::FlowError>> {
async move {
let imp = self.elem.imp();
gst::debug!(CAT, imp: imp, "Pushing {buffer:?}");
imp.src_pad.push(buffer).await?;
gst::log!(CAT, imp: imp, "Successfully pushed buffer");
self.buffer_count += 1;
#[cfg(feature = "tuning")]
if self.is_main_elem {
if let Some(parked_duration_init) = self.parked_duration_init {
if self.buffer_count % LOG_BUFFER_INTERVAL == 0 {
let parked_duration =
runtime::Context::current().unwrap().parked_duration()
- parked_duration_init;
gst::info!(
CAT,
"Parked: {:5.2?}%",
parked_duration.as_nanos() as f32 * 100.0
/ self.log_start.elapsed().as_nanos() as f32,
);
}
} else if self.buffer_count == RAMPUP_BUFFER_COUNT {
self.parked_duration_init =
Some(runtime::Context::current().unwrap().parked_duration());
self.log_start = Instant::now();
gst::info!(CAT, "Ramp up complete");
}
}
if self.num_buffers.opt_eq(self.buffer_count) == Some(true) {
return Err(gst::FlowError::Eos);
}
Ok(())
}
.boxed()
}
fn handle_loop_error(&mut self, err: gst::FlowError) -> BoxFuture<'_, task::Trigger> {
async move {
match err {
gst::FlowError::Flushing => {
gst::debug!(CAT, obj: self.elem, "Flushing");
task::Trigger::FlushStart
}
gst::FlowError::Eos => {
gst::debug!(CAT, obj: self.elem, "EOS");
self.elem
.imp()
.src_pad
.push_event(gst::event::Eos::new())
.await;
task::Trigger::Stop
}
err => {
gst::error!(CAT, obj: self.elem, "Got error {err}");
gst::element_error!(
&self.elem,
gst::StreamError::Failed,
("Internal data stream error"),
["streaming stopped, reason {}", err]
);
task::Trigger::Error
}
}
}
.boxed()
}
}
#[derive(Debug)]
pub struct AudioTestSrc {
src_pad: PadSrc,
task: Task,
settings: Mutex<Settings>,
}
impl AudioTestSrc {
fn prepare(&self) -> Result<(), gst::ErrorMessage> {
gst::debug!(CAT, imp: self, "Preparing");
let settings = self.settings.lock().unwrap();
let context =
runtime::Context::acquire(&settings.context, settings.context_wait).map_err(|err| {
gst::error_msg!(
gst::ResourceError::OpenRead,
["Failed to acquire Context: {}", err]
)
})?;
drop(settings);
self.task
.prepare(AudioTestSrcTask::new(self.obj().clone()), context)
.block_on()?;
gst::debug!(CAT, imp: self, "Prepared");
Ok(())
}
fn unprepare(&self) {
gst::debug!(CAT, imp: self, "Unpreparing");
self.task.unprepare().block_on().unwrap();
gst::debug!(CAT, imp: self, "Unprepared");
}
fn stop(&self) -> Result<(), gst::ErrorMessage> {
gst::debug!(CAT, imp: self, "Stopping");
self.task.stop().block_on()?;
gst::debug!(CAT, imp: self, "Stopped");
Ok(())
}
fn start(&self) -> Result<(), gst::ErrorMessage> {
gst::debug!(CAT, imp: self, "Starting");
self.task.start().block_on()?;
gst::debug!(CAT, imp: self, "Started");
Ok(())
}
fn pause(&self) -> Result<(), gst::ErrorMessage> {
gst::debug!(CAT, imp: self, "Pausing");
self.task.pause().block_on()?;
gst::debug!(CAT, imp: self, "Paused");
Ok(())
}
}
#[glib::object_subclass]
impl ObjectSubclass for AudioTestSrc {
const NAME: &'static str = "TsAudioTestSrc";
type Type = super::AudioTestSrc;
type ParentType = gst::Element;
fn with_class(klass: &Self::Class) -> Self {
Self {
src_pad: PadSrc::new(
gst::Pad::from_template(&klass.pad_template("src").unwrap(), Some("src")),
AudioTestSrcPadHandler,
),
task: Task::default(),
settings: Default::default(),
}
}
}
impl ObjectImpl for AudioTestSrc {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecString::builder("context")
.nick("Context")
.blurb("Context name to share threads with")
.default_value(Some(DEFAULT_CONTEXT))
.build(),
glib::ParamSpecUInt::builder("context-wait")
.nick("Context Wait")
.blurb("Throttle poll loop to run at most once every this many ms")
.maximum(1000)
.default_value(DEFAULT_CONTEXT_WAIT.as_millis() as u32)
.build(),
glib::ParamSpecBoolean::builder("do-timestamp")
.nick("Do timestamp")
.blurb("Apply current stream time to buffers")
.build(),
glib::ParamSpecBoolean::builder("is-live")
.nick("Is live")
.blurb("Whether to act as a live source")
.build(),
glib::ParamSpecUInt::builder("buffer-duration")
.nick("Buffer duration")
.blurb("Buffer duration in ms")
.default_value(DEFAULT_BUFFER_DURATION.mseconds() as u32)
.build(),
glib::ParamSpecInt::builder("num-buffers")
.nick("Num Buffers")
.blurb("Number of buffers to output before sending EOS (-1 = unlimited)")
.minimum(-1i32)
.default_value(DEFAULT_NUM_BUFFERS)
.build(),
#[cfg(feature = "tuning")]
glib::ParamSpecBoolean::builder("main-elem")
.nick("Main Element")
.blurb("Declare this element as the main one")
.write_only()
.build(),
]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
let mut settings = self.settings.lock().unwrap();
match pspec.name() {
"context" => {
settings.context = value
.get::<Option<String>>()
.unwrap()
.unwrap_or_else(|| DEFAULT_CONTEXT.into());
}
"context-wait" => {
settings.context_wait = Duration::from_millis(value.get::<u32>().unwrap().into());
}
"do-timestamp" => {
settings.do_timestamp = value.get::<bool>().unwrap();
}
"is-live" => {
settings.is_live = value.get::<bool>().unwrap();
}
"buffer-duration" => {
settings.buffer_duration = (value.get::<u32>().unwrap() as u64).mseconds();
}
"num-buffers" => {
let value = value.get::<i32>().unwrap();
settings.num_buffers = if value > 0 { Some(value as u32) } else { None };
}
#[cfg(feature = "tuning")]
"main-elem" => {
settings.is_main_elem = value.get::<bool>().unwrap();
}
_ => unimplemented!(),
}
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
let settings = self.settings.lock().unwrap();
match pspec.name() {
"context" => settings.context.to_value(),
"context-wait" => (settings.context_wait.as_millis() as u32).to_value(),
"do-timestamp" => settings.do_timestamp.to_value(),
"is-live" => settings.is_live.to_value(),
"buffer-duration" => (settings.buffer_duration.mseconds() as u32).to_value(),
"num-buffers" => settings
.num_buffers
.and_then(|val| val.try_into().ok())
.unwrap_or(-1i32)
.to_value(),
#[cfg(feature = "tuning")]
"main-elem" => settings.is_main_elem.to_value(),
_ => unimplemented!(),
}
}
fn constructed(&self) {
self.parent_constructed();
let obj = self.obj();
obj.add_pad(self.src_pad.gst_pad()).unwrap();
obj.set_element_flags(gst::ElementFlags::SOURCE);
}
}
impl GstObjectImpl for AudioTestSrc {}
impl ElementImpl for AudioTestSrc {
fn metadata() -> Option<&'static gst::subclass::ElementMetadata> {
static ELEMENT_METADATA: Lazy<gst::subclass::ElementMetadata> = Lazy::new(|| {
gst::subclass::ElementMetadata::new(
"Thread-sharing audio test source",
"Source/Test",
"Thread-sharing audio test source",
"François Laignel <fengalin@free.fr>",
)
});
Some(&*ELEMENT_METADATA)
}
fn pad_templates() -> &'static [gst::PadTemplate] {
static PAD_TEMPLATES: Lazy<Vec<gst::PadTemplate>> = Lazy::new(|| {
let src_pad_template = gst::PadTemplate::new(
"src",
gst::PadDirection::Src,
gst::PadPresence::Always,
&DEFAULT_CAPS,
)
.unwrap();
vec![src_pad_template]
});
PAD_TEMPLATES.as_ref()
}
fn change_state(
&self,
transition: gst::StateChange,
) -> Result<gst::StateChangeSuccess, gst::StateChangeError> {
gst::trace!(CAT, imp: self, "Changing state {transition:?}");
match transition {
gst::StateChange::NullToReady => {
self.prepare().map_err(|err| {
self.post_error_message(err);
gst::StateChangeError
})?;
}
gst::StateChange::PlayingToPaused => {
self.pause().map_err(|_| gst::StateChangeError)?;
}
gst::StateChange::ReadyToNull => {
self.unprepare();
}
_ => (),
}
let mut success = self.parent_change_state(transition)?;
match transition {
gst::StateChange::ReadyToPaused => {
self.pause().map_err(|_| gst::StateChangeError)?;
success = gst::StateChangeSuccess::NoPreroll;
}
gst::StateChange::PausedToPlaying => {
self.start().map_err(|_| gst::StateChangeError)?;
}
gst::StateChange::PlayingToPaused => {
success = gst::StateChangeSuccess::NoPreroll;
}
gst::StateChange::PausedToReady => {
self.stop().map_err(|_| gst::StateChangeError)?;
}
_ => (),
}
Ok(success)
}
}

View file

@ -0,0 +1,17 @@
use gst::glib;
use gst::prelude::*;
mod imp;
glib::wrapper! {
pub struct AudioTestSrc(ObjectSubclass<imp::AudioTestSrc>) @extends gst::Element, gst::Object;
}
pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
gst::Element::register(
Some(plugin),
"ts-audiotestsrc",
gst::Rank::None,
AudioTestSrc::static_type(),
)
}

View file

@ -28,7 +28,7 @@ use gst_rtp::RTPBuffer;
use once_cell::sync::Lazy;
use std::cmp::{max, min, Ordering};
use std::cmp::Ordering;
use std::collections::{BTreeSet, VecDeque};
use std::mem;
use std::sync::Arc;
@ -412,7 +412,7 @@ impl SinkHandler {
}
if let Some(last_in_seqnum) = inner.last_in_seqnum {
let gap = gst_rtp::compare_seqnum(last_in_seqnum as u16, seq);
let gap = gst_rtp::compare_seqnum(last_in_seqnum, seq);
if gap == 1 {
self.calculate_packet_spacing(inner, &mut state, rtptime, pts);
} else {
@ -463,7 +463,7 @@ impl SinkHandler {
state.equidistant += 1;
}
state.equidistant = min(max(state.equidistant, -7), 7);
state.equidistant = state.equidistant.clamp(-7, 7);
inner.last_rtptime = Some(rtptime);
@ -679,7 +679,7 @@ impl SrcHandler {
// FIXME reason why we can expect Some for the 2 lines below
let mut last_popped_pts = state.last_popped_pts.unwrap();
let interval = pts.into().unwrap().saturating_sub(last_popped_pts);
let spacing = interval / (gap as u64 + 1);
let spacing = interval / (gap + 1);
*discont = true;
@ -1259,7 +1259,7 @@ impl JitterBuffer {
self.task
.prepare(
JitterBufferTask::new(&*self.obj(), &self.src_pad_handler, &self.sink_pad_handler),
JitterBufferTask::new(&self.obj(), &self.src_pad_handler, &self.sink_pad_handler),
context,
)
.block_on()?;

View file

@ -16,29 +16,30 @@
#[macro_use]
pub mod runtime;
pub mod socket;
mod tcpclientsrc;
mod udpsink;
mod udpsrc;
mod appsrc;
mod audiotestsrc;
pub mod dataqueue;
mod inputselector;
mod jitterbuffer;
mod proxy;
mod queue;
pub mod socket;
mod tcpclientsrc;
mod udpsink;
mod udpsrc;
use gst::glib;
fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
udpsrc::register(plugin)?;
udpsink::register(plugin)?;
tcpclientsrc::register(plugin)?;
queue::register(plugin)?;
proxy::register(plugin)?;
appsrc::register(plugin)?;
jitterbuffer::register(plugin)?;
audiotestsrc::register(plugin)?;
inputselector::register(plugin)?;
jitterbuffer::register(plugin)?;
proxy::register(plugin)?;
queue::register(plugin)?;
tcpclientsrc::register(plugin)?;
udpsink::register(plugin)?;
udpsrc::register(plugin)?;
Ok(())
}

File diff suppressed because it is too large Load diff

View file

@ -336,13 +336,13 @@ fn eos() {
.name("src-eos")
.property("caps", &caps)
.property("do-timestamp", true)
.property("context", &CONTEXT)
.property("context", CONTEXT)
.build()
.unwrap();
let queue = gst::ElementFactory::make("ts-queue")
.name("queue-eos")
.property("context", &CONTEXT)
.property("context", CONTEXT)
.build()
.unwrap();
@ -636,7 +636,7 @@ fn socket_play_null_play() {
let sink = gst::ElementFactory::make("ts-udpsink")
.name(format!("sink-{}", TEST).as_str())
.property("socket", &socket)
.property("context", &TEST)
.property("context", TEST)
.property("context-wait", 20u32)
.build()
.unwrap();

View file

@ -100,6 +100,6 @@ fn test_chain() {
assert!(buf == [42, 43, 44, 45, 0]);
});
let buf = gst::Buffer::from_slice(&[42, 43, 44, 45]);
let buf = gst::Buffer::from_slice([42, 43, 44, 45]);
assert!(h.push(buf) == Ok(gst::FlowSuccess::Ok));
}

View file

@ -1,11 +1,16 @@
project('gst-plugins-rs',
'rust',
'c',
version: '0.9.0-alpha.1',
version: '0.9.8',
meson_version : '>= 0.60')
python = import('python').find_installation()
# dependencies.py needs a toml parsing module
python = import('python').find_installation(modules: ['tomllib'], required: false)
if not python.found()
python = import('python').find_installation(modules: ['tomli'])
endif
fs = import('fs')
host_system = host_machine.system()
if get_option('debug')
target = 'debug'
@ -23,9 +28,9 @@ if not cargo_c.found()
endif
system = build_machine.system()
ext_exe = ''
exe_suffix = ''
if system == 'windows'
ext_exe = 'exe'
exe_suffix = '.exe'
ext_dynamic = 'dll'
ext_static = 'lib'
elif system == 'darwin'
@ -36,135 +41,18 @@ else
ext_static = 'a'
endif
# workspace name -> lib name
# kept in the same order as the `members` list in Cargo.toml
plugins = {
'gst-plugin-audiofx': 'libgstrsaudiofx',
'gst-plugin-claxon': 'libgstclaxon',
# csound has an external dependency, see below
'gst-plugin-lewton': 'libgstlewton',
'gst-plugin-spotify': 'libgstspotify',
'gst-plugin-file': 'libgstrsfile',
# sodium has an external dependency, see below
'gst-plugin-threadshare': 'libgstthreadshare',
'gst-plugin-fmp4': 'libgstfmp4',
'gst-plugin-aws': 'libgstaws',
'gst-plugin-hlssink3': 'libgsthlssink3',
'gst-plugin-ndi': 'libgstndi',
'gst-plugin-onvif': 'libgstrsonvif',
'gst-plugin-raptorq': 'libgstraptorq',
'gst-plugin-reqwest': 'libgstreqwest',
'gst-plugin-rtp': 'libgstrsrtp',
'gst-plugin-webrtchttp': 'libgstwebrtchttp',
'gst-plugin-webrtc': 'libgstrswebrtc',
'gst-plugin-textahead': 'libgsttextahead',
'gst-plugin-json': 'libgstjson',
'gst-plugin-regex': 'libgstregex',
'gst-plugin-textwrap': 'libgsttextwrap',
'gst-plugin-fallbackswitch': 'libgstfallbackswitch',
'gst-plugin-togglerecord': 'libgsttogglerecord',
'gst-plugin-tracers': 'libgstrstracers',
'gst-plugin-uriplaylistbin': 'libgsturiplaylistbin',
'gst-plugin-cdg': 'libgstcdg',
# closedcaption has an external dependency, see below
# dav1d has an external dependency, see below
'gst-plugin-ffv1': 'libgstffv1',
'gst-plugin-flavors': 'libgstrsflv',
'gst-plugin-gif': 'libgstgif',
# gtk4 has an external dependency, see below
'gst-plugin-hsv': 'libgsthsv',
'gst-plugin-png': 'libgstrspng',
'gst-plugin-rav1e': 'libgstrav1e',
# videofx has an external dependency, see below
# FIXME: libwebp-sys2 will build its bundled version on msvc and apple platforms
# https://github.com/qnighy/libwebp-sys2-rs/issues/4
'gst-plugin-webp': 'libgstrswebp',
}
# Extra env to pass to cargo
extra_env = {}
if dependency('cairo-gobject', required : get_option('videofx')).found()
plugins += {'gst-plugin-videofx': 'libgstrsvideofx',}
endif
if dependency('pangocairo', required : get_option('closedcaption')).found()
plugins += {'gst-plugin-closedcaption' : 'libgstrsclosedcaption',}
endif
if dependency('dav1d', version : '>= 1.0.0', required : get_option('dav1d')).found()
plugins += {'gst-plugin-dav1d' : 'libgstdav1d'}
endif
sodium = get_option ('sodium')
if sodium == 'system'
dependency('libsodium')
plugins += {'gst-plugin-sodium': 'libgstsodium'}
extra_env += {'SODIUM_USE_PKG_CONFIG': '1'}
elif sodium == 'built-in'
plugins += {'gst-plugin-sodium': 'libgstsodium'}
endif
cc = meson.get_compiler('c')
csound_option = get_option('csound')
# try first to find csound using pkg-config
csound_dep = dependency('', required: false)
if not csound_dep.found() and not csound_option.disabled()
# if csound isn't distributed with pkg-config then user needs to define CSOUND_LIB_DIR with its location
res = run_command(python, '-c', 'import os; print(os.environ["CSOUND_LIB_DIR"])', check: false)
if res.returncode() == 0
csound_libdir = res.stdout().strip()
csound_dep = cc.find_library('csound64', dirs: csound_libdir, required: false)
if csound_dep.found()
extra_env += {'CSOUND_LIB_DIR': csound_libdir}
endif
endif
endif
if csound_dep.found()
plugins += {'gst-plugin-csound' : 'libgstcsound'}
elif csound_option.enabled()
error('csound option is enabled, but csound64 library could not be found and CSOUND_LIB_DIR was not set')
else
message('csound not found, disabling its plugin')
endif
if dependency('gtk4', required : get_option('gtk4')).found()
plugins += {'gst-plugin-gtk4' : 'libgstgtk4',}
endif
output = []
extensions = []
# Add the plugin file as output
if get_option('default_library') == 'shared' or get_option('default_library') == 'both'
extensions += [ext_dynamic]
foreach p, lib : plugins
output += [lib + '.' + ext_dynamic]
endforeach
endif
static_build = get_option('default_library') == 'static'
if static_build or get_option('default_library') == 'both'
extensions += [ext_static]
foreach p, lib : plugins
output += [lib + '.' + ext_static]
endforeach
endif
# Used to not lookup the same dependency multiple times which clutters logs
deps_cache = {}
# Need to depends on all gstreamer-rs deps to ensure they are built
# before gstreamer-rs when building with gst-build.
# Custom targets can't depend on dependency() objects so we have to depend
# on the library variable from the subproject instead.
gst_req = '>= 1.18.0'
glib_req = '>=2.62'
gst_req = '>=1.20.0'
depends = []
deps = [
@ -174,6 +62,7 @@ deps = [
['gstreamer-audio-1.0', 'gst-plugins-base', 'audio_dep', 'gstaudio'],
['gstreamer-base-1.0', 'gstreamer', 'gst_base_dep', 'gst_base'],
['gstreamer-check-1.0', 'gstreamer', 'gst_check_dep', 'gst_check'],
['gstreamer-gl-1.0', 'gst-plugins-base', 'gst_gl_dep', 'gstgl'],
['gstreamer-net-1.0', 'gstreamer', 'gst_net_dep', 'gst_net'],
['gstreamer-rtp-1.0', 'gst-plugins-base', 'rtp_dep', 'gst_rtp'],
['gstreamer-video-1.0', 'gst-plugins-base', 'video_dep', 'gstvideo'],
@ -181,11 +70,11 @@ deps = [
['gstreamer-webrtc-1.0', 'gst-plugins-bad', 'gstwebrtc_dep', 'gstwebrtc'],
]
# Used to not lookup the same dependency multiple times which clutters logs
deps_cache = {}
glib_dep = dependency('glib-2.0', version: glib_req)
deps_cache += {'glib-2.0': glib_dep}
foreach d: deps
dep = dependency(d[0], version : gst_req,
dep = dependency(d[0], version: gst_req,
fallback : [d[1], d[2]])
set_variable(d[2], dep)
deps_cache += {d[0]: dep}
@ -195,22 +84,315 @@ foreach d: deps
endif
endforeach
include = ','.join(plugins.keys())
# kept in the same order as the `members` list in Cargo.toml
plugins = {
'audiofx': {
'library': 'libgstrsaudiofx',
'examples': ['hrtfrender'],
},
'claxon': {'library': 'libgstclaxon'},
# csound has a non-trivial external dependency, see below
'lewton': {'library': 'libgstlewton'},
'spotify': {'library': 'libgstspotify'},
# serialize extra_env
extra_env_list = []
foreach key, value : extra_env
extra_env_list += key + ':' + value
'file': {'library': 'libgstrsfile'},
# sodium can have an external dependency, see below
'threadshare': {
'library': 'libgstthreadshare',
'examples': [
'ts-benchmark',
'udpsrc-benchmark-sender',
'tcpclientsrc-benchmark-sender',
'ts-standalone',
],
},
'mp4': {'library': 'libgstmp4'},
'fmp4': {
'library': 'libgstfmp4',
'examples': [
'dash_vod',
'hls_live',
'hls_vod',
],
},
'aws': {
'library': 'libgstaws',
'extra-deps': {'openssl': '>=1.1'},
},
'hlssink3': {'library': 'libgsthlssink3'},
'ndi': {'library': 'libgstndi'},
'onvif': {
'library': 'libgstrsonvif',
'extra-deps': {'pangocairo': ''},
},
'raptorq': {'library': 'libgstraptorq'},
'reqwest': {'library': 'libgstreqwest'},
'rtp': {'library': 'libgstrsrtp'},
'webrtchttp': {'library': 'libgstwebrtchttp'},
'webrtc': {
'library': 'libgstrswebrtc',
'examples': ['webrtcsink-stats-server'],
},
'textahead': {'library': 'libgsttextahead'},
'json': {'library': 'libgstjson'},
'regex': {'library': 'libgstregex'},
'textwrap': {'library': 'libgsttextwrap'},
'fallbackswitch': {
'library': 'libgstfallbackswitch',
'examples': ['gtk-fallbackswitch'],
'features': ['gtk', 'gio', 'gst-plugin-gtk4'],
},
'livesync': {
'library': 'libgstlivesync',
'examples': ['gtk-livesync'],
'features': ['gtk', 'gio', 'gst-plugin-gtk4'],
},
'togglerecord': {
'library': 'libgsttogglerecord',
'examples': ['gtk-recording'],
'features': ['gtk', 'gio', 'gst-plugin-gtk4'],
},
'tracers': {'library': 'libgstrstracers'},
'uriplaylistbin': {
'library': 'libgsturiplaylistbin',
'examples': ['playlist'],
'features': ['clap'],
},
'cdg': {'library': 'libgstcdg'},
'closedcaption': {
'library': 'libgstrsclosedcaption',
'extra-deps': {
'pango': '',
'pangocairo': '',
'cairo-gobject': '',
}
},
'dav1d': {
'library': 'libgstdav1d',
'extra-deps': {'dav1d': '>=1.0'},
},
'ffv1': {'library': 'libgstffv1'},
'flavors': {'library': 'libgstrsflv'},
'gif': {
'library': 'libgstgif',
'examples': ['testvideosrc2gif'],
},
# gtk4 is added below
'hsv': {'library': 'libgsthsv'},
'png': {
'library': 'libgstrspng',
'examples': ['pngenc'],
},
'rav1e': {'library': 'libgstrav1e'},
'videofx': {
'library': 'libgstrsvideofx',
'extra-deps': {'cairo-gobject': ''},
},
}
# Won't build on platforms where it bundles the sources because of:
# https://github.com/qnighy/libwebp-sys2-rs/issues/12
# the fix is:
# https://github.com/qnighy/libwebp-sys2-rs/pull/13
if host_system not in ['windows', 'darwin']
# FIXME: libwebp-sys2 will build its bundled version on msvc and apple platforms
# https://github.com/qnighy/libwebp-sys2-rs/issues/4
plugins += {'webp': {
'library': 'libgstrswebp',
'extra-deps': {'libwebpdemux': ''},
}}
endif
sodium_opt = get_option('sodium')
if sodium_opt.allowed()
sodium_plugin = {'sodium': {
'library': 'libgstsodium',
'examples': ['generate-keys', 'encrypt-example', 'decrypt-example'],
'features': ['serde', 'serde_json', 'clap'],
}}
if get_option('sodium-source') == 'system'
sodium_dep = dependency('libsodium', required: sodium_opt.enabled())
extra_env += {'SODIUM_USE_PKG_CONFIG': '1'}
if sodium_dep.found()
plugins += sodium_plugin
endif
else
plugins += sodium_plugin
endif
endif
cc = meson.get_compiler('c')
csound_option = get_option('csound')
if csound_option.allowed()
# if csound isn't distributed with pkg-config then user needs to define CSOUND_LIB_DIR with its location
res = run_command(python, '-c', 'import os; print(os.environ["CSOUND_LIB_DIR"])', check: false)
if res.returncode() == 0
csound_libdir = res.stdout().strip()
csound_dep = cc.find_library('csound64', dirs: csound_libdir, required: false)
if csound_dep.found()
plugins += {'csound': {
'library': 'libgstcsound',
'examples': ['csound-effect'],
}}
extra_env += {'CSOUND_LIB_DIR': csound_libdir}
elif csound_option.enabled()
error('csound option is enabled, but csound64 library could not be found and CSOUND_LIB_DIR was not set')
endif
endif
endif
if get_option('gtk4').allowed()
gtk4_features = []
gl_winsys = gst_gl_dep.get_variable('gl_winsys').split()
gl_platforms = gst_gl_dep.get_variable('gl_platforms').split()
if host_system == 'linux'
if 'wayland' in gl_winsys
gtk4_features += 'wayland'
endif
if 'x11' in gl_winsys
if 'egl' in gl_platforms
gtk4_features += 'x11egl'
endif
if 'glx' in gl_platforms
gtk4_features += 'x11glx'
endif
endif
endif
plugins += {'gtk4': {
'library': 'libgstgtk4',
'examples': ['gtksink'],
'extra-deps': {'gtk4': '>=4.6'},
'features': gtk4_features,
}}
endif
# Process plugins list
default_library = get_option('default_library')
library_suffixes = []
if default_library in ['shared', 'both']
library_suffixes += [ext_dynamic]
endif
if default_library in ['static', 'both']
library_suffixes += [ext_static]
endif
# cargo packages (plugins) to build
packages = []
# cargo features
features = []
# examples to build
examples = []
# Add the plugin library files as output
output = []
if get_option('gtk4').allowed()
if glib_dep.version().version_compare('>=2.74')
features += ['glib/v2_74', 'gio/v2_74']
elif glib_dep.version().version_compare('>=2.72')
features += ['glib/v2_72', 'gio/v2_72']
elif glib_dep.version().version_compare('>=2.70')
features += ['glib/v2_70', 'gio/v2_70']
elif glib_dep.version().version_compare('>=2.68')
features += ['glib/v2_68', 'gio/v2_68']
elif glib_dep.version().version_compare('>=2.66')
features += ['glib/v2_66', 'gio/v2_66']
elif glib_dep.version().version_compare('>=2.64')
features += ['glib/v2_64', 'gio/v2_64']
elif glib_dep.version().version_compare('>=2.62')
features += ['glib/v2_62', 'gio/v2_62']
elif glib_dep.version().version_compare('>=2.60')
features += ['glib/v2_60', 'gio/v2_60']
elif glib_dep.version().version_compare('>=2.58')
features += ['glib/v2_58', 'gio/v2_58']
endif
endif
if gst_dep.version().version_compare('>=1.21')
components = [
'', '-app', '-audio', '-base', '-check',
'-rtp', '-sdp', '-utils', '-video', '-webrtc',
]
if get_option('tracers').allowed()
components += '-plugin-tracers'
endif
if get_option('threadshare').allowed()
components += '-net'
endif
if get_option('mp4').allowed() or get_option('fmp4').allowed()
components += '-pbutils'
endif
foreach c: components
features += f'gst@c@/v1_22'
endforeach
if get_option('webrtc').allowed()
features += 'gst-plugin-webrtc/gst1_22'
endif
endif
if get_option('rav1e').allowed() and find_program('nasm', required: false).found()
features += 'gst-plugin-rav1e/asm'
endif
foreach plugin_name, details: plugins
plugin_opt = get_option(plugin_name)
if plugin_opt.allowed()
plugin_deps_found = true
foreach dep_name, dep_ver: details.get('extra-deps', {})
if dep_ver != ''
dep = dependency(dep_name, version: dep_ver, required: plugin_opt)
else
dep = dependency(dep_name, required: plugin_opt)
endif
deps_cache += {dep_name: dep}
if not dep.found()
plugin_deps_found = false
endif
endforeach
if plugin_deps_found
packages += f'gst-plugin-@plugin_name@'
features += details.get('features', [])
examples += details.get('examples', [])
lib = details.get('library')
if default_library in ['shared', 'both']
output += [lib + '.' + ext_dynamic]
endif
if default_library in ['static', 'both']
output += [lib + '.' + ext_static]
endif
endif
endif
endforeach
extra_env_str = ','.join(extra_env_list)
plugins_install_dir = get_option('libdir') / 'gstreamer-1.0'
pkgconfig_install_dir = get_option('libdir') / 'pkgconfig'
extra_args = []
if get_option('doc').disabled()
disable_doc = ['--disable-doc']
else
disable_doc = []
extra_args = ['--disable-doc']
endif
# 'pkgconfig' is the entry in the machine file, if specified
pkg_config = find_program('pkgconfig', required: false)
if pkg_config.found()
extra_env += {'PKG_CONFIG': pkg_config.full_path()}
endif
pkg_config_path = get_option('pkg_config_path')
if pkg_config_path.length() > 0
pathsep = ':'
if host_system == 'windows'
pathsep = ';'
endif
extra_env += {'PKG_CONFIG_PATH': pathsep.join(pkg_config_path)}
endif
rs_plugins = custom_target('gst-plugins-rs',
@ -221,19 +403,20 @@ rs_plugins = custom_target('gst-plugins-rs',
install_dir: plugins_install_dir,
depends: depends,
depfile: 'gst-plugins-rs.dep',
env: extra_env,
command: [cargo_wrapper,
'build',
meson.current_build_dir(),
meson.current_source_dir(),
meson.global_build_root(),
target,
include,
extra_env_str,
get_option('prefix'),
get_option('libdir'),
'--packages', packages,
'--features', features,
'--depfile', '@DEPFILE@',
'--exts', extensions,
] + disable_doc)
'--lib-suffixes', library_suffixes,
] + extra_args)
plugins = rs_plugins.to_list()
@ -245,6 +428,17 @@ foreach plugin : plugins
# skip the 'lib' prefix and extension from plugin path
plugin_name = fs.stem(plugin.full_path()).substring(3)
option_name = plugin_name.substring(3)
if option_name.startswith('rs')
option_name = option_name.substring(2)
endif
if option_name == 'flv'
option_name = 'flavors'
endif
if not get_option(option_name).allowed()
continue
endif
# Extract plugin dependencies from their Cargo.toml file
plugin_deps = []
p = run_command('dependencies.py', meson.current_source_dir(), plugin_name,
@ -252,9 +446,9 @@ foreach plugin : plugins
check: true)
foreach dep_name : p.stdout().strip().split(',')
dep_name_version = dep_name.split('|')
dep_name = dep_name_version.get(0)
dep_name = dep_name_version.get(0).strip()
if dep_name_version.length() > 1
extras = {'version': dep_name_version.get(1)}
extras = {'version': dep_name_version.get(1).strip()}
else
extras = {}
endif
@ -274,17 +468,17 @@ foreach plugin : plugins
)
meson.override_dependency(plugin_name, dep)
if static_build and plugin_name in ['gstcsound', 'gstthreadshare']
if default_library == 'static' and plugin_name in ['gstcsound', 'gstthreadshare', 'gstgtk4']
warning('Static plugin @0@ is known to fail. It will not be included in libgstreamer-full.'.format(plugin_name))
else
gst_plugins += dep
endif
pc_files += [plugin_name + '.pc']
if plugin_name.startswith('gst')
plugin_names += [plugin_name.substring(3)]
else
plugin_names += [plugin_name]
pc_files += [plugin_name + '.pc']
if plugin_name.startswith('gst')
plugin_names += [plugin_name.substring(3)]
else
plugin_names += [plugin_name]
endif
endif
endforeach
@ -302,40 +496,64 @@ custom_target('gst-plugins-rs-pc-files',
depends: rs_plugins,
command: [python, '-c', '""'])
if get_option('webrtc').allowed()
custom_target('gst-webrtc-signalling-server',
build_by_default: true,
output: 'gst-webrtc-signalling-server',
console: true,
install: true,
install_dir: get_option('bindir'),
depfile: 'gst-webrtc-signalling-server.dep',
env: extra_env,
command: [cargo_wrapper,
'build',
meson.current_build_dir(),
meson.current_source_dir(),
meson.global_build_root(),
target,
get_option('prefix'),
get_option('libdir'),
'--depfile', '@DEPFILE@',
'--bin', 'gst-webrtc-signalling-server',
'--exe-suffix', exe_suffix,
])
endif
custom_target('gst-webrtc-signalling-server',
build_by_default: true,
output: 'gst-webrtc-signalling-server',
console: true,
install: true,
install_dir: get_option('bindir'),
depfile: 'gst-webrtc-signalling-server.dep',
command: [cargo_wrapper,
'build',
meson.current_build_dir(),
meson.current_source_dir(),
meson.global_build_root(),
target,
'',
'',
get_option('prefix'),
get_option('libdir'),
'--depfile', '@DEPFILE@',
'--exts', ext_exe,
'--bin', 'gst-webrtc-signalling-server'
])
if get_option('examples').allowed() and examples.length() > 0
custom_target('gst-plugins-rs-examples',
build_by_default: true,
output: examples,
console: true,
install: false,
depfile: 'gst-plugins-rs-examples.dep',
env: extra_env,
command: [cargo_wrapper,
'build',
meson.current_build_dir(),
meson.current_source_dir(),
meson.global_build_root(),
target,
get_option('prefix'),
get_option('libdir'),
'--depfile', '@DEPFILE@',
'--packages', packages,
'--features', features,
'--examples', examples,
'--exe-suffix', exe_suffix,
])
endif
test('tests',
cargo_wrapper,
env: extra_env,
args: ['test',
meson.current_build_dir(),
meson.current_source_dir(),
meson.global_build_root(),
target,
include,
extra_env_str,
get_option('prefix'),
get_option('libdir')],
get_option('libdir'),
'--packages', packages],
timeout: 600)
summary({

View file

@ -1,12 +1,64 @@
option('videofx', type : 'feature', value : 'auto', description : 'Build videofx plugin')
option('closedcaption', type : 'feature', value : 'auto', description : 'Build closedcaption plugin')
option('dav1d', type : 'feature', value : 'auto', description : 'Build dav1d plugin')
option('sodium', type : 'combo',
choices : ['system', 'built-in', 'disabled'], value : 'built-in',
description : 'Weither to use libsodium from the system or the built-in version from the sodiumoxide crate')
option('csound', type : 'feature', value : 'auto', description : 'Build csound plugin')
option('gtk4', type : 'feature', value : 'auto', description : 'Build GTK4 plugin')
# Same order as members in Cargo.toml
# audio
option('audiofx', type: 'feature', value: 'auto', description: 'Build audiofx plugin')
option('claxon', type: 'feature', value: 'auto', description: 'Build claxon plugin')
option('csound', type: 'feature', value: 'auto', description: 'Build csound plugin')
option('lewton', type: 'feature', value: 'auto', description: 'Build lewton plugin')
option('spotify', type: 'feature', value: 'auto', description: 'Build spotify plugin')
# generic
option('file', type: 'feature', value: 'auto', description: 'Build file plugin')
option('sodium', type: 'feature', value: 'auto', description: 'Build sodium plugin')
option('sodium-source', type: 'combo',
choices: ['system', 'built-in'], value: 'built-in',
description: 'Whether to use libsodium from the system or the built-in version from the sodiumoxide crate')
option('threadshare', type: 'feature', value: 'auto', description: 'Build threadshare plugin')
# mux
option('flavors', type: 'feature', value: 'auto', description: 'Build flavors plugin')
option('fmp4', type: 'feature', value: 'auto', description: 'Build fmp4 plugin')
option('mp4', type: 'feature', value: 'auto', description: 'Build mp4 plugin')
# net
option('aws', type: 'feature', value: 'auto', description: 'Build aws plugin')
option('hlssink3', type: 'feature', value: 'auto', description: 'Build hlssink3 plugin')
option('ndi', type: 'feature', value: 'auto', description: 'Build ndi plugin')
option('onvif', type: 'feature', value: 'auto', description: 'Build onvif plugin')
option('raptorq', type: 'feature', value: 'auto', description: 'Build raptorq plugin')
option('reqwest', type: 'feature', value: 'auto', description: 'Build reqwest plugin')
option('rtp', type: 'feature', value: 'auto', description: 'Build rtp plugin')
option('webrtc', type: 'feature', value: 'auto', description: 'Build webrtc plugin')
option('webrtchttp', type: 'feature', value: 'auto', description: 'Build webrtchttp plugin')
# text
option('textahead', type: 'feature', value: 'auto', description: 'Build textahead plugin')
option('json', type: 'feature', value: 'auto', description: 'Build json plugin')
option('regex', type: 'feature', value: 'auto', description: 'Build regex plugin')
option('textwrap', type: 'feature', value: 'auto', description: 'Build textwrap plugin')
# utils
option('fallbackswitch', type: 'feature', value: 'auto', description: 'Build fallbackswitch plugin')
option('livesync', type: 'feature', value: 'auto', description: 'Build livesync plugin')
option('togglerecord', type: 'feature', value: 'auto', description: 'Build togglerecord plugin')
option('tracers', type: 'feature', value: 'auto', description: 'Build tracers plugin')
option('uriplaylistbin', type: 'feature', value: 'auto', description: 'Build uriplaylistbin plugin')
# video
option('cdg', type: 'feature', value: 'auto', description: 'Build cdg plugin')
option('closedcaption', type: 'feature', value: 'auto', description: 'Build closedcaption plugin')
option('dav1d', type: 'feature', value: 'auto', description: 'Build dav1d plugin')
option('ffv1', type: 'feature', value: 'auto', description: 'Build ffv1 plugin')
option('gif', type: 'feature', value: 'auto', description: 'Build gif plugin')
option('gtk4', type: 'feature', value: 'auto', description: 'Build GTK4 plugin')
option('hsv', type: 'feature', value: 'auto', description: 'Build hsv plugin')
option('png', type: 'feature', value: 'auto', description: 'Build png plugin')
option('rav1e', type: 'feature', value: 'auto', description: 'Build rav1e plugin')
option('videofx', type: 'feature', value: 'auto', description: 'Build videofx plugin')
option('webp', type: 'feature', value: 'auto', description: 'Build webp plugin')
# Common options
option('doc', type : 'feature', value : 'auto', yield: true,
description: 'Enable documentation.')
option('doc', type: 'feature', value: 'auto', yield: true,
description: 'Enable documentation')
option('examples', type: 'feature', value: 'disabled', yield: true,
description: 'Build examples')

View file

@ -1,6 +1,6 @@
[package]
name = "gst-plugin-flavors"
version = "0.9.0-alpha.1"
version = "0.9.10"
authors = ["Sebastian Dröge <sebastian@centricular.com>"]
repository = "https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs"
license = "MIT OR Apache-2.0"
@ -9,9 +9,9 @@ rust-version = "1.63"
description = "GStreamer Rust FLV Plugin"
[dependencies]
gst = { package = "gstreamer", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
gst-base = { package = "gstreamer-base", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
gst-audio = { package = "gstreamer-audio", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
gst = { package = "gstreamer", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19.1" }
gst-base = { package = "gstreamer-base", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19" }
gst-audio = { package = "gstreamer-audio", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19" }
num-rational = { version = "0.4", default-features = false, features = [] }
nom = "7"
flavors = { git = "https://github.com/rust-av/flavors" }
@ -26,7 +26,7 @@ crate-type = ["cdylib", "rlib"]
path = "src/lib.rs"
[build-dependencies]
gst-plugin-version-helper = { path="../../version-helper" }
gst-plugin-version-helper = { version = "0.7", path="../../version-helper" }
[features]
static = []

View file

@ -463,7 +463,7 @@ impl FlvDemux {
match *state {
State::Stopped => unreachable!(),
State::NeedHeader => {
let header = match self.find_header(&mut *adapter) {
let header = match self.find_header(&mut adapter) {
Ok(header) => header,
Err(_) => {
gst::trace!(CAT, imp: self, "Need more data");
@ -503,7 +503,7 @@ impl FlvDemux {
*skip_left -= skip as u32;
}
State::Streaming(ref mut sstate) => {
let res = sstate.handle_tag(self, &mut *adapter);
let res = sstate.handle_tag(self, &mut adapter);
match res {
Ok(None) => {
@ -533,7 +533,7 @@ impl FlvDemux {
while adapter.available() >= 9 {
let data = adapter.map(9).unwrap();
if let Ok((_, header)) = flavors::header(&*data) {
if let Ok((_, header)) = flavors::header(&data) {
gst::debug!(CAT, imp: self, "Found FLV header: {:?}", header);
drop(data);
adapter.flush(9);
@ -745,7 +745,7 @@ impl StreamingState {
let data = adapter.map(tag_header.data_size as usize).unwrap();
match flavors::script_data(&*data) {
match flavors::script_data(&data) {
Ok((_, ref script_data)) if script_data.name == "onMetaData" => {
gst::trace!(CAT, imp: imp, "Got script tag: {:?}", script_data);
@ -823,7 +823,9 @@ impl StreamingState {
}
}
if (!self.expect_video || self.video != None) && self.audio != None && !self.got_all_streams
if (!self.expect_video || self.video.is_some())
&& self.audio.is_some()
&& !self.got_all_streams
{
gst::debug!(CAT, imp: imp, "Have all expected streams now");
self.got_all_streams = true;
@ -853,7 +855,7 @@ impl StreamingState {
let data = adapter.map(1).unwrap();
match flavors::aac_audio_packet_header(&*data) {
match flavors::aac_audio_packet_header(&data) {
Err(nom::Err::Error(err)) | Err(nom::Err::Failure(err)) => {
gst::error!(CAT, imp: imp, "Invalid AAC audio packet header: {:?}", err);
drop(data);
@ -894,7 +896,7 @@ impl StreamingState {
assert!(adapter.available() >= tag_header.data_size as usize);
let data = adapter.map(1).unwrap();
let data_header = match flavors::audio_data_header(&*data) {
let data_header = match flavors::audio_data_header(&data) {
Err(nom::Err::Error(err)) | Err(nom::Err::Failure(err)) => {
gst::error!(CAT, imp: imp, "Invalid audio data header: {:?}", err);
drop(data);
@ -925,7 +927,7 @@ impl StreamingState {
return Ok(events);
}
if self.audio == None {
if self.audio.is_none() {
adapter.flush((tag_header.data_size - offset) as usize);
return Ok(events);
}
@ -983,7 +985,9 @@ impl StreamingState {
}
}
if (!self.expect_audio || self.audio != None) && self.video != None && !self.got_all_streams
if (!self.expect_audio || self.audio.is_some())
&& self.video.is_some()
&& !self.got_all_streams
{
gst::debug!(CAT, imp: imp, "Have all expected streams now");
self.got_all_streams = true;
@ -1012,7 +1016,7 @@ impl StreamingState {
}
let data = adapter.map(4).unwrap();
match flavors::avc_video_packet_header(&*data) {
match flavors::avc_video_packet_header(&data) {
Err(nom::Err::Error(err)) | Err(nom::Err::Failure(err)) => {
gst::error!(CAT, imp: imp, "Invalid AVC video packet header: {:?}", err);
drop(data);
@ -1065,7 +1069,7 @@ impl StreamingState {
assert!(adapter.available() >= tag_header.data_size as usize);
let data = adapter.map(1).unwrap();
let data_header = match flavors::video_data_header(&*data) {
let data_header = match flavors::video_data_header(&data) {
Err(nom::Err::Error(err)) | Err(nom::Err::Failure(err)) => {
gst::error!(CAT, imp: imp, "Invalid video data header: {:?}", err);
drop(data);
@ -1101,7 +1105,7 @@ impl StreamingState {
return Ok(events);
}
if self.video == None {
if self.video.is_none() {
adapter.flush((tag_header.data_size - offset) as usize);
return Ok(events);
}
@ -1419,7 +1423,7 @@ impl VideoFormat {
flavors::CodecId::H264 => self.avc_sequence_header.as_ref().map(|header| {
gst::Caps::builder("video/x-h264")
.field("stream-format", "avc")
.field("codec_data", &header)
.field("codec_data", header)
.build()
}),
flavors::CodecId::H263 => Some(gst::Caps::builder("video/x-h263").build()),

View file

@ -1,6 +1,6 @@
[package]
name = "gst-plugin-fmp4"
version = "0.9.0-alpha.1"
version = "0.9.10"
authors = ["Sebastian Dröge <sebastian@centricular.com>"]
license = "MPL-2.0"
description = "GStreamer Fragmented MP4 Plugin"
@ -10,12 +10,12 @@ rust-version = "1.63"
[dependencies]
anyhow = "1"
gst = { package = "gstreamer", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
gst-base = { package = "gstreamer-base", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
gst-audio = { package = "gstreamer-audio", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
gst-video = { package = "gstreamer-video", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
gst = { package = "gstreamer", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19.1", features = ["v1_18"] }
gst-base = { package = "gstreamer-base", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19", features = ["v1_18"] }
gst-audio = { package = "gstreamer-audio", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19", features = ["v1_18"] }
gst-video = { package = "gstreamer-video", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19", features = ["v1_18"] }
gst-pbutils = { package = "gstreamer-pbutils", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19", features = ["v1_18"] }
once_cell = "1.0"
uuid = { version = "1", features = ["v4"] }
[lib]
name = "gstfmp4"
@ -23,20 +23,20 @@ crate-type = ["cdylib", "rlib"]
path = "src/lib.rs"
[dev-dependencies]
gst-app = { package = "gstreamer-app", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", features = ["v1_18"] }
gst-check = { package = "gstreamer-check", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", features = ["v1_18"] }
gst-pbutils = { package = "gstreamer-pbutils", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", features = ["v1_20"] }
gst-app = { package = "gstreamer-app", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19", features = ["v1_18"] }
gst-check = { package = "gstreamer-check", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19", features = ["v1_18"] }
gst-pbutils = { package = "gstreamer-pbutils", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19", features = ["v1_20"] }
m3u8-rs = "5.0"
chrono = "0.4"
[build-dependencies]
gst-plugin-version-helper = { path="../../version-helper" }
gst-plugin-version-helper = { version = "0.7", path="../../version-helper" }
[features]
default = ["v1_18"]
default = []
static = []
capi = []
v1_18 = ["gst-video/v1_18"]
doc = []
[package.metadata.capi]
min_version = "0.8.0"

View file

@ -225,7 +225,7 @@ fn main() -> Result<(), Error> {
"###,
duration = duration, segment_timeline = segment_timeline);
std::fs::write(path, &manifest).expect("failed to write manifest");
std::fs::write(path, manifest).expect("failed to write manifest");
})
.build(),
);

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -12,6 +12,10 @@ use gst::prelude::*;
mod boxes;
mod imp;
glib::wrapper! {
pub(crate) struct FMP4MuxPad(ObjectSubclass<imp::FMP4MuxPad>) @extends gst_base::AggregatorPad, gst::Pad, gst::Object;
}
glib::wrapper! {
pub(crate) struct FMP4Mux(ObjectSubclass<imp::FMP4Mux>) @extends gst_base::Aggregator, gst::Element, gst::Object;
}
@ -33,8 +37,12 @@ glib::wrapper! {
}
pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
FMP4Mux::static_type().mark_as_plugin_api(gst::PluginAPIFlags::empty());
HeaderUpdateMode::static_type().mark_as_plugin_api(gst::PluginAPIFlags::empty());
#[cfg(feature = "doc")]
{
FMP4Mux::static_type().mark_as_plugin_api(gst::PluginAPIFlags::empty());
FMP4MuxPad::static_type().mark_as_plugin_api(gst::PluginAPIFlags::empty());
HeaderUpdateMode::static_type().mark_as_plugin_api(gst::PluginAPIFlags::empty());
}
gst::Element::register(
Some(plugin),
"isofmp4mux",
@ -64,33 +72,87 @@ pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
}
#[derive(Debug)]
pub(crate) struct HeaderConfiguration<'a> {
pub(crate) struct HeaderConfiguration {
variant: Variant,
update: bool,
/// Pre-defined movie timescale if not 0.
movie_timescale: u32,
/// First caps must be the video/reference stream. Must be in the order the tracks are going to
/// be used later for the fragments too.
streams: &'a [gst::Caps],
streams: Vec<HeaderStream>,
write_mehd: bool,
duration: Option<gst::ClockTime>,
/// Start UTC time in ONVIF mode.
/// Since Jan 1 1601 in 100ns units.
start_utc_time: Option<u64>,
}
#[derive(Debug)]
pub(crate) struct HeaderStream {
/// Caps of this stream
caps: gst::Caps,
/// Set if this is an intra-only stream
delta_frames: DeltaFrames,
/// Pre-defined trak timescale if not 0.
trak_timescale: u32,
}
#[derive(Debug)]
pub(crate) struct FragmentHeaderConfiguration<'a> {
variant: Variant,
/// Sequence number for this fragment.
sequence_number: u32,
streams: &'a [(gst::Caps, Option<FragmentTimingInfo>)],
/// If this is a full fragment or only a chunk.
chunk: bool,
streams: &'a [FragmentHeaderStream],
buffers: &'a [Buffer],
}
#[derive(Debug)]
pub(crate) struct FragmentTimingInfo {
/// Start time of this fragment
start_time: gst::ClockTime,
pub(crate) struct FragmentHeaderStream {
/// Caps of this stream
caps: gst::Caps,
/// Set if this is an intra-only stream
intra_only: bool,
delta_frames: DeltaFrames,
/// Pre-defined trak timescale if not 0.
trak_timescale: u32,
/// Start time of this fragment
///
/// `None` if this stream has no buffers in this fragment.
start_time: Option<gst::ClockTime>,
}
#[derive(Debug, Copy, Clone)]
pub(crate) enum DeltaFrames {
/// Only single completely decodable frames
IntraOnly,
/// Frames may depend on past frames
PredictiveOnly,
/// Frames may depend on past or future frames
Bidirectional,
}
impl DeltaFrames {
/// Whether dts is required to order buffers differently from presentation order
pub(crate) fn requires_dts(&self) -> bool {
matches!(self, Self::Bidirectional)
}
/// Whether this coding structure does not allow delta flags on buffers
pub(crate) fn intra_only(&self) -> bool {
matches!(self, Self::IntraOnly)
}
}
#[derive(Debug)]

View file

@ -1,3 +1,4 @@
// Copyright (C) 2021 Sebastian Dröge <sebastian@centricular.com>
//
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at
@ -18,7 +19,7 @@ fn init() {
});
}
fn test_buffer_flags_single_stream(cmaf: bool) {
fn test_buffer_flags_single_stream(cmaf: bool, set_dts: bool, caps: gst::Caps) {
let mut h = if cmaf {
gst_check::Harness::new("cmafmux")
} else {
@ -30,16 +31,7 @@ fn test_buffer_flags_single_stream(cmaf: bool) {
.unwrap()
.set_property("fragment-duration", 5.seconds());
h.set_src_caps(
gst::Caps::builder("video/x-h264")
.field("width", 1920i32)
.field("height", 1080i32)
.field("framerate", gst::Fraction::new(30, 1))
.field("stream-format", "avc")
.field("alignment", "au")
.field("codec_data", gst::Buffer::with_size(1).unwrap())
.build(),
);
h.set_src_caps(caps);
h.play();
let output_offset = if cmaf {
@ -54,7 +46,9 @@ fn test_buffer_flags_single_stream(cmaf: bool) {
{
let buffer = buffer.get_mut().unwrap();
buffer.set_pts(i.seconds());
buffer.set_dts(i.seconds());
if set_dts {
buffer.set_dts(i.seconds());
}
buffer.set_duration(gst::ClockTime::SECOND);
if i != 0 && i != 5 {
buffer.set_flags(gst::BufferFlags::DELTA_UNIT);
@ -84,13 +78,18 @@ fn test_buffer_flags_single_stream(cmaf: bool) {
}
}
// Crank the clock: this should bring us to the end of the first fragment
h.crank_single_clock_wait().unwrap();
let header = h.pull().unwrap();
assert_eq!(
header.flags(),
gst::BufferFlags::HEADER | gst::BufferFlags::DISCONT
);
assert_eq!(header.pts(), Some(gst::ClockTime::ZERO + output_offset));
assert_eq!(header.dts(), Some(gst::ClockTime::ZERO + output_offset));
if set_dts {
assert_eq!(header.dts(), Some(gst::ClockTime::ZERO + output_offset));
}
let fragment_header = h.pull().unwrap();
assert_eq!(fragment_header.flags(), gst::BufferFlags::HEADER);
@ -98,10 +97,12 @@ fn test_buffer_flags_single_stream(cmaf: bool) {
fragment_header.pts(),
Some(gst::ClockTime::ZERO + output_offset)
);
assert_eq!(
fragment_header.dts(),
Some(gst::ClockTime::ZERO + output_offset)
);
if set_dts {
assert_eq!(
fragment_header.dts(),
Some(gst::ClockTime::ZERO + output_offset)
);
}
assert_eq!(fragment_header.duration(), Some(5.seconds()));
for i in 0..5 {
@ -115,7 +116,9 @@ fn test_buffer_flags_single_stream(cmaf: bool) {
assert_eq!(buffer.flags(), gst::BufferFlags::DELTA_UNIT);
}
assert_eq!(buffer.pts(), Some(i.seconds() + output_offset));
assert_eq!(buffer.dts(), Some(i.seconds() + output_offset));
if set_dts {
assert_eq!(buffer.dts(), Some(i.seconds() + output_offset));
}
assert_eq!(buffer.duration(), Some(gst::ClockTime::SECOND));
}
@ -124,7 +127,9 @@ fn test_buffer_flags_single_stream(cmaf: bool) {
let fragment_header = h.pull().unwrap();
assert_eq!(fragment_header.flags(), gst::BufferFlags::HEADER);
assert_eq!(fragment_header.pts(), Some(5.seconds() + output_offset));
assert_eq!(fragment_header.dts(), Some(5.seconds() + output_offset));
if set_dts {
assert_eq!(fragment_header.dts(), Some(5.seconds() + output_offset));
}
assert_eq!(fragment_header.duration(), Some(2.seconds()));
for i in 5..7 {
@ -138,7 +143,9 @@ fn test_buffer_flags_single_stream(cmaf: bool) {
assert_eq!(buffer.flags(), gst::BufferFlags::DELTA_UNIT);
}
assert_eq!(buffer.pts(), Some(i.seconds() + output_offset));
assert_eq!(buffer.dts(), Some(i.seconds() + output_offset));
if set_dts {
assert_eq!(buffer.dts(), Some(i.seconds() + output_offset));
}
assert_eq!(buffer.duration(), Some(gst::ClockTime::SECOND));
}
@ -153,17 +160,53 @@ fn test_buffer_flags_single_stream(cmaf: bool) {
}
#[test]
fn test_buffer_flags_single_stream_cmaf() {
fn test_buffer_flags_single_h264_stream_cmaf() {
init();
test_buffer_flags_single_stream(true);
let caps = gst::Caps::builder("video/x-h264")
.field("width", 1920i32)
.field("height", 1080i32)
.field("framerate", gst::Fraction::new(30, 1))
.field("stream-format", "avc")
.field("alignment", "au")
.field("codec_data", gst::Buffer::with_size(1).unwrap())
.build();
test_buffer_flags_single_stream(true, true, caps);
}
#[test]
fn test_buffer_flags_single_stream_iso() {
fn test_buffer_flags_single_h264_stream_iso() {
init();
test_buffer_flags_single_stream(false);
let caps = gst::Caps::builder("video/x-h264")
.field("width", 1920i32)
.field("height", 1080i32)
.field("framerate", gst::Fraction::new(30, 1))
.field("stream-format", "avc")
.field("alignment", "au")
.field("codec_data", gst::Buffer::with_size(1).unwrap())
.build();
test_buffer_flags_single_stream(false, true, caps);
}
#[test]
fn test_buffer_flags_single_vp9_stream_iso() {
init();
let caps = gst::Caps::builder("video/x-vp9")
.field("width", 1920i32)
.field("height", 1080i32)
.field("framerate", gst::Fraction::new(30, 1))
.field("profile", "0")
.field("chroma-format", "4:2:0")
.field("bit-depth-luma", 8u32)
.field("bit-depth-chroma", 8u32)
.field("colorimetry", "bt709")
.build();
test_buffer_flags_single_stream(false, false, caps);
}
#[test]
@ -273,6 +316,9 @@ fn test_buffer_flags_multi_stream() {
}
}
// Crank the clock: this should bring us to the end of the first fragment
h1.crank_single_clock_wait().unwrap();
let header = h1.pull().unwrap();
assert_eq!(
header.flags(),
@ -470,8 +516,7 @@ fn test_live_timeout() {
}
}
// Advance time and crank the clock: this should bring us to the end of the first fragment
h1.set_time(5.seconds()).unwrap();
// Crank the clock: this should bring us to the end of the first fragment
h1.crank_single_clock_wait().unwrap();
let header = h1.pull().unwrap();
@ -689,8 +734,7 @@ fn test_gap_events() {
}
}
// Advance time and crank the clock: this should bring us to the end of the first fragment
h1.set_time(5.seconds()).unwrap();
// Crank the clock: this should bring us to the end of the first fragment
h1.crank_single_clock_wait().unwrap();
let header = h1.pull().unwrap();
@ -980,6 +1024,9 @@ fn test_single_stream_long_gops() {
}
}
// Crank the clock: this should bring us to the end of the first fragment
h.crank_single_clock_wait().unwrap();
let header = h.pull().unwrap();
assert_eq!(
header.flags(),
@ -1091,8 +1138,8 @@ fn test_buffer_multi_stream_short_gops() {
let output_offset = (60 * 60 * 1000).seconds();
// Push 8 buffers of 1s each, 1st, 4th and 7th buffer without DELTA_UNIT flag
for i in 0..8 {
// Push 9 buffers of 1s each, 1st, 4th and 7th buffer without DELTA_UNIT flag
for i in 0..9 {
let mut buffer = gst::Buffer::with_size(1).unwrap();
{
let buffer = buffer.get_mut().unwrap();
@ -1114,7 +1161,7 @@ fn test_buffer_multi_stream_short_gops() {
}
assert_eq!(h2.push(buffer), Ok(gst::FlowSuccess::Ok));
if i == 2 || i == 7 {
if i == 2 || i == 8 {
let ev = loop {
let ev = h1.pull_upstream_event().unwrap();
if ev.type_() != gst::EventType::Reconfigure
@ -1207,12 +1254,12 @@ fn test_buffer_multi_stream_short_gops() {
assert_eq!(fragment_header.flags(), gst::BufferFlags::HEADER);
assert_eq!(fragment_header.pts(), Some(3.seconds() + output_offset));
assert_eq!(fragment_header.dts(), Some(3.seconds() + output_offset));
assert_eq!(fragment_header.duration(), Some(5.seconds()));
assert_eq!(fragment_header.duration(), Some(6.seconds()));
for i in 3..8 {
for i in 3..9 {
for j in 0..2 {
let buffer = h1.pull().unwrap();
if i == 7 && j == 1 {
if i == 8 && j == 1 {
assert_eq!(
buffer.flags(),
gst::BufferFlags::DELTA_UNIT | gst::BufferFlags::MARKER
@ -1239,3 +1286,386 @@ fn test_buffer_multi_stream_short_gops() {
let ev = h1.pull_event().unwrap();
assert_eq!(ev.type_(), gst::EventType::Eos);
}
#[test]
fn test_chunking_single_stream() {
init();
let caps = gst::Caps::builder("video/x-h264")
.field("width", 1920i32)
.field("height", 1080i32)
.field("framerate", gst::Fraction::new(30, 1))
.field("stream-format", "avc")
.field("alignment", "au")
.field("codec_data", gst::Buffer::with_size(1).unwrap())
.build();
let mut h = gst_check::Harness::new("cmafmux");
// 5s fragment duration, 1s chunk duration
h.element()
.unwrap()
.set_property("fragment-duration", 5.seconds());
h.element()
.unwrap()
.set_property("chunk-duration", 1.seconds());
h.set_src_caps(caps);
h.play();
// Push 15 buffers of 0.5s each, 1st and 11th buffer without DELTA_UNIT flag
for i in 0..15 {
let mut buffer = gst::Buffer::with_size(1).unwrap();
{
let buffer = buffer.get_mut().unwrap();
buffer.set_pts(i * 500.mseconds());
buffer.set_dts(i * 500.mseconds());
buffer.set_duration(500.mseconds());
if i != 0 && i != 10 {
buffer.set_flags(gst::BufferFlags::DELTA_UNIT);
}
}
assert_eq!(h.push(buffer), Ok(gst::FlowSuccess::Ok));
if i == 2 {
let ev = loop {
let ev = h.pull_upstream_event().unwrap();
if ev.type_() != gst::EventType::Reconfigure
&& ev.type_() != gst::EventType::Latency
{
break ev;
}
};
assert_eq!(ev.type_(), gst::EventType::CustomUpstream);
assert_eq!(
gst_video::UpstreamForceKeyUnitEvent::parse(&ev).unwrap(),
gst_video::UpstreamForceKeyUnitEvent {
running_time: Some(5.seconds()),
all_headers: true,
count: 0
}
);
}
}
// Crank the clock: this should bring us to the end of the first fragment
h.crank_single_clock_wait().unwrap();
let header = h.pull().unwrap();
assert_eq!(
header.flags(),
gst::BufferFlags::HEADER | gst::BufferFlags::DISCONT
);
assert_eq!(header.pts(), Some(gst::ClockTime::ZERO));
assert_eq!(header.dts(), Some(gst::ClockTime::ZERO));
// There should be 7 chunks now, and the 1st and 6th are starting a fragment.
// Each chunk should have two buffers.
for chunk in 0..7 {
let chunk_header = h.pull().unwrap();
if chunk == 0 || chunk == 5 {
assert_eq!(chunk_header.flags(), gst::BufferFlags::HEADER);
} else {
assert_eq!(
chunk_header.flags(),
gst::BufferFlags::HEADER | gst::BufferFlags::DELTA_UNIT
);
}
assert_eq!(chunk_header.pts(), Some(chunk * 1.seconds()));
assert_eq!(chunk_header.dts(), Some(chunk * 1.seconds()));
assert_eq!(chunk_header.duration(), Some(1.seconds()));
for buffer_idx in 0..2 {
let buffer = h.pull().unwrap();
if buffer_idx == 1 {
assert_eq!(
buffer.flags(),
gst::BufferFlags::DELTA_UNIT | gst::BufferFlags::MARKER
);
} else {
assert_eq!(buffer.flags(), gst::BufferFlags::DELTA_UNIT);
}
assert_eq!(
buffer.pts(),
Some((chunk * 2 + buffer_idx) * 500.mseconds())
);
assert_eq!(
buffer.dts(),
Some((chunk * 2 + buffer_idx) * 500.mseconds())
);
assert_eq!(buffer.duration(), Some(500.mseconds()));
}
}
h.push_event(gst::event::Eos::new());
// There should be the remaining chunk now, containing one 500ms buffer.
for chunk in 7..8 {
let chunk_header = h.pull().unwrap();
assert_eq!(
chunk_header.flags(),
gst::BufferFlags::HEADER | gst::BufferFlags::DELTA_UNIT
);
assert_eq!(chunk_header.pts(), Some(chunk * 1.seconds()));
assert_eq!(chunk_header.dts(), Some(chunk * 1.seconds()));
assert_eq!(chunk_header.duration(), Some(500.mseconds()));
for buffer_idx in 0..1 {
let buffer = h.pull().unwrap();
assert_eq!(
buffer.flags(),
gst::BufferFlags::DELTA_UNIT | gst::BufferFlags::MARKER
);
assert_eq!(
buffer.pts(),
Some((chunk * 2 + buffer_idx) * 500.mseconds())
);
assert_eq!(
buffer.dts(),
Some((chunk * 2 + buffer_idx) * 500.mseconds())
);
assert_eq!(buffer.duration(), Some(500.mseconds()));
}
}
let ev = h.pull_event().unwrap();
assert_eq!(ev.type_(), gst::EventType::StreamStart);
let ev = h.pull_event().unwrap();
assert_eq!(ev.type_(), gst::EventType::Caps);
let ev = h.pull_event().unwrap();
assert_eq!(ev.type_(), gst::EventType::Segment);
let ev = h.pull_event().unwrap();
assert_eq!(ev.type_(), gst::EventType::Eos);
}
#[test]
fn test_chunking_multi_stream() {
init();
let mut h1 = gst_check::Harness::with_padnames("isofmp4mux", Some("sink_0"), Some("src"));
let mut h2 = gst_check::Harness::with_element(&h1.element().unwrap(), Some("sink_1"), None);
// 5s fragment duration, 1s chunk duration
h1.element()
.unwrap()
.set_property("fragment-duration", 5.seconds());
h1.element()
.unwrap()
.set_property("chunk-duration", 1.seconds());
h1.set_src_caps(
gst::Caps::builder("video/x-h264")
.field("width", 1920i32)
.field("height", 1080i32)
.field("framerate", gst::Fraction::new(30, 1))
.field("stream-format", "avc")
.field("alignment", "au")
.field("codec_data", gst::Buffer::with_size(1).unwrap())
.build(),
);
h1.play();
h2.set_src_caps(
gst::Caps::builder("audio/mpeg")
.field("mpegversion", 4i32)
.field("channels", 1i32)
.field("rate", 44100i32)
.field("stream-format", "raw")
.field("base-profile", "lc")
.field("profile", "lc")
.field("level", "2")
.field(
"codec_data",
gst::Buffer::from_slice([0x12, 0x08, 0x56, 0xe5, 0x00]),
)
.build(),
);
h2.play();
let output_offset = (60 * 60 * 1000).seconds();
// Push 15 buffers of 0.5s each, 1st and 11th buffer without DELTA_UNIT flag
for i in 0..15 {
let mut buffer = gst::Buffer::with_size(1).unwrap();
{
let buffer = buffer.get_mut().unwrap();
buffer.set_pts(i * 500.mseconds());
buffer.set_dts(i * 500.mseconds());
buffer.set_duration(500.mseconds());
if i != 0 && i != 10 {
buffer.set_flags(gst::BufferFlags::DELTA_UNIT);
}
}
assert_eq!(h1.push(buffer), Ok(gst::FlowSuccess::Ok));
let mut buffer = gst::Buffer::with_size(1).unwrap();
{
let buffer = buffer.get_mut().unwrap();
buffer.set_pts(i * 500.mseconds());
buffer.set_dts(i * 500.mseconds());
buffer.set_duration(500.mseconds());
}
assert_eq!(h2.push(buffer), Ok(gst::FlowSuccess::Ok));
if i == 2 {
let ev = loop {
let ev = h1.pull_upstream_event().unwrap();
if ev.type_() != gst::EventType::Reconfigure
&& ev.type_() != gst::EventType::Latency
{
break ev;
}
};
assert_eq!(ev.type_(), gst::EventType::CustomUpstream);
assert_eq!(
gst_video::UpstreamForceKeyUnitEvent::parse(&ev).unwrap(),
gst_video::UpstreamForceKeyUnitEvent {
running_time: Some(5.seconds()),
all_headers: true,
count: 0
}
);
let ev = loop {
let ev = h2.pull_upstream_event().unwrap();
if ev.type_() != gst::EventType::Reconfigure
&& ev.type_() != gst::EventType::Latency
{
break ev;
}
};
assert_eq!(ev.type_(), gst::EventType::CustomUpstream);
assert_eq!(
gst_video::UpstreamForceKeyUnitEvent::parse(&ev).unwrap(),
gst_video::UpstreamForceKeyUnitEvent {
running_time: Some(5.seconds()),
all_headers: true,
count: 0
}
);
}
}
// Crank the clock: this should bring us to the end of the first fragment
h1.crank_single_clock_wait().unwrap();
let header = h1.pull().unwrap();
assert_eq!(
header.flags(),
gst::BufferFlags::HEADER | gst::BufferFlags::DISCONT
);
assert_eq!(header.pts(), Some(gst::ClockTime::ZERO + output_offset));
assert_eq!(header.dts(), Some(gst::ClockTime::ZERO + output_offset));
// There should be 7 chunks now, and the 1st and 6th are starting a fragment.
// Each chunk should have two buffers.
for chunk in 0..7 {
let chunk_header = h1.pull().unwrap();
if chunk == 0 || chunk == 5 {
assert_eq!(chunk_header.flags(), gst::BufferFlags::HEADER);
} else {
assert_eq!(
chunk_header.flags(),
gst::BufferFlags::HEADER | gst::BufferFlags::DELTA_UNIT
);
}
assert_eq!(
chunk_header.pts(),
Some(chunk * 1.seconds() + output_offset)
);
assert_eq!(
chunk_header.dts(),
Some(chunk * 1.seconds() + output_offset)
);
assert_eq!(chunk_header.duration(), Some(1.seconds()));
for buffer_idx in 0..2 {
for stream_idx in 0..2 {
let buffer = h1.pull().unwrap();
if buffer_idx == 1 && stream_idx == 1 {
assert_eq!(
buffer.flags(),
gst::BufferFlags::DELTA_UNIT | gst::BufferFlags::MARKER
);
} else {
assert_eq!(buffer.flags(), gst::BufferFlags::DELTA_UNIT);
}
assert_eq!(
buffer.pts(),
Some((chunk * 2 + buffer_idx) * 500.mseconds() + output_offset)
);
if stream_idx == 0 {
assert_eq!(
buffer.dts(),
Some((chunk * 2 + buffer_idx) * 500.mseconds() + output_offset)
);
} else {
assert!(buffer.dts().is_none());
}
assert_eq!(buffer.duration(), Some(500.mseconds()));
}
}
}
h1.push_event(gst::event::Eos::new());
h2.push_event(gst::event::Eos::new());
// There should be the remaining chunk now, containing one 500ms buffer.
for chunk in 7..8 {
let chunk_header = h1.pull().unwrap();
assert_eq!(
chunk_header.flags(),
gst::BufferFlags::HEADER | gst::BufferFlags::DELTA_UNIT
);
assert_eq!(
chunk_header.pts(),
Some(chunk * 1.seconds() + output_offset)
);
assert_eq!(
chunk_header.dts(),
Some(chunk * 1.seconds() + output_offset)
);
assert_eq!(chunk_header.duration(), Some(500.mseconds()));
for buffer_idx in 0..1 {
for stream_idx in 0..2 {
let buffer = h1.pull().unwrap();
if buffer_idx == 0 && stream_idx == 1 {
assert_eq!(
buffer.flags(),
gst::BufferFlags::DELTA_UNIT | gst::BufferFlags::MARKER
);
} else {
assert_eq!(buffer.flags(), gst::BufferFlags::DELTA_UNIT);
}
assert_eq!(
buffer.pts(),
Some((chunk * 2 + buffer_idx) * 500.mseconds() + output_offset)
);
if stream_idx == 0 {
assert_eq!(
buffer.dts(),
Some((chunk * 2 + buffer_idx) * 500.mseconds() + output_offset)
);
} else {
assert!(buffer.dts().is_none());
}
assert_eq!(buffer.duration(), Some(500.mseconds()));
}
}
}
let ev = h1.pull_event().unwrap();
assert_eq!(ev.type_(), gst::EventType::StreamStart);
let ev = h1.pull_event().unwrap();
assert_eq!(ev.type_(), gst::EventType::Caps);
let ev = h1.pull_event().unwrap();
assert_eq!(ev.type_(), gst::EventType::Segment);
let ev = h1.pull_event().unwrap();
assert_eq!(ev.type_(), gst::EventType::Eos);
}

49
mux/mp4/Cargo.toml Normal file
View file

@ -0,0 +1,49 @@
[package]
name = "gst-plugin-mp4"
version = "0.9.10"
authors = ["Sebastian Dröge <sebastian@centricular.com>"]
license = "MPL-2.0"
description = "GStreamer Rust MP4 Plugin"
repository = "https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs"
edition = "2021"
rust-version = "1.63"
[dependencies]
anyhow = "1"
gst = { package = "gstreamer", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19.1", features = ["v1_18"] }
gst-base = { package = "gstreamer-base", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19", features = ["v1_18"] }
gst-audio = { package = "gstreamer-audio", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19", features = ["v1_18"] }
gst-video = { package = "gstreamer-video", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19", features = ["v1_18"] }
gst-pbutils = { package = "gstreamer-pbutils", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19", features = ["v1_18"] }
once_cell = "1.0"
[lib]
name = "gstmp4"
crate-type = ["cdylib", "rlib"]
path = "src/lib.rs"
[dev-dependencies]
tempfile = "3"
url = "2"
[build-dependencies]
gst-plugin-version-helper = { version = "0.7", path="../../version-helper" }
[features]
default = []
static = []
capi = []
doc = []
[package.metadata.capi]
min_version = "0.8.0"
[package.metadata.capi.header]
enabled = false
[package.metadata.capi.library]
install_subdir = "gstreamer-1.0"
versioning = false
[package.metadata.capi.pkg_config]
requires_private = "gstreamer-1.0, gstreamer-base-1.0, gstreamer-audio-1.0, gstreamer-video-1.0, gobject-2.0, glib-2.0, gmodule-2.0"

1
mux/mp4/LICENSE Symbolic link
View file

@ -0,0 +1 @@
../../LICENSE-MPL-2.0

3
mux/mp4/build.rs Normal file
View file

@ -0,0 +1,3 @@
fn main() {
gst_plugin_version_helper::info()
}

34
mux/mp4/src/lib.rs Normal file
View file

@ -0,0 +1,34 @@
// Copyright (C) 2022 Sebastian Dröge <sebastian@centricular.com>
//
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at
// <https://mozilla.org/MPL/2.0/>.
//
// SPDX-License-Identifier: MPL-2.0
#![allow(clippy::non_send_fields_in_send_ty, unused_doc_comments)]
/**
* plugin-mp4:
*
* Since: plugins-rs-0.10.0
*/
use gst::glib;
mod mp4mux;
fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
mp4mux::register(plugin)
}
gst::plugin_define!(
mp4,
env!("CARGO_PKG_DESCRIPTION"),
plugin_init,
concat!(env!("CARGO_PKG_VERSION"), "-", env!("COMMIT_ID")),
// FIXME: MPL-2.0 is only allowed since 1.18.3 (as unknown) and 1.20 (as known)
"MPL",
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_REPOSITORY"),
env!("BUILD_REL_DATE")
);

1784
mux/mp4/src/mp4mux/boxes.rs Normal file

File diff suppressed because it is too large Load diff

1730
mux/mp4/src/mp4mux/imp.rs Normal file

File diff suppressed because it is too large Load diff

145
mux/mp4/src/mp4mux/mod.rs Normal file
View file

@ -0,0 +1,145 @@
// Copyright (C) 2022 Sebastian Dröge <sebastian@centricular.com>
//
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at
// <https://mozilla.org/MPL/2.0/>.
//
// SPDX-License-Identifier: MPL-2.0
use gst::glib;
use gst::prelude::*;
mod boxes;
mod imp;
glib::wrapper! {
pub(crate) struct MP4MuxPad(ObjectSubclass<imp::MP4MuxPad>) @extends gst_base::AggregatorPad, gst::Pad, gst::Object;
}
glib::wrapper! {
pub(crate) struct MP4Mux(ObjectSubclass<imp::MP4Mux>) @extends gst_base::Aggregator, gst::Element, gst::Object;
}
glib::wrapper! {
pub(crate) struct ISOMP4Mux(ObjectSubclass<imp::ISOMP4Mux>) @extends MP4Mux, gst_base::Aggregator, gst::Element, gst::Object;
}
glib::wrapper! {
pub(crate) struct ONVIFMP4Mux(ObjectSubclass<imp::ONVIFMP4Mux>) @extends MP4Mux, gst_base::Aggregator, gst::Element, gst::Object;
}
pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
#[cfg(feature = "doc")]
{
MP4Mux::static_type().mark_as_plugin_api(gst::PluginAPIFlags::empty());
MP4MuxPad::static_type().mark_as_plugin_api(gst::PluginAPIFlags::empty());
}
gst::Element::register(
Some(plugin),
"isomp4mux",
gst::Rank::Marginal,
ISOMP4Mux::static_type(),
)?;
gst::Element::register(
Some(plugin),
"onvifmp4mux",
gst::Rank::Marginal,
ONVIFMP4Mux::static_type(),
)?;
Ok(())
}
#[derive(Debug, Copy, Clone)]
pub(crate) enum DeltaFrames {
/// Only single completely decodable frames
IntraOnly,
/// Frames may depend on past frames
PredictiveOnly,
/// Frames may depend on past or future frames
Bidirectional,
}
impl DeltaFrames {
/// Whether dts is required to order samples differently from presentation order
pub(crate) fn requires_dts(&self) -> bool {
matches!(self, Self::Bidirectional)
}
/// Whether this coding structure does not allow delta flags on samples
pub(crate) fn intra_only(&self) -> bool {
matches!(self, Self::IntraOnly)
}
}
#[derive(Debug)]
pub(crate) struct Sample {
/// Sync point
sync_point: bool,
/// Sample duration
duration: gst::ClockTime,
/// Composition time offset
///
/// This is `None` for streams that have no concept of DTS.
composition_time_offset: Option<i64>,
/// Size
size: u32,
}
#[derive(Debug)]
pub(crate) struct Chunk {
/// Chunk start offset
offset: u64,
/// Samples of this stream that are part of this chunk
samples: Vec<Sample>,
}
#[derive(Debug)]
pub(crate) struct Stream {
/// Caps of this stream
caps: gst::Caps,
/// If this stream has delta frames, and if so if it can have B frames.
delta_frames: DeltaFrames,
/// Pre-defined trak timescale if not 0.
trak_timescale: u32,
/// Start DTS
///
/// If this is negative then an edit list entry is needed to
/// make all sample times positive.
///
/// This is `None` for streams that have no concept of DTS.
start_dts: Option<gst::Signed<gst::ClockTime>>,
/// Earliest PTS
///
/// If this is >0 then an edit list entry is needed to shift
earliest_pts: gst::ClockTime,
/// End PTS
end_pts: gst::ClockTime,
/// All the chunks stored for this stream
chunks: Vec<Chunk>,
}
#[derive(Debug)]
pub(crate) struct Header {
#[allow(dead_code)]
variant: Variant,
/// Pre-defined movie timescale if not 0.
movie_timescale: u32,
streams: Vec<Stream>,
}
#[allow(clippy::upper_case_acronyms)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Variant {
ISO,
ONVIF,
}

128
mux/mp4/tests/tests.rs Normal file
View file

@ -0,0 +1,128 @@
// Copyright (C) 2022 Sebastian Dröge <sebastian@centricular.com>
//
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at
// <https://mozilla.org/MPL/2.0/>.
//
// SPDX-License-Identifier: MPL-2.0
//
use gst::prelude::*;
use gst_pbutils::prelude::*;
fn init() {
use std::sync::Once;
static INIT: Once = Once::new();
INIT.call_once(|| {
gst::init().unwrap();
gstmp4::plugin_register_static().unwrap();
});
}
#[test]
fn test_basic() {
init();
struct Pipeline(gst::Pipeline);
impl std::ops::Deref for Pipeline {
type Target = gst::Pipeline;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Drop for Pipeline {
fn drop(&mut self) {
let _ = self.0.set_state(gst::State::Null);
}
}
let pipeline = match gst::parse_launch(
"videotestsrc num-buffers=99 ! x264enc ! mux. \
audiotestsrc num-buffers=140 ! fdkaacenc ! mux. \
isomp4mux name=mux ! filesink name=sink \
",
) {
Ok(pipeline) => Pipeline(pipeline.downcast::<gst::Pipeline>().unwrap()),
Err(_) => return,
};
let dir = tempfile::TempDir::new().unwrap();
let mut location = dir.path().to_owned();
location.push("test.mp4");
let sink = pipeline.by_name("sink").unwrap();
sink.set_property("location", location.to_str().expect("Non-UTF8 filename"));
pipeline
.set_state(gst::State::Playing)
.expect("Unable to set the pipeline to the `Playing` state");
for msg in pipeline.bus().unwrap().iter_timed(gst::ClockTime::NONE) {
use gst::MessageView;
match msg.view() {
MessageView::Eos(..) => break,
MessageView::Error(err) => {
panic!(
"Error from {:?}: {} ({:?})",
err.src().map(|s| s.path_string()),
err.error(),
err.debug()
);
}
_ => (),
}
}
pipeline
.set_state(gst::State::Null)
.expect("Unable to set the pipeline to the `Null` state");
drop(pipeline);
let discoverer = gst_pbutils::Discoverer::new(gst::ClockTime::from_seconds(5))
.expect("Failed to create discoverer");
let info = discoverer
.discover_uri(
url::Url::from_file_path(&location)
.expect("Failed to convert filename to URL")
.as_str(),
)
.expect("Failed to discover MP4 file");
assert_eq!(info.duration(), Some(gst::ClockTime::from_mseconds(3_300)));
let audio_streams = info.audio_streams();
assert_eq!(audio_streams.len(), 1);
let audio_stream = &audio_streams[0];
assert_eq!(audio_stream.channels(), 1);
assert_eq!(audio_stream.sample_rate(), 44_100);
let caps = audio_stream.caps().unwrap();
assert!(
caps.can_intersect(
&gst::Caps::builder("audio/mpeg")
.any_features()
.field("mpegversion", 4i32)
.build()
),
"Unexpected audio caps {:?}",
caps
);
let video_streams = info.video_streams();
assert_eq!(video_streams.len(), 1);
let video_stream = &video_streams[0];
assert_eq!(video_stream.width(), 320);
assert_eq!(video_stream.height(), 240);
assert_eq!(video_stream.framerate(), gst::Fraction::new(30, 1));
assert_eq!(video_stream.par(), gst::Fraction::new(1, 1));
assert!(!video_stream.is_interlaced());
let caps = video_stream.caps().unwrap();
assert!(
caps.can_intersect(&gst::Caps::builder("video/x-h264").any_features().build()),
"Unexpected video caps {:?}",
caps
);
}

View file

@ -1,6 +1,6 @@
[package]
name = "gst-plugin-aws"
version = "0.9.0-alpha.1"
version = "0.9.10"
authors = ["Arun Raghavan <arun@arunraghavan.net>",
"Jordan Petridis <jordan@centricular.com>",
"Mathieu Duponchelle <mathieu@centricular.com>"]
@ -13,22 +13,23 @@ rust-version = "1.63"
[dependencies]
bytes = "1.0"
futures = "0.3"
gst = { package = "gstreamer", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
gst-base = { package = "gstreamer-base", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
gst-audio = { package = "gstreamer-audio", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", features = ["v1_16"] }
aws-config = "0.49.0"
aws-sdk-s3 = "0.19.0"
aws-sdk-transcribe = "0.19.0"
aws-types = "0.49.0"
aws-sig-auth = "0.49.0"
aws-smithy-http = { version = "0.49.0", features = [ "rt-tokio" ] }
aws-smithy-types = "0.49.0"
gst = { package = "gstreamer", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19.1" }
gst-base = { package = "gstreamer-base", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19" }
gst-audio = { package = "gstreamer-audio", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19", features = ["v1_16"] }
aws-config = "0.53.0"
aws-sdk-s3 = "0.23.0"
aws-sdk-transcribe = "0.23.0"
aws-types = "0.53.0"
aws-credential-types = "0.53.0"
aws-sig-auth = "0.53.0"
aws-smithy-http = { version = "0.53.0", features = [ "rt-tokio" ] }
aws-smithy-types = "0.53.0"
http = "0.2.7"
chrono = "0.4"
url = "2"
percent-encoding = "2"
tokio = { version = "1.0", features = [ "full" ] }
async-tungstenite = { version = "0.18", features = ["tokio", "tokio-runtime", "tokio-native-tls"] }
async-tungstenite = { version = "0.19", features = ["tokio", "tokio-runtime", "tokio-native-tls"] }
nom = "7"
crc = "3"
byteorder = "1.3.4"
@ -39,14 +40,14 @@ serde_json = "1"
atomic_refcell = "0.1"
base32 = "0.4"
backoff = { version = "0.4", features = [ "futures", "tokio" ] }
gio = { git = "https://github.com/gtk-rs/gtk-rs-core.git", package = "gio" }
gio = { git = "https://github.com/gtk-rs/gtk-rs-core", branch = "0.16", version = "0.16", package = "gio" }
[dev-dependencies]
chrono = { version = "0.4", features = [ "alloc" ] }
env_logger = "0.9"
gst-check = { package = "gstreamer-check", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", features = ["v1_18"] }
env_logger = "0.10"
gst-check = { package = "gstreamer-check", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19", features = ["v1_18"] }
rand = "0.8"
test-with = { version = "0.8", default-features = false }
test-with = { version = "0.9", default-features = false }
[lib]
name = "gstaws"
@ -54,7 +55,7 @@ crate-type = ["cdylib", "rlib"]
path = "src/lib.rs"
[build-dependencies]
gst-plugin-version-helper = { path="../../version-helper" }
gst-plugin-version-helper = { version = "0.7", path="../../version-helper" }
[features]
static = []

View file

@ -1,4 +1,4 @@
# gst-plugin-s3
# gst-plugin-aws
This is a [GStreamer](https://gstreamer.freedesktop.org/) plugin to interact
with [Amazon Web Services](https://aws.amazon.com/). We currently have elements

View file

@ -170,8 +170,8 @@ impl TranscribeParse {
}
};
let start_pts = ((start_time as f64 * 1_000_000_000.0) as u64).nseconds();
let end_pts = ((end_time as f64 * 1_000_000_000.0) as u64).nseconds();
let start_pts = ((start_time * 1_000_000_000.0) as u64).nseconds();
let end_pts = ((end_time * 1_000_000_000.0) as u64).nseconds();
let duration = end_pts.saturating_sub(start_pts);
if start_pts > last_pts {

View file

@ -14,11 +14,11 @@ use gst::{element_imp_error, error_msg, loggable_error};
use std::default::Default;
use aws_config::default_provider::credentials::DefaultCredentialsChain;
use aws_credential_types::{provider::ProvideCredentials, Credentials};
use aws_sig_auth::signer::{self, HttpSignatureType, OperationSigningConfig, RequestConfig};
use aws_smithy_http::body::SdkBody;
use aws_types::credentials::ProvideCredentials;
use aws_types::region::{Region, SigningRegion};
use aws_types::{Credentials, SigningService};
use aws_types::SigningService;
use std::time::{Duration, SystemTime};
use chrono::prelude::*;

View file

@ -22,10 +22,8 @@ use gst::{element_imp_error, glib, prelude::*, subclass::prelude::*};
use aws_sdk_s3::config;
use aws_sdk_s3::model::ObjectCannedAcl;
use aws_sdk_s3::types::ByteStream;
use aws_sdk_s3::Endpoint;
use aws_sdk_s3::{Client, Credentials, Region, RetryConfig};
use aws_sdk_s3::{config::retry::RetryConfig, Client, Credentials, Region};
use aws_types::sdk_config::SdkConfig;
use http::Uri;
use crate::s3utils;
@ -85,6 +83,7 @@ impl Default for Settings {
pub struct S3HlsSink {
settings: Mutex<Settings>,
state: Mutex<State>,
hlssink: gst::Element,
canceller: Mutex<Option<future::AbortHandle>>,
}
@ -132,6 +131,18 @@ enum S3RequestControl {
Pause,
}
enum State {
Stopped,
Started(Started),
}
#[derive(Default)]
struct Started {
num_uploads_started: usize,
num_uploads_completed: usize,
num_bytes_uploaded: usize,
}
impl S3Upload {
fn new(
s3_client: Client,
@ -258,8 +269,9 @@ impl S3HlsSink {
let put_object_req_future = put_object_req.send();
let result = s3utils::wait(&self.canceller, put_object_req_future);
if let Err(err) = result {
gst::error!(
match result {
Err(err) => {
gst::error!(
CAT,
imp: self,
"Put object request for S3 key {} of data length {} failed with error {:?}",
@ -267,12 +279,25 @@ impl S3HlsSink {
s3_data_len,
err,
);
element_imp_error!(
self,
gst::ResourceError::Write,
["Put object request failed"]
);
break;
element_imp_error!(
self,
gst::ResourceError::Write,
["Put object request failed"]
);
break;
}
Ok(_) => {
let mut state = self.state.lock().unwrap();
match *state {
State::Started(ref mut state) => {
state.num_bytes_uploaded += s3_data_len;
state.num_uploads_completed += 1;
}
State::Stopped => {
unreachable!("State not started yet")
}
};
}
};
}
Ok(S3Request::Delete(data)) => {
@ -349,29 +374,13 @@ impl S3HlsSink {
}
let sdk_config = settings.config.as_ref().expect("SDK config must be set");
let endpoint_uri = match &settings.endpoint_uri {
Some(endpoint) => match endpoint.parse::<Uri>() {
Ok(uri) => Some(uri),
Err(e) => {
element_imp_error!(
self,
gst::ResourceError::Settings,
["Invalid S3 endpoint uri. Error: {}", e]
);
None
}
},
None => None,
};
let config_builder = config::Builder::from(sdk_config)
.region(settings.s3_region.clone())
.retry_config(RetryConfig::standard().with_max_attempts(settings.retry_attempts));
let config = if let Some(uri) = endpoint_uri {
config_builder
.endpoint_resolver(Endpoint::mutable(uri))
.build()
let config = if let Some(ref uri) = settings.endpoint_uri {
config_builder.endpoint_url(uri).build()
} else {
config_builder.build()
};
@ -399,6 +408,27 @@ impl S3HlsSink {
}
};
};
let mut state = self.state.lock().unwrap();
*state = State::Stopped
}
fn create_stats(&self) -> gst::Structure {
let state = self.state.lock().unwrap();
match &*state {
State::Started(state) => gst::Structure::builder("stats")
.field("num-uploads-started", state.num_uploads_started as u32)
.field("num-uploads-completed", state.num_uploads_completed as u32)
.field("num-bytes-uploaded", state.num_bytes_uploaded as u32)
.build(),
State::Stopped => gst::Structure::builder("stats")
.field("num-uploads-started", 0)
.field("num-uploads-completed", 0)
.field("num-bytes-uploaded", 0)
.build(),
}
}
}
@ -411,18 +441,19 @@ impl ObjectSubclass for S3HlsSink {
fn with_class(_klass: &Self::Class) -> Self {
/* Prefer hlssink3 here due to it's support for media playlist types */
let hlssink = match gst::ElementFactory::make("hlssink3")
.name("hlssink3")
.name("hlssink")
.build()
{
Ok(element) => element,
Err(_) => gst::ElementFactory::make("hlssink2")
.name("hlssink2")
.name("hlssink")
.build()
.expect("Could not find hlssink2. Need hlssink2 or hlssink3."),
};
Self {
settings: Mutex::new(Settings::default()),
state: Mutex::new(State::Stopped),
hlssink,
canceller: Mutex::new(None),
}
@ -488,6 +519,13 @@ impl ObjectImpl for S3HlsSink {
.minimum(1)
.default_value(DEFAULT_TIMEOUT_IN_MSECS)
.build(),
glib::ParamSpecBoxed::new(
"stats",
"Various statistics",
"Various statistics",
gst::Structure::static_type(),
glib::ParamFlags::READABLE,
),
glib::ParamSpecString::builder("endpoint-uri")
.nick("S3 endpoint URI")
.blurb("The S3 endpoint URI to use")
@ -568,6 +606,7 @@ impl ObjectImpl for S3HlsSink {
"acl" => settings.s3_acl.as_str().to_value(),
"retry-attempts" => settings.retry_attempts.to_value(),
"request-timeout" => (settings.request_timeout.as_millis() as u64).to_value(),
"stats" => self.create_stats().to_value(),
"endpoint-uri" => settings.endpoint_uri.to_value(),
_ => unimplemented!(),
}
@ -610,6 +649,12 @@ impl ObjectImpl for S3HlsSink {
let s3client = self_.s3client_from_settings();
let settings = self_.settings.lock().unwrap();
let mut state = self_.state.lock().unwrap();
match *state {
State::Started(ref mut state) => state.num_uploads_started += 1,
State::Stopped => unreachable!("State not started yet"),
};
drop(state);
let s3_location = args[1].get::<&str>().unwrap();
let upload = S3Upload::new(
@ -639,6 +684,12 @@ impl ObjectImpl for S3HlsSink {
let s3client = self_.s3client_from_settings();
let settings = self_.settings.lock().unwrap();
let mut state = self_.state.lock().unwrap();
match *state {
State::Started(ref mut state) => state.num_uploads_started += 1,
State::Stopped => unreachable!("State not started yet"),
};
drop(state);
let s3_location = args[1].get::<&str>().unwrap();
let upload = S3Upload::new(
@ -691,7 +742,7 @@ impl ObjectImpl for S3HlsSink {
// The signature on delete-fragment signal is different for
// hlssink2 and hlssink3.
if self_.hlssink.name().contains("hlssink3") {
if self_.hlssink.factory().unwrap().name().contains("hlssink3") {
if res.is_ok() {
Some(true.to_value())
} else {
@ -766,6 +817,24 @@ impl ElementImpl for S3HlsSink {
let settings = self.settings.lock().unwrap();
match transition {
gst::StateChange::ReadyToPaused => {
let mut state = self.state.lock().unwrap();
*state = State::Started(Started::default());
}
gst::StateChange::PausedToPlaying => {
let s3_txc = settings.s3_txc.clone();
if let Some(tx) = s3_txc {
gst::debug!(
CAT,
imp: self,
"Sending continue request to S3 request thread."
);
if tx.send(S3RequestControl::Continue).is_err() {
gst::error!(CAT, imp: self, "Could not send continue request.");
}
}
}
gst::StateChange::PlayingToPaused => {
let s3_txc = settings.s3_txc.clone();
if let Some(tx) = s3_txc {
@ -781,19 +850,7 @@ impl ElementImpl for S3HlsSink {
}
}
}
gst::StateChange::PausedToPlaying => {
let s3_txc = settings.s3_txc.clone();
if let Some(tx) = s3_txc {
gst::debug!(
CAT,
imp: self,
"Sending continue request to S3 request thread."
);
if tx.send(S3RequestControl::Continue).is_err() {
gst::error!(CAT, imp: self, "Could not send continue request.");
}
}
}
gst::StateChange::ReadyToNull => {
drop(settings);
/*

View file

@ -17,9 +17,7 @@ use aws_sdk_s3::client::fluent_builders::{
use aws_sdk_s3::config;
use aws_sdk_s3::model::{CompletedMultipartUpload, CompletedPart};
use aws_sdk_s3::types::ByteStream;
use aws_sdk_s3::Endpoint;
use aws_sdk_s3::{Client, Credentials, Region, RetryConfig};
use http::Uri;
use aws_sdk_s3::{config::retry::RetryConfig, Client, Credentials, Region};
use futures::future;
use once_cell::sync::Lazy;
@ -88,6 +86,7 @@ impl Started {
enum State {
Stopped,
Completed,
Started(Started),
}
@ -102,6 +101,7 @@ struct Settings {
bucket: Option<String>,
key: Option<String>,
content_type: Option<String>,
content_disposition: Option<String>,
buffer_size: u64,
access_key: Option<String>,
secret_access_key: Option<String>,
@ -154,6 +154,7 @@ impl Default for Settings {
bucket: None,
key: None,
content_type: None,
content_disposition: None,
access_key: None,
secret_access_key: None,
session_token: None,
@ -185,12 +186,68 @@ static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
});
impl S3Sink {
fn flush_multipart_upload(&self, state: &mut Started) {
let settings = self.settings.lock().unwrap();
match settings.multipart_upload_on_error {
OnError::Abort => {
gst::log!(
CAT,
imp: self,
"Aborting multipart upload request with id: {}",
state.upload_id
);
match self.abort_multipart_upload_request(state) {
Ok(()) => {
gst::log!(
CAT,
imp: self,
"Aborting multipart upload request succeeded."
);
}
Err(err) => gst::error!(
CAT,
imp: self,
"Aborting multipart upload failed: {}",
err.to_string()
),
}
}
OnError::Complete => {
gst::log!(
CAT,
imp: self,
"Completing multipart upload request with id: {}",
state.upload_id
);
match self.complete_multipart_upload_request(state) {
Ok(()) => {
gst::log!(
CAT,
imp: self,
"Complete multipart upload request succeeded."
);
}
Err(err) => gst::error!(
CAT,
imp: self,
"Completing multipart upload failed: {}",
err.to_string()
),
}
}
OnError::DoNothing => (),
}
}
fn flush_current_buffer(&self) -> Result<(), Option<gst::ErrorMessage>> {
let upload_part_req: UploadPart = self.create_upload_part_request()?;
let mut state = self.state.lock().unwrap();
let state = match *state {
State::Started(ref mut started_state) => started_state,
State::Completed => {
unreachable!("Upload should not be completed yet");
}
State::Stopped => {
unreachable!("Element should be started");
}
@ -202,56 +259,7 @@ impl S3Sink {
let output =
s3utils::wait(&self.canceller, upload_part_req_future).map_err(|err| match err {
WaitError::FutureError(err) => {
let settings = self.settings.lock().unwrap();
match settings.multipart_upload_on_error {
OnError::Abort => {
gst::log!(
CAT,
imp: self,
"Aborting multipart upload request with id: {}",
state.upload_id
);
match self.abort_multipart_upload_request(state) {
Ok(()) => {
gst::log!(
CAT,
imp: self,
"Aborting multipart upload request succeeded."
);
}
Err(err) => gst::error!(
CAT,
imp: self,
"Aborting multipart upload failed: {}",
err.to_string()
),
}
}
OnError::Complete => {
gst::log!(
CAT,
imp: self,
"Completing multipart upload request with id: {}",
state.upload_id
);
match self.complete_multipart_upload_request(state) {
Ok(()) => {
gst::log!(
CAT,
imp: self,
"Complete multipart upload request succeeded."
);
}
Err(err) => gst::error!(
CAT,
imp: self,
"Completing multipart upload failed: {}",
err.to_string()
),
}
}
OnError::DoNothing => (),
}
self.flush_multipart_upload(state);
Some(gst::error_msg!(
gst::ResourceError::OpenWrite,
["Failed to upload part: {}", err]
@ -277,6 +285,9 @@ impl S3Sink {
let mut state = self.state.lock().unwrap();
let state = match *state {
State::Started(ref mut started_state) => started_state,
State::Completed => {
unreachable!("Upload should not be completed yet");
}
State::Stopped => {
unreachable!("Element should be started");
}
@ -341,6 +352,7 @@ impl S3Sink {
let bucket = Some(url.bucket.clone());
let key = Some(url.object.clone());
let content_type = settings.content_type.clone();
let content_disposition = settings.content_disposition.clone();
let metadata = settings.to_metadata(self);
client
@ -348,6 +360,7 @@ impl S3Sink {
.set_bucket(bucket)
.set_key(key)
.set_content_type(content_type)
.set_content_disposition(content_disposition)
.set_metadata(metadata)
}
@ -437,12 +450,21 @@ impl S3Sink {
let mut state = self.state.lock().unwrap();
let started_state = match *state {
State::Started(ref mut started_state) => started_state,
State::Completed => {
unreachable!("Upload should not be completed yet");
}
State::Stopped => {
unreachable!("Element should be started");
}
};
self.complete_multipart_upload_request(started_state)
let res = self.complete_multipart_upload_request(started_state);
if res.is_ok() {
*state = State::Completed;
}
res
}
fn start(&self) -> Result<(), gst::ErrorMessage> {
@ -497,26 +519,11 @@ impl S3Sink {
}
})?;
let endpoint_uri = match &settings.endpoint_uri {
Some(endpoint) => match endpoint.parse::<Uri>() {
Ok(uri) => Some(uri),
Err(e) => {
return Err(gst::error_msg!(
gst::ResourceError::Settings,
["Invalid S3 endpoint uri. Error: {}", e]
));
}
},
None => None,
};
let config_builder = config::Builder::from(&sdk_config)
.retry_config(RetryConfig::standard().with_max_attempts(settings.retry_attempts));
let config = if let Some(uri) = endpoint_uri {
config_builder
.endpoint_resolver(Endpoint::mutable(uri))
.build()
let config = if let Some(ref uri) = settings.endpoint_uri {
config_builder.endpoint_url(uri).build()
} else {
config_builder.build()
};
@ -562,6 +569,9 @@ impl S3Sink {
let mut state = self.state.lock().unwrap();
let started_state = match *state {
State::Started(ref mut started_state) => started_state,
State::Completed => {
unreachable!("Upload should not be completed yet");
}
State::Stopped => {
unreachable!("Element should be started already");
}
@ -747,6 +757,14 @@ impl ObjectImpl for S3Sink {
.nick("S3 endpoint URI")
.blurb("The S3 endpoint URI to use")
.build(),
glib::ParamSpecString::builder("content-type")
.nick("content-type")
.blurb("Content-Type header to set for uploaded object")
.build(),
glib::ParamSpecString::builder("content-disposition")
.nick("content-disposition")
.blurb("Content-Disposition header to set for uploaded object")
.build(),
]
});
@ -850,6 +868,16 @@ impl ObjectImpl for S3Sink {
let _ = self.set_uri(Some(&settings.to_uri()));
}
}
"content-type" => {
settings.content_type = value
.get::<Option<String>>()
.expect("type checked upstream");
}
"content-disposition" => {
settings.content_disposition = value
.get::<Option<String>>()
.expect("type checked upstream");
}
_ => unimplemented!(),
}
}
@ -889,6 +917,8 @@ impl ObjectImpl for S3Sink {
(settings.retry_attempts as i64 * request_timeout).to_value()
}
"endpoint-uri" => settings.endpoint_uri.to_value(),
"content-type" => settings.content_type.to_value(),
"content-disposition" => settings.content_disposition.to_value(),
_ => unimplemented!(),
}
}
@ -953,6 +983,17 @@ impl BaseSinkImpl for S3Sink {
fn stop(&self) -> Result<(), gst::ErrorMessage> {
let mut state = self.state.lock().unwrap();
if let State::Started(ref mut state) = *state {
gst::warning!(CAT, imp: self, "Stopped without EOS");
// We're stopping without an EOS -- treat this as an error and deal with the open
// multipart upload accordingly _if_ we managed to upload any parts
if !state.completed_parts.is_empty() {
self.flush_multipart_upload(state);
}
}
*state = State::Stopped;
gst::info!(CAT, imp: self, "Stopped");
@ -965,6 +1006,15 @@ impl BaseSinkImpl for S3Sink {
return Err(gst::FlowError::Error);
}
if let State::Completed = *self.state.lock().unwrap() {
gst::element_imp_error!(
self,
gst::CoreError::Failed,
["Trying to render after upload complete"]
);
return Err(gst::FlowError::Error);
}
gst::trace!(CAT, imp: self, "Rendering {:?}", buffer);
let map = buffer.map_readable().map_err(|_| {
gst::element_imp_error!(self, gst::CoreError::Failed, ["Failed to map buffer"]);

View file

@ -13,9 +13,7 @@ use std::sync::Mutex;
use std::time::Duration;
use aws_sdk_s3::config;
use aws_sdk_s3::Endpoint;
use aws_sdk_s3::{Client, Credentials, RetryConfig};
use http::Uri;
use aws_sdk_s3::{config::retry::RetryConfig, Client, Credentials};
use gst::glib;
use gst::prelude::*;
@ -130,26 +128,11 @@ impl S3Src {
}
})?;
let endpoint_uri = match &settings.endpoint_uri {
Some(endpoint) => match endpoint.parse::<Uri>() {
Ok(uri) => Some(uri),
Err(e) => {
return Err(gst::error_msg!(
gst::ResourceError::Settings,
["Invalid S3 endpoint uri. Error: {}", e]
));
}
},
None => None,
};
let config_builder = config::Builder::from(&sdk_config)
.retry_config(RetryConfig::standard().with_max_attempts(settings.retry_attempts));
let config = if let Some(uri) = endpoint_uri {
config_builder
.endpoint_resolver(Endpoint::mutable(uri))
.build()
let config = if let Some(ref uri) = settings.endpoint_uri {
config_builder.endpoint_url(uri).build()
} else {
config_builder.build()
};

View file

@ -102,7 +102,7 @@ pub fn parse_s3_url(url_str: &str) -> Result<GstS3Url, String> {
Some(_) => return Err("Bad query, only 'version' is supported".to_owned()),
};
if q.next() != None {
if q.next().is_some() {
return Err("Extra query terms, only 'version' is supported".to_owned());
}

View file

@ -7,11 +7,10 @@
// SPDX-License-Identifier: MPL-2.0
use aws_config::meta::region::RegionProviderChain;
use aws_sdk_s3::{Credentials, Region};
use aws_sdk_s3::{config::timeout::TimeoutConfig, Credentials, Region};
use aws_types::sdk_config::SdkConfig;
use aws_smithy_http::byte_stream::{ByteStream, Error};
use aws_smithy_types::{timeout, tristate::TriState};
use aws_smithy_http::byte_stream::{error::Error, ByteStream};
use bytes::{buf::BufMut, Bytes, BytesMut};
use futures::stream::TryStreamExt;
@ -96,19 +95,16 @@ pub fn wait_stream(
}
// See setting-timeouts example in aws-sdk-rust.
pub fn timeout_config(request_timeout: Duration) -> timeout::Config {
timeout::Config::new().with_api_timeouts(
timeout::Api::new()
// This timeout acts at the "HTTP request" level and sets a separate timeout for each
// HTTP request made as part of a "service request."
.with_call_attempt_timeout(TriState::Set(request_timeout)),
)
pub fn timeout_config(request_timeout: Duration) -> TimeoutConfig {
TimeoutConfig::builder()
.operation_attempt_timeout(request_timeout)
.build()
}
pub fn wait_config(
canceller: &Mutex<Option<future::AbortHandle>>,
region: Region,
timeout_config: timeout::Config,
timeout_config: TimeoutConfig,
credentials: Option<Credentials>,
) -> Result<SdkConfig, WaitError<Error>> {
let region_provider = RegionProviderChain::first_try(region)

View file

@ -22,6 +22,8 @@ fn init() {
});
}
// The test times out on Windows for some reason, skip until we figure out why
#[cfg(not(target_os = "windows"))]
#[test_with::env(AWS_ACCESS_KEY_ID)]
#[test_with::env(AWS_SECRET_ACCESS_KEY)]
#[tokio::test]

View file

@ -2,27 +2,27 @@
name = "gst-plugin-hlssink3"
description = "GStreamer HLS (HTTP Live Streaming) Plugin"
repository = "https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs"
version = "0.9.0-alpha.1"
version = "0.9.10"
authors = ["Rafael Caricio <rafael@caricio.com>"]
edition = "2021"
license = "MPL-2.0"
rust-version = "1.63"
[dependencies]
gst = { package = "gstreamer", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
gst-base = { package = "gstreamer-base", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
glib = { git = "https://github.com/gtk-rs/gtk-rs-core" }
gio = { git = "https://github.com/gtk-rs/gtk-rs-core" }
gst = { package = "gstreamer", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19.1" }
gst-base = { package = "gstreamer-base", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19" }
glib = { git = "https://github.com/gtk-rs/gtk-rs-core", branch = "0.16", version = "0.16.2" }
gio = { git = "https://github.com/gtk-rs/gtk-rs-core", branch = "0.16", version = "0.16" }
once_cell = "1.7.2"
m3u8-rs = "5.0"
regex = "1"
[dev-dependencies]
gst-audio = { package = "gstreamer-audio", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
gst-check = { package = "gstreamer-check", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
gst-audio = { package = "gstreamer-audio", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19" }
gst-check = { package = "gstreamer-check", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19" }
[build-dependencies]
gst-plugin-version-helper = { path = "../../version-helper" }
gst-plugin-version-helper = { version = "0.7", path = "../../version-helper" }
[lib]
name = "gsthlssink3"

View file

@ -25,6 +25,7 @@ const DEFAULT_MAX_NUM_SEGMENT_FILES: u32 = 10;
const DEFAULT_TARGET_DURATION: u32 = 15;
const DEFAULT_PLAYLIST_LENGTH: u32 = 5;
const DEFAULT_PLAYLIST_TYPE: HlsSink3PlaylistType = HlsSink3PlaylistType::Unspecified;
const DEFAULT_I_FRAMES_ONLY_PLAYLIST: bool = false;
const DEFAULT_SEND_KEYFRAME_REQUESTS: bool = true;
const SIGNAL_GET_PLAYLIST_STREAM: &str = "get-playlist-stream";
@ -66,6 +67,7 @@ struct Settings {
playlist_type: Option<MediaPlaylistType>,
max_num_segment_files: usize,
target_duration: u32,
i_frames_only: bool,
send_keyframe_requests: bool,
splitmuxsink: gst::Element,
@ -94,6 +96,7 @@ impl Default for Settings {
max_num_segment_files: DEFAULT_MAX_NUM_SEGMENT_FILES as usize,
target_duration: DEFAULT_TARGET_DURATION,
send_keyframe_requests: DEFAULT_SEND_KEYFRAME_REQUESTS,
i_frames_only: DEFAULT_I_FRAMES_ONLY_PLAYLIST,
splitmuxsink,
giostreamsink,
@ -111,9 +114,13 @@ pub(crate) struct StartedState {
}
impl StartedState {
fn new(target_duration: f32, playlist_type: Option<MediaPlaylistType>) -> Self {
fn new(
target_duration: f32,
playlist_type: Option<MediaPlaylistType>,
i_frames_only: bool,
) -> Self {
Self {
playlist: Playlist::new(target_duration, playlist_type),
playlist: Playlist::new(target_duration, playlist_type, i_frames_only),
current_segment_location: None,
fragment_opened_at: None,
old_segment_locations: Vec::new(),
@ -148,17 +155,22 @@ impl HlsSink3 {
fn start(&self) {
gst::info!(CAT, imp: self, "Starting");
let (target_duration, playlist_type) = {
let (target_duration, playlist_type, i_frames_only) = {
let settings = self.settings.lock().unwrap();
(
settings.target_duration as f32,
settings.playlist_type.clone(),
settings.i_frames_only,
)
};
let mut state = self.state.lock().unwrap();
if let State::Stopped = *state {
*state = State::Started(StartedState::new(target_duration, playlist_type));
*state = State::Started(StartedState::new(
target_duration,
playlist_type,
i_frames_only,
));
}
}
@ -405,7 +417,7 @@ impl BinImpl for HlsSink3 {
"splitmuxsink-fragment-closed" => {
let s = msg.structure().unwrap();
if let Ok(fragment_closed_at) = s.get::<gst::ClockTime>("running-time") {
self.write_playlist(Some(fragment_closed_at)).unwrap();
let _ = self.write_playlist(Some(fragment_closed_at));
}
}
_ => {}
@ -452,6 +464,11 @@ impl ObjectImpl for HlsSink3 {
.nick("Playlist Type")
.blurb("The type of the playlist to use. When VOD type is set, the playlist will be live until the pipeline ends execution.")
.build(),
glib::ParamSpecBoolean::builder("i-frames-only")
.nick("I-Frames only playlist")
.blurb("Each video segments is single iframe, So put EXT-X-I-FRAMES-ONLY tag in the playlist")
.default_value(DEFAULT_I_FRAMES_ONLY_PLAYLIST)
.build(),
glib::ParamSpecBoolean::builder("send-keyframe-requests")
.nick("Send Keyframe Requests")
.blurb("Send keyframe requests to ensure correct fragmentation. If this is disabled then the input must have keyframes in regular intervals.")
@ -509,6 +526,17 @@ impl ObjectImpl for HlsSink3 {
.expect("type checked upstream")
.into();
}
"i-frames-only" => {
settings.i_frames_only = value.get().expect("type checked upstream");
if settings.i_frames_only && settings.audio_sink {
gst::element_error!(
self.obj(),
gst::StreamError::WrongType,
("Invalid configuration"),
["Audio not allowed for i-frames-only-stream"]
);
}
}
"send-keyframe-requests" => {
settings.send_keyframe_requests = value.get().expect("type checked upstream");
settings
@ -535,6 +563,7 @@ impl ObjectImpl for HlsSink3 {
let playlist_type: HlsSink3PlaylistType = settings.playlist_type.as_ref().into();
playlist_type.to_value()
}
"i-frames-only" => settings.i_frames_only.to_value(),
"send-keyframe-requests" => settings.send_keyframe_requests.to_value(),
_ => unimplemented!(),
}
@ -545,7 +574,7 @@ impl ObjectImpl for HlsSink3 {
vec![
glib::subclass::Signal::builder(SIGNAL_GET_PLAYLIST_STREAM)
.param_types([String::static_type()])
.return_type::<gio::OutputStream>()
.return_type::<Option<gio::OutputStream>>()
.class_handler(|_, args| {
let element = args[0]
.get::<super::HlsSink3>()
@ -554,12 +583,7 @@ impl ObjectImpl for HlsSink3 {
args[1].get::<String>().expect("playlist-stream signal arg");
let hlssink3 = element.imp();
Some(
hlssink3
.new_file_stream(&playlist_location)
.ok()?
.to_value(),
)
Some(hlssink3.new_file_stream(&playlist_location).ok().to_value())
})
.accumulator(|_hint, ret, value| {
// First signal handler wins
@ -569,7 +593,7 @@ impl ObjectImpl for HlsSink3 {
.build(),
glib::subclass::Signal::builder(SIGNAL_GET_FRAGMENT_STREAM)
.param_types([String::static_type()])
.return_type::<gio::OutputStream>()
.return_type::<Option<gio::OutputStream>>()
.class_handler(|_, args| {
let element = args[0]
.get::<super::HlsSink3>()
@ -578,12 +602,7 @@ impl ObjectImpl for HlsSink3 {
args[1].get::<String>().expect("fragment-stream signal arg");
let hlssink3 = element.imp();
Some(
hlssink3
.new_file_stream(&fragment_location)
.ok()?
.to_value(),
)
Some(hlssink3.new_file_stream(&fragment_location).ok().to_value())
})
.accumulator(|_hint, ret, value| {
// First signal handler wins
@ -738,7 +757,8 @@ impl ElementImpl for HlsSink3 {
};
if write_final {
self.write_final_playlist()?;
// Don't fail transitioning to READY if this fails
let _ = self.write_final_playlist();
}
}
gst::StateChange::ReadyToNull => {
@ -767,6 +787,15 @@ impl ElementImpl for HlsSink3 {
);
return None;
}
if settings.i_frames_only {
gst::element_error!(
self.obj(),
gst::StreamError::WrongType,
("Invalid configuration"),
["Audio not allowed for i-frames-only-stream"]
);
return None;
}
let peer_pad = settings.splitmuxsink.request_pad_simple("audio_0").unwrap();
let sink_pad =

View file

@ -11,7 +11,8 @@ use once_cell::sync::Lazy;
use regex::Regex;
use std::io::Write;
const GST_M3U8_PLAYLIST_VERSION: usize = 3;
const GST_M3U8_PLAYLIST_V3: usize = 3;
const GST_M3U8_PLAYLIST_V4: usize = 4;
static SEGMENT_IDX_PATTERN: Lazy<regex::Regex> = Lazy::new(|| Regex::new(r"(%0(\d+)d)").unwrap());
@ -29,7 +30,11 @@ pub struct Playlist {
}
impl Playlist {
pub fn new(target_duration: f32, playlist_type: Option<MediaPlaylistType>) -> Self {
pub fn new(
target_duration: f32,
playlist_type: Option<MediaPlaylistType>,
i_frames_only: bool,
) -> Self {
let mut turn_vod = false;
let playlist_type = if playlist_type == Some(MediaPlaylistType::Vod) {
turn_vod = true;
@ -37,16 +42,21 @@ impl Playlist {
} else {
playlist_type
};
let m3u8_version = if i_frames_only {
GST_M3U8_PLAYLIST_V4
} else {
GST_M3U8_PLAYLIST_V3
};
Self {
inner: MediaPlaylist {
version: Some(GST_M3U8_PLAYLIST_VERSION),
version: Some(m3u8_version),
target_duration,
media_sequence: 0,
segments: vec![],
discontinuity_sequence: 0,
end_list: false,
playlist_type,
i_frames_only: false,
i_frames_only,
start: None,
independent_segments: false,
unknown_tags: vec![],
@ -93,7 +103,7 @@ impl Playlist {
}
self.playlist_index += 1;
self.inner.media_sequence = self.playlist_index as u64 - self.inner.segments.len() as u64;
self.inner.media_sequence = self.playlist_index - self.inner.segments.len() as u64;
}
/// Sets the playlist to started state.

View file

@ -1,6 +1,6 @@
[package]
name = "gst-plugin-ndi"
version = "0.9.0-alpha.1"
version = "0.9.10"
authors = ["Ruben Gonzalez <rubenrua@teltek.es>", "Daniel Vilar <daniel.peiteado@teltek.es>", "Sebastian Dröge <sebastian@centricular.com>"]
repository = "https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs"
license = "MPL-2.0"
@ -9,11 +9,11 @@ edition = "2021"
rust-version = "1.63"
[dependencies]
glib = { git = "https://github.com/gtk-rs/gtk-rs-core"}
gst = { package = "gstreamer", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
gst-base = { package = "gstreamer-base", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
gst-audio = { package = "gstreamer-audio", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
gst-video = { package = "gstreamer-video", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
glib = { git = "https://github.com/gtk-rs/gtk-rs-core", branch = "0.16", version = "0.16.2"}
gst = { package = "gstreamer", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19.1" }
gst-base = { package = "gstreamer-base", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19" }
gst-audio = { package = "gstreamer-audio", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19" }
gst-video = { package = "gstreamer-video", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19" }
byte-slice-cast = "1"
once_cell = "1.0"
byteorder = "1.0"
@ -21,7 +21,7 @@ atomic_refcell = "0.1"
libloading = "0.7"
[build-dependencies]
gst-plugin-version-helper = { path = "../../version-helper" }
gst-plugin-version-helper = { version = "0.7", path = "../../version-helper" }
[features]
default = ["interlaced-fields", "sink"]

View file

@ -215,16 +215,12 @@ impl GstObjectImpl for Device {}
impl DeviceImpl for Device {
fn create_element(&self, name: Option<&str>) -> Result<gst::Element, gst::LoggableError> {
let source_info = self.source.get().unwrap();
let element = glib::Object::with_type(
crate::ndisrc::NdiSrc::static_type(),
&[
("name", &name),
("ndi-name", &source_info.ndi_name()),
("url-address", &source_info.url_address()),
],
)
.dynamic_cast::<gst::Element>()
.unwrap();
let element = glib::Object::builder::<crate::ndisrc::NdiSrc>()
.property("name", name)
.property("ndi-name", source_info.ndi_name())
.property("url-address", source_info.url_address())
.build()
.upcast::<gst::Element>();
Ok(element)
}
@ -242,16 +238,16 @@ impl super::Device {
// Put the url-address into the extra properties
let extra_properties = gst::Structure::builder("properties")
.field("ndi-name", &source.ndi_name())
.field("url-address", &source.url_address())
.field("ndi-name", source.ndi_name())
.field("url-address", source.url_address())
.build();
let device = glib::Object::new::<super::Device>(&[
("caps", &caps),
("display-name", &display_name),
("device-class", &device_class),
("properties", &extra_properties),
]);
let device = glib::Object::builder::<super::Device>()
.property("caps", caps)
.property("display-name", display_name)
.property("device-class", device_class)
.property("properties", extra_properties)
.build();
let imp = device.imp();
imp.source.set(source.to_owned()).unwrap();

View file

@ -121,23 +121,23 @@ impl ElementImpl for NdiSink {
gst::Structure::builder("video/x-raw")
.field(
"format",
&gst::List::new(&[
&gst_video::VideoFormat::Uyvy.to_str(),
&gst_video::VideoFormat::I420.to_str(),
&gst_video::VideoFormat::Nv12.to_str(),
&gst_video::VideoFormat::Nv21.to_str(),
&gst_video::VideoFormat::Yv12.to_str(),
&gst_video::VideoFormat::Bgra.to_str(),
&gst_video::VideoFormat::Bgrx.to_str(),
&gst_video::VideoFormat::Rgba.to_str(),
&gst_video::VideoFormat::Rgbx.to_str(),
gst::List::new([
gst_video::VideoFormat::Uyvy.to_str(),
gst_video::VideoFormat::I420.to_str(),
gst_video::VideoFormat::Nv12.to_str(),
gst_video::VideoFormat::Nv21.to_str(),
gst_video::VideoFormat::Yv12.to_str(),
gst_video::VideoFormat::Bgra.to_str(),
gst_video::VideoFormat::Bgrx.to_str(),
gst_video::VideoFormat::Rgba.to_str(),
gst_video::VideoFormat::Rgbx.to_str(),
]),
)
.field("width", &gst::IntRange::<i32>::new(1, std::i32::MAX))
.field("height", &gst::IntRange::<i32>::new(1, std::i32::MAX))
.field("width", gst::IntRange::<i32>::new(1, std::i32::MAX))
.field("height", gst::IntRange::<i32>::new(1, std::i32::MAX))
.field(
"framerate",
&gst::FractionRange::new(
gst::FractionRange::new(
gst::Fraction::new(0, 1),
gst::Fraction::new(std::i32::MAX, 1),
),
@ -146,10 +146,10 @@ impl ElementImpl for NdiSink {
)
.structure(
gst::Structure::builder("audio/x-raw")
.field("format", &gst_audio::AUDIO_FORMAT_F32.to_str())
.field("rate", &gst::IntRange::<i32>::new(1, i32::MAX))
.field("channels", &gst::IntRange::<i32>::new(1, i32::MAX))
.field("layout", &"interleaved")
.field("format", gst_audio::AUDIO_FORMAT_F32.to_str())
.field("rate", gst::IntRange::<i32>::new(1, i32::MAX))
.field("channels", gst::IntRange::<i32>::new(1, i32::MAX))
.field("layout", "interleaved")
.build(),
)
.build();

View file

@ -25,7 +25,16 @@ struct State {
// to the current_video_buffer below!
video_info: Option<gst_video::VideoInfo>,
audio_info: Option<gst_audio::AudioInfo>,
current_video_buffer: Option<(gst::Buffer, gst::ClockTime)>,
// These are only ever set when a change is pending mid-stream. They apply to the currently
// pending buffer on the pad and not to the current_video_buffer.
pending_caps: Option<gst::Caps>,
pending_segment: Option<gst::Segment>,
current_video_buffer: Option<(
gst::Buffer,
gst::ClockTime,
Option<gst::Caps>,
Option<gst::Segment>,
)>,
current_audio_buffers: Vec<(gst::Buffer, gst_audio::AudioInfo, i64)>,
}
@ -181,6 +190,8 @@ impl AggregatorImpl for NdiSinkCombiner {
*state_storage = Some(State {
audio_info: None,
video_info: None,
pending_caps: None,
pending_segment: None,
current_video_buffer: None,
current_audio_buffers: Vec::new(),
});
@ -373,75 +384,87 @@ impl AggregatorImpl for NdiSinkCombiner {
None => return Err(gst::FlowError::Flushing),
};
let (mut current_video_buffer, current_video_running_time_end, next_video_buffer) =
if let Some((video_buffer, video_segment)) = video_buffer_and_segment {
let video_running_time = video_segment.to_running_time(video_buffer.pts()).unwrap();
let (
mut current_video_buffer,
current_video_running_time_end,
pending_caps,
pending_segment,
next_video_buffer,
) = if let Some((video_buffer, video_segment)) = video_buffer_and_segment {
let video_running_time = video_segment.to_running_time(video_buffer.pts()).unwrap();
if let Some(pending_segment) = &state.pending_segment {
assert_eq!(video_segment.upcast_ref(), pending_segment);
}
match state.current_video_buffer {
None => {
gst::trace!(CAT, imp: self, "First video buffer, waiting for second");
state.current_video_buffer = Some((video_buffer, video_running_time));
drop(state_storage);
self.video_pad.drop_buffer();
return Err(gst_base::AGGREGATOR_FLOW_NEED_DATA);
}
Some((ref buffer, _)) => (
buffer.clone(),
Some(video_running_time),
Some((video_buffer, video_running_time)),
),
match &state.current_video_buffer {
None => {
gst::trace!(CAT, imp: self, "First video buffer, waiting for second");
state.current_video_buffer = Some((
video_buffer,
video_running_time,
state.pending_caps.take(),
state.pending_segment.take(),
));
drop(state_storage);
self.video_pad.drop_buffer();
return Err(gst_base::AGGREGATOR_FLOW_NEED_DATA);
}
} else {
match (&state.current_video_buffer, &audio_buffer_segment_and_pad) {
(None, None) => {
gst::trace!(
CAT,
imp: self,
"All pads are EOS and no buffers are queued, finishing"
);
return Err(gst::FlowError::Eos);
}
(None, Some((ref audio_buffer, ref audio_segment, _))) => {
// Create an empty dummy buffer for attaching the audio. This is going to
// be dropped by the sink later.
let audio_running_time =
audio_segment.to_running_time(audio_buffer.pts()).unwrap();
Some((ref buffer, _, pending_caps, pending_segment)) => (
buffer.clone(),
Some(video_running_time),
pending_caps.clone(),
pending_segment.clone(),
Some((video_buffer, video_running_time)),
),
}
} else {
match (&state.current_video_buffer, &audio_buffer_segment_and_pad) {
(None, None) => {
gst::trace!(
CAT,
imp: self,
"All pads are EOS and no buffers are queued, finishing"
);
return Err(gst::FlowError::Eos);
}
(None, Some((ref audio_buffer, ref audio_segment, _))) => {
// Create an empty dummy buffer for attaching the audio. This is going to
// be dropped by the sink later.
let audio_running_time =
audio_segment.to_running_time(audio_buffer.pts()).unwrap();
let video_segment = self.video_pad.segment();
let video_segment = match video_segment.downcast::<gst::ClockTime>() {
Ok(video_segment) => video_segment,
Err(video_segment) => {
gst::error!(
CAT,
imp: self,
"Video segment of wrong format {:?}",
video_segment.format()
);
return Err(gst::FlowError::Error);
}
};
let video_pts =
video_segment.position_from_running_time(audio_running_time);
if video_pts.is_none() {
gst::warning!(
let video_segment = self.video_pad.segment();
let video_segment = match video_segment.downcast::<gst::ClockTime>() {
Ok(video_segment) => video_segment,
Err(video_segment) => {
gst::error!(
CAT,
imp: self,
"Can't output more audio after video EOS"
"Video segment of wrong format {:?}",
video_segment.format()
);
return Err(gst::FlowError::Eos);
return Err(gst::FlowError::Error);
}
let mut buffer = gst::Buffer::new();
{
let buffer = buffer.get_mut().unwrap();
buffer.set_pts(video_pts);
}
(buffer, gst::ClockTime::NONE, None)
};
let video_pts = video_segment.position_from_running_time(audio_running_time);
if video_pts.is_none() {
gst::warning!(CAT, imp: self, "Can't output more audio after video EOS");
return Err(gst::FlowError::Eos);
}
(Some((ref buffer, _)), _) => (buffer.clone(), gst::ClockTime::NONE, None),
let mut buffer = gst::Buffer::new();
{
let buffer = buffer.get_mut().unwrap();
buffer.set_pts(video_pts);
}
(buffer, gst::ClockTime::NONE, None, None, None)
}
};
(Some((ref buffer, _, _, _)), _) => {
(buffer.clone(), gst::ClockTime::NONE, None, None, None)
}
}
};
if let Some((audio_buffer, audio_segment, audio_pad)) = audio_buffer_segment_and_pad {
let audio_info = match state.audio_info {
@ -508,7 +531,7 @@ impl AggregatorImpl for NdiSinkCombiner {
}
if let Some((video_buffer, video_running_time)) = next_video_buffer {
state.current_video_buffer = Some((video_buffer, video_running_time));
state.current_video_buffer = Some((video_buffer, video_running_time, None, None));
drop(state_storage);
self.video_pad.drop_buffer();
} else {
@ -522,7 +545,14 @@ impl AggregatorImpl for NdiSinkCombiner {
"Finishing video buffer {:?}",
current_video_buffer
);
self.obj().finish_buffer(current_video_buffer)
if let Some(caps) = pending_caps {
self.obj().set_src_caps(&caps);
}
if let Some(segment) = pending_segment {
self.obj().update_segment(&segment);
}
let ret = self.obj().finish_buffer(current_video_buffer);
ret
}
fn sink_event(&self, pad: &gst_base::AggregatorPad, event: gst::Event) -> bool {
@ -539,6 +569,7 @@ impl AggregatorImpl for NdiSinkCombiner {
};
if pad == &self.video_pad {
let mut send_caps_immediately = true;
let info = match gst_video::VideoInfo::from_caps(&caps) {
Ok(info) => info,
Err(_) => {
@ -559,13 +590,21 @@ impl AggregatorImpl for NdiSinkCombiner {
};
state.video_info = Some(info);
if state.current_video_buffer.is_some() {
state.pending_caps = Some(caps.clone());
send_caps_immediately = false;
} else {
state.pending_caps = None;
}
drop(state_storage);
self.obj().set_latency(latency, gst::ClockTime::NONE);
// The video caps are passed through as the audio is included only in a meta
self.obj().set_src_caps(&caps);
if send_caps_immediately {
self.obj().set_src_caps(&caps);
}
} else {
let info = match gst_audio::AudioInfo::from_caps(&caps) {
Ok(info) => info,
@ -582,7 +621,33 @@ impl AggregatorImpl for NdiSinkCombiner {
EventView::Segment(segment) if pad == &self.video_pad => {
let segment = segment.segment();
gst::debug!(CAT, obj: pad, "Updating segment {:?}", segment);
self.obj().update_segment(segment);
let mut state_storage = self.state.lock().unwrap();
let state = match &mut *state_storage {
Some(ref mut state) => state,
None => return false,
};
let mut send_segment_immediately = true;
if state.current_video_buffer.is_some() {
state.pending_segment = Some(segment.clone());
send_segment_immediately = false;
} else {
state.pending_caps = None;
}
drop(state_storage);
if send_segment_immediately {
self.obj().update_segment(segment);
}
}
EventView::FlushStop(_) if pad == &self.video_pad => {
let mut state_storage = self.state.lock().unwrap();
let state = match &mut *state_storage {
Some(ref mut state) => state,
None => return false,
};
state.pending_segment = None;
state.pending_caps = None;
}
_ => (),
}

View file

@ -109,6 +109,12 @@ impl ObjectSubclass for NdiSrc {
impl ObjectImpl for NdiSrc {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
let receiver = glib::ParamSpecString::builder("receiver-ndi-name")
.nick("Receiver NDI Name")
.blurb("NDI stream name of this receiver");
#[cfg(feature = "doc")]
let receiver = receiver.doc_show_default();
vec![
glib::ParamSpecString::builder("ndi-name")
.nick("NDI Name")
@ -118,11 +124,7 @@ impl ObjectImpl for NdiSrc {
.nick("URL/Address")
.blurb("URL/address and port of the sender, e.g. 127.0.0.1:5961")
.build(),
glib::ParamSpecString::builder("receiver-ndi-name")
.nick("Receiver NDI Name")
.blurb("NDI stream name of this receiver")
.doc_show_default()
.build(),
receiver.build(),
glib::ParamSpecUInt::builder("connect-timeout")
.nick("Connect Timeout")
.blurb("Connection timeout in ms")

View file

@ -915,14 +915,14 @@ impl Receiver {
real_time_now,
);
let res_timestamp = self.0.observations_timestamp[if is_audio { 0 } else { 1 }].process(
let res_timestamp = self.0.observations_timestamp[usize::from(!is_audio)].process(
element,
timestamp,
receive_time,
duration,
);
let res_timecode = self.0.observations_timecode[if is_audio { 0 } else { 1 }].process(
let res_timecode = self.0.observations_timecode[usize::from(!is_audio)].process(
element,
Some(timecode),
receive_time,
@ -1299,14 +1299,14 @@ impl Receiver {
gst::ReferenceTimestampMeta::add(
buffer,
&*crate::TIMECODE_CAPS,
&crate::TIMECODE_CAPS,
(video_frame.timecode() as u64 * 100).nseconds(),
gst::ClockTime::NONE,
);
if video_frame.timestamp() != ndisys::NDIlib_recv_timestamp_undefined {
gst::ReferenceTimestampMeta::add(
buffer,
&*crate::TIMESTAMP_CAPS,
&crate::TIMESTAMP_CAPS,
(video_frame.timestamp() as u64 * 100).nseconds(),
gst::ClockTime::NONE,
);
@ -1587,11 +1587,19 @@ impl Receiver {
let fourcc = audio_frame.fourcc();
if [NDIlib_FourCC_audio_type_FLTp].contains(&fourcc) {
let channels = audio_frame.no_channels() as u32;
let mut positions = [gst_audio::AudioChannelPosition::None; 64];
let _ = gst_audio::AudioChannelPosition::positions_from_mask(
gst_audio::AudioChannelPosition::fallback_mask(channels),
&mut positions[..channels as usize],
);
let builder = gst_audio::AudioInfo::builder(
gst_audio::AUDIO_FORMAT_F32,
audio_frame.sample_rate() as u32,
audio_frame.no_channels() as u32,
);
channels,
)
.positions(&positions[..channels as usize]);
let info = builder.build().map_err(|_| {
gst::element_error!(
@ -1670,14 +1678,14 @@ impl Receiver {
gst::ReferenceTimestampMeta::add(
buffer,
&*crate::TIMECODE_CAPS,
&crate::TIMECODE_CAPS,
(audio_frame.timecode() as u64 * 100).nseconds(),
gst::ClockTime::NONE,
);
if audio_frame.timestamp() != ndisys::NDIlib_recv_timestamp_undefined {
gst::ReferenceTimestampMeta::add(
buffer,
&*crate::TIMESTAMP_CAPS,
&crate::TIMESTAMP_CAPS,
(audio_frame.timestamp() as u64 * 100).nseconds(),
gst::ClockTime::NONE,
);

View file

@ -21,6 +21,8 @@ const LIBRARY_NAME: &str = "Processing.NDI.Lib.x86.dll";
const LIBRARY_NAME: &str = "libndi.so.5";
#[cfg(target_os = "macos")]
const LIBRARY_NAME: &str = "libndi.dylib";
#[cfg(all(unix, not(any(target_os = "linux", target_os = "macos"))))]
const LIBRARY_NAME: &str = "libndi.so";
#[allow(clippy::type_complexity)]
struct FFI {

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