@@ -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
4244namespace 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
349386private:
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