Skip to content

Commit 07a445e

Browse files
committed
gstreamer: classify appsinks from the pipeline string, follow the engine quantum
(cherry picked from commit cb5ca64)
1 parent bc44640 commit 07a445e

1 file changed

Lines changed: 148 additions & 26 deletions

File tree

src/plugins/score-plugin-gfx/Gfx/GStreamer/GStreamerDevice.cpp

Lines changed: 148 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,10 @@ extern "C" {
3535
#include <Gfx/GStreamer/GStreamerLoader.hpp>
3636
#include <Video/GStreamerCompatibility.hpp>
3737

38+
#include <ossia/detail/parse_strict.hpp>
39+
3840
#include <climits>
39-
#include <regex>
41+
#include <ctre.hpp>
4042
#include <thread>
4143

4244
namespace Gfx::GStreamer
@@ -72,6 +74,11 @@ struct gstreamer_pipeline
7274

7375
// Ring buffer per channel, written by GStreamer thread, read by audio engine
7476
static constexpr std::size_t ring_size = 65536;
77+
78+
// Max block the audio thread may resize the output storage to; the
79+
// parameter reserves this up front so the per-tick resize never reallocates
80+
// (a realloc would free a buffer the audio thread is reading through).
81+
static constexpr std::size_t max_block = 1 << 15;
7582
std::vector<std::vector<float>> ring; // [channel][ring_size]
7683
std::atomic<std::size_t> write_pos{0};
7784
std::atomic<std::size_t> read_pos{0};
@@ -100,11 +107,38 @@ struct gstreamer_pipeline
100107
write_pos.store(wp + num_samples, std::memory_order_release);
101108
}
102109

110+
// Points at the parameter's audio spans so read_into_output can re-point
111+
// them after a resize. A raw pointer (not a std::function) so that clearing
112+
// or using it during teardown can never throw on the audio thread.
113+
ossia::small_vector<std::span<float>, 8>* output_spans{};
114+
103115
// Called by audio engine (indirectly): copy from ring into output spans
104116
void read_into_output(int block_size)
105117
{
106118
if(!output_data)
107119
return;
120+
121+
// The engine tick size can differ from the configured buffer size
122+
// (e.g. PipeWire dynamic quantum); the storage follows it, but never
123+
// beyond the capacity reserved at construction (so no reallocation).
124+
if(block_size > (int)max_block)
125+
block_size = max_block;
126+
bool resized = false;
127+
for(auto& v : *output_data)
128+
{
129+
if(std::ssize(v) != block_size)
130+
{
131+
v.resize(block_size);
132+
resized = true;
133+
}
134+
}
135+
if(resized && output_spans && output_data)
136+
{
137+
const std::size_t n = std::min(output_spans->size(), output_data->size());
138+
for(std::size_t i = 0; i < n; i++)
139+
(*output_spans)[i] = (*output_data)[i];
140+
}
141+
108142
auto rp = read_pos.load(std::memory_order_relaxed);
109143
auto wp = write_pos.load(std::memory_order_acquire);
110144

@@ -237,12 +271,15 @@ struct gstreamer_pipeline
237271
}
238272
else
239273
{
240-
// Caps not yet negotiated; default to video
241-
info.is_video = true;
242-
info.width = 640;
243-
info.height = 480;
244-
info.pixfmt = AV_PIX_FMT_RGBA;
245-
video_count++;
274+
// Caps not yet negotiated (e.g. live sources: v4l2src, webrtcsrc,
275+
// audiotestsrc is-live=true don't preroll in PAUSED).
276+
// Fall back to classifying from the pipeline description: the
277+
// nearest audio/video token before this appsink wins.
278+
classify_from_pipeline_string(pipeline_string, sink_name, info);
279+
if(info.is_video)
280+
video_count++;
281+
else
282+
audio_count++;
246283
}
247284
gst.object_unref(pad);
248285
}
@@ -347,17 +384,16 @@ struct gstreamer_pipeline
347384
}
348385

349386
private:
387+
static constexpr auto appsink_name_rexp
388+
= ctll::fixed_string{R"(appsink\b[^!]*?\bname\s*=\s*([A-Za-z0-9_]+))"};
389+
static constexpr auto elem_name_rexp
390+
= ctll::fixed_string{R"(\bname\s*=\s*([A-Za-z0-9_]+))"};
391+
350392
static std::vector<std::string> find_appsink_names(const std::string& pipeline)
351393
{
352394
std::vector<std::string> names;
353-
// Match "appsink" optionally followed by properties including name=<identifier>
354-
std::regex re(R"(appsink\b[^!]*?\bname\s*=\s*(\w+))");
355-
auto begin = std::sregex_iterator(pipeline.begin(), pipeline.end(), re);
356-
auto end = std::sregex_iterator();
357-
for(auto it = begin; it != end; ++it)
358-
{
359-
names.push_back((*it)[1].str());
360-
}
395+
for(auto m : ctre::search_all<appsink_name_rexp>(pipeline))
396+
names.push_back(m.get<1>().to_string());
361397
return names;
362398
}
363399

@@ -366,14 +402,87 @@ struct gstreamer_pipeline
366402
static std::vector<std::string> find_all_named_elements(const std::string& pipeline)
367403
{
368404
std::vector<std::string> names;
369-
std::regex re(R"(\bname\s*=\s*(\w+))");
370-
auto begin = std::sregex_iterator(pipeline.begin(), pipeline.end(), re);
371-
auto end = std::sregex_iterator();
372-
for(auto it = begin; it != end; ++it)
405+
for(auto m : ctre::search_all<elem_name_rexp>(pipeline))
406+
names.push_back(m.get<1>().to_string());
407+
return names;
408+
}
409+
410+
static constexpr auto channels_rexp
411+
= ctll::fixed_string{R"(channels=(?:\(int\))?\s*([0-9]+))"};
412+
static constexpr auto rate_rexp
413+
= ctll::fixed_string{R"(rate=(?:\(int\))?\s*([0-9]+))"};
414+
static constexpr auto width_rexp
415+
= ctll::fixed_string{R"(width=(?:\(int\))?\s*([0-9]+))"};
416+
static constexpr auto height_rexp
417+
= ctll::fixed_string{R"(height=(?:\(int\))?\s*([0-9]+))"};
418+
static constexpr auto format_rexp
419+
= ctll::fixed_string{R"(format=(?:\(string\))?\s*([A-Za-z0-9]+))"};
420+
421+
template <ctll::fixed_string Rexp>
422+
static int last_int_of(std::string_view str, int fallback)
423+
{
424+
int ret = fallback;
425+
for(auto m : ctre::search_all<Rexp>(str))
426+
if(auto v = ossia::parse_strict<int>(m.template get<1>().to_view()))
427+
ret = *v;
428+
return ret;
429+
}
430+
431+
// Guess an appsink's media type from the pipeline string when caps are
432+
// not negotiated yet: look for the last audio/video-ish token occurring
433+
// before "appsink ... name=<sink_name>".
434+
static void classify_from_pipeline_string(
435+
const std::string& pipeline, const std::string& sink_name, AppsinkInfo& info)
436+
{
437+
std::size_t sink_pos = std::string::npos;
438+
for(auto m : ctre::search_all<appsink_name_rexp>(pipeline))
373439
{
374-
names.push_back((*it)[1].str());
440+
if(m.get<1>().to_view() == sink_name)
441+
{
442+
sink_pos = std::distance(pipeline.begin(), m.get<0>().begin());
443+
break;
444+
}
445+
}
446+
const std::string_view before
447+
= std::string_view{pipeline}.substr(0, sink_pos);
448+
449+
static constexpr std::string_view audio_tokens[]
450+
= {"audio/x-raw", "audioconvert", "audioresample", "audiotestsrc"};
451+
static constexpr std::string_view video_tokens[]
452+
= {"video/x-raw", "videoconvert", "videoscale", "videotestsrc", "videorate"};
453+
454+
std::size_t last_audio = std::string::npos, last_video = std::string::npos;
455+
for(auto tok : audio_tokens)
456+
if(auto p = before.rfind(tok); p != std::string::npos)
457+
last_audio = (last_audio == std::string::npos) ? p : std::max(last_audio, p);
458+
for(auto tok : video_tokens)
459+
if(auto p = before.rfind(tok); p != std::string::npos)
460+
last_video = (last_video == std::string::npos) ? p : std::max(last_video, p);
461+
462+
const bool is_audio = last_audio != std::string::npos
463+
&& (last_video == std::string::npos || last_audio > last_video);
464+
if(is_audio)
465+
{
466+
info.is_video = false;
467+
info.channels = last_int_of<channels_rexp>(before, 2);
468+
info.rate = last_int_of<rate_rexp>(before, 48000);
469+
}
470+
else
471+
{
472+
info.is_video = true;
473+
info.width = last_int_of<width_rexp>(before, 640);
474+
info.height = last_int_of<height_rexp>(before, 480);
475+
info.pixfmt = AV_PIX_FMT_RGBA;
476+
std::string format;
477+
for(auto m : ctre::search_all<format_rexp>(before))
478+
format = m.get<1>().to_string();
479+
if(!format.empty())
480+
{
481+
auto& map = ::Video::gstreamerToLibav();
482+
if(auto it = map.find(format); it != map.end())
483+
info.pixfmt = it->second;
484+
}
375485
}
376-
return names;
377486
}
378487

379488
static void parse_video_caps(
@@ -601,18 +710,29 @@ class gstreamer_audio_parameter final : public ossia::audio_parameter
601710
, m_audio_data(num_channels)
602711
, m_block_size{bs}
603712
{
604-
// Set up owned buffers and point audio spans to them permanently
713+
// Set up owned buffers and point audio spans to them permanently.
714+
// Reserve a generous capacity up front so the per-tick resize() in
715+
// read_into_output (block size follows the engine quantum) never
716+
// reallocates — a realloc here would free a buffer the audio thread may
717+
// still be reading through the spans, causing a double free.
605718
audio.resize(num_channels);
606719
for(int i = 0; i < num_channels; i++)
607720
{
721+
m_audio_data[i].reserve(gstreamer_pipeline::AudioBuffer::max_block);
608722
m_audio_data[i].resize(bs, 0.f);
609723
audio[i] = m_audio_data[i];
610724
}
611-
// Give the AudioBuffer a pointer so read_into_output can fill our buffers
725+
// Give the AudioBuffer pointers so read_into_output can fill our buffers
726+
// and re-point our spans after a resize.
612727
m_buffer.output_data = &m_audio_data;
728+
m_buffer.output_spans = &audio;
613729
}
614730

615-
virtual ~gstreamer_audio_parameter() { m_buffer.output_data = nullptr; }
731+
virtual ~gstreamer_audio_parameter()
732+
{
733+
m_buffer.output_data = nullptr;
734+
m_buffer.output_spans = nullptr;
735+
}
616736

617737
// clone_value() (not virtual) reads from audio spans.
618738
// We use push_value(audio_port) as a hook — it's called by the audio engine
@@ -633,8 +753,10 @@ class gstreamer_audio_parameter final : public ossia::audio_parameter
633753

634754
void refresh_from_ring()
635755
{
636-
m_buffer.read_into_output(m_block_size);
637-
// Re-point spans (in case vectors reallocated, though they shouldn't)
756+
// Follow whatever block size the engine last requested in pre_tick
757+
const int bs
758+
= m_audio_data.empty() ? m_block_size : (int)m_audio_data[0].size();
759+
m_buffer.read_into_output(bs);
638760
for(std::size_t i = 0; i < m_audio_data.size(); i++)
639761
audio[i] = m_audio_data[i];
640762
}

0 commit comments

Comments
 (0)