[vlc-commits] [Git][videolan/vlc][master] 10 commits: pulse: move drain in a new function
Jean-Baptiste Kempf (@jbk)
gitlab at videolan.org
Thu Nov 16 09:36:03 UTC 2023
Jean-Baptiste Kempf pushed to branch master at VideoLAN / VLC
Commits:
d7eccba7 by Thomas Guillem at 2023-11-16T09:10:49+00:00
pulse: move drain in a new function
No functional changes.
- - - - -
83967414 by Thomas Guillem at 2023-11-16T09:10:49+00:00
pulse: move data_free up
No functional changes. For the next commit
- - - - -
4d868b52 by Thomas Guillem at 2023-11-16T09:10:49+00:00
pulse: add stream_get_interpolated_latency()
This function interpolate the last timing. This will be used to rework
the draining and when switching to data callback.
An alternative is to use vlc_pa_get_latency() directly with the
PA_STREAM_INTERPOLATE_TIMING flag but timings can go in the past when
using this flag.
- - - - -
fb79d34d by Thomas Guillem at 2023-11-16T09:10:49+00:00
pulse: simplify draining
No need to trigger the drain callback from the timing callback anymore
since we can now use stream_get_interpolated_latency().
- - - - -
ee4a520c by Thomas Guillem at 2023-11-16T09:10:49+00:00
pulse: remove TriggerDrain()
Since it is now used only one time
- - - - -
25c17ac8 by Thomas Guillem at 2023-11-16T09:10:49+00:00
pulse: earlier return in stream_drain
This specific case will be gone on the next commit.
No functional changes.
- - - - -
d08924ed by Thomas Guillem at 2023-11-16T09:10:49+00:00
pulse: add VLC_TICK_0 to audio_ts timing
In the (very) unlikely case where rt is 0 (=> invalid for the VLC clock).
- - - - -
7e9281cb by Thomas Guillem at 2023-11-16T09:10:49+00:00
pulse: use pa_stream_set_write_callback()
Keep a fifo block in the module and send blocks to pulse via the
write_callback.
Pulse is now uncorked sooner (on the first play), silence is sent via
the data callback to reach for the start date.
This fixes overflow when sending more then 5 seconds of audio.
- - - - -
a720d19e by Thomas Guillem at 2023-11-16T09:10:49+00:00
pulse: remove overflow handling
It is now less likely to happen.
It's better to do nothing if it happens since the writing is already
paced via the callback and pa_stream_writable_size().
- - - - -
eeef5936 by Thomas Guillem at 2023-11-16T09:10:49+00:00
pulse: earlier return in stream_latency_cb()
- - - - -
1 changed file:
- modules/audio_output/pulse.c
Changes:
=====================================
modules/audio_output/pulse.c
=====================================
@@ -65,11 +65,20 @@ typedef struct
pa_stream *stream; /**< PulseAudio playback stream object */
pa_context *context; /**< PulseAudio connection context */
pa_threaded_mainloop *mainloop; /**< PulseAudio thread */
- pa_time_event *trigger; /**< Deferred stream trigger */
pa_time_event *drain_trigger; /**< Drain stream trigger */
bool draining;
pa_cvolume cvolume; /**< actual sink input volume */
- vlc_tick_t last_date; /**< Play system timestamp of last buffer */
+
+ bool start_date_reached;
+ vlc_tick_t start_date;
+ size_t total_silence_bytes;
+
+ struct {
+ size_t size;
+ block_t **last;
+ block_t *first;
+ } fifo;
+
pa_usec_t flush_rt;
pa_volume_t volume_force; /**< Forced volume (stream must be NULL) */
@@ -77,8 +86,26 @@ typedef struct
char *sink_force; /**< Forced sink name (stream must be NULL) */
struct sink *sinks; /**< Locally-cached list of sinks */
+
+ vlc_tick_t timing_system_ts;
} aout_sys_t;
+static vlc_tick_t stream_get_interpolated_latency(pa_stream *s,
+ audio_output_t *aout,
+ vlc_tick_t system_date)
+{
+ aout_sys_t *sys = aout->sys;
+
+ if (unlikely(sys->timing_system_ts == VLC_TICK_INVALID))
+ return 0;
+
+ vlc_tick_t latency = vlc_pa_get_latency(aout, sys->context, s);
+ if (unlikely(latency == VLC_TICK_INVALID))
+ return 0;
+
+ return latency + sys->timing_system_ts - system_date;
+}
+
static void VolumeReport(audio_output_t *aout)
{
aout_sys_t *sys = aout->sys;
@@ -97,7 +124,6 @@ static void drain_trigger_cb(pa_mainloop_api *api, pa_time_event *e,
vlc_pa_rttime_free(sys->mainloop, sys->drain_trigger);
sys->drain_trigger = NULL;
- sys->draining = false;
aout_DrainedReport(aout);
(void) api; (void) e; (void) tv;
@@ -109,21 +135,6 @@ static void stream_wait_cb(pa_stream *s, int success, void *userdata)
pa_threaded_mainloop_signal(userdata, 0);
}
-static int TriggerDrain(audio_output_t *aout)
-{
- aout_sys_t *sys = aout->sys;
- assert(sys->drain_trigger == NULL);
-
- vlc_tick_t delay = vlc_pa_get_latency(aout, sys->context, sys->stream);
- if (delay == VLC_TICK_INVALID)
- return VLC_EGENERIC;
-
- delay += pa_rtclock_now();
- sys->drain_trigger = pa_context_rttime_new(sys->context, delay,
- drain_trigger_cb, aout);
- return sys->drain_trigger ? VLC_SUCCESS : VLC_ENOMEM;
-}
-
/*** Sink ***/
static void sink_add_cb(pa_context *ctx, const pa_sink_info *i, int eol,
void *userdata)
@@ -214,14 +225,14 @@ static void stream_start_now(pa_stream *s, audio_output_t *aout)
{
pa_operation *op;
- assert ( ((aout_sys_t *)aout->sys)->trigger == NULL );
-
op = pa_stream_cork(s, 0, NULL, NULL);
if (op != NULL)
pa_operation_unref(op);
op = pa_stream_trigger(s, NULL, NULL);
if (likely(op != NULL))
pa_operation_unref(op);
+
+ (void) aout;
}
static void stream_stop(pa_stream *s, audio_output_t *aout)
@@ -229,11 +240,6 @@ static void stream_stop(pa_stream *s, audio_output_t *aout)
aout_sys_t *sys = aout->sys;
pa_operation *op;
- if (sys->trigger != NULL) {
- vlc_pa_rttime_free(sys->mainloop, sys->trigger);
- sys->trigger = NULL;
- }
-
op = pa_stream_cork(s, 1, stream_wait_cb, sys->mainloop);
if (op != NULL)
{
@@ -243,73 +249,11 @@ static void stream_stop(pa_stream *s, audio_output_t *aout)
}
}
-static void stream_trigger_cb(pa_mainloop_api *api, pa_time_event *e,
- const struct timeval *tv, void *userdata)
-{
- audio_output_t *aout = userdata;
- aout_sys_t *sys = aout->sys;
-
- assert (sys->trigger == e);
-
- msg_Dbg(aout, "starting deferred");
- vlc_pa_rttime_free(sys->mainloop, sys->trigger);
- sys->trigger = NULL;
- stream_start_now(sys->stream, aout);
- (void) api; (void) e; (void) tv;
-}
-
-/**
- * Starts or resumes the playback stream.
- * Tries start playing back audio samples at the most accurate time
- * in order to minimize desync and resampling during early playback.
- * @note PulseAudio lock required.
- */
-static void stream_start(pa_stream *s, audio_output_t *aout, vlc_tick_t date)
-{
- aout_sys_t *sys = aout->sys;
- vlc_tick_t delta;
-
- assert (sys->last_date != VLC_TICK_INVALID);
-
- if (sys->trigger != NULL) {
- vlc_pa_rttime_free(sys->mainloop, sys->trigger);
- sys->trigger = NULL;
- }
-
- delta = vlc_pa_get_latency(aout, sys->context, s);
- if (unlikely(delta == VLC_TICK_INVALID)) {
- msg_Dbg(aout, "cannot synchronize start");
- delta = 0; /* screwed */
- }
-
- delta = (date - vlc_tick_now()) - delta;
- if (delta > 0) {
- msg_Dbg(aout, "deferring start (%"PRId64" us)", delta);
- delta += pa_rtclock_now();
- sys->trigger = pa_context_rttime_new(sys->context, delta,
- stream_trigger_cb, aout);
- } else {
- msg_Warn(aout, "starting late (%"PRId64" us)", delta);
- stream_start_now(s, aout);
- }
-}
-
static void stream_latency_cb(pa_stream *s, void *userdata)
{
audio_output_t *aout = userdata;
aout_sys_t *sys = aout->sys;
- /* This callback is _never_ called while paused. */
- if (sys->last_date == VLC_TICK_INVALID)
- {
- if (sys->draining && sys->drain_trigger == NULL)
- TriggerDrain(aout);
- return; /* nothing to do if buffers are (still) empty */
- }
-
- if (pa_stream_is_corked(s) > 0)
- stream_start(s, aout, sys->last_date);
-
const pa_timing_info *ti = pa_stream_get_timing_info(s);
if (unlikely(ti == NULL) || !ti->playing)
return;
@@ -320,36 +264,43 @@ static void stream_latency_cb(pa_stream *s, void *userdata)
return;
}
- if (pa_stream_is_corked(s) == 0)
- {
- pa_usec_t rt;
- if (pa_stream_get_time(s, &rt) == 0 && rt > 0)
- {
- if (likely(rt >= sys->flush_rt))
- {
- /* Subtract the timestamp of the timing_info from the monotonic
- * time */
- pa_usec_t ti_age_us = pa_timeval_age(&ti->timestamp);
- vlc_tick_t system_ts = vlc_tick_now()
- - VLC_TICK_FROM_US(ti_age_us);
+ if (sys->draining)
+ return;
- rt -= sys->flush_rt;
+ if (pa_stream_is_corked(s) != 0)
+ return;
- aout_TimingReport(aout, system_ts, VLC_TICK_FROM_US(rt));
- }
+ pa_usec_t rt;
+ if (pa_stream_get_time(s, &rt) != 0 || rt == 0)
+ return;
+
+ /* Subtract the timestamp of the timing_info from the monotonic time */
+ pa_usec_t ti_age_us = pa_timeval_age(&ti->timestamp);
+ sys->timing_system_ts = vlc_tick_now()
+ - VLC_TICK_FROM_US(ti_age_us);
+
+ const pa_sample_spec *ss = pa_stream_get_sample_spec(s);
+ pa_usec_t silence_us =
+ pa_bytes_to_usec(sys->total_silence_bytes, ss);
+
+ if (sys->start_date_reached
+ && likely(rt >= sys->flush_rt + silence_us))
+ {
+ vlc_tick_t audio_ts = VLC_TICK_0 +
+ VLC_TICK_FROM_US(rt - sys->flush_rt - silence_us);
+
+ aout_TimingReport(aout, sys->timing_system_ts, audio_ts);
+ }
#ifndef NDEBUG
- else
- {
- /* The time returned by pa_stream_get_time() might be smaller
- * than flush_rt just after a flush (depending on
- * transport_usec, sink_usec), but the current read index
- * should always be superior or equal. */
- const pa_sample_spec *ss = pa_stream_get_sample_spec(s);
- assert(pa_bytes_to_usec(ti->read_index, ss) >= sys->flush_rt);
- }
-#endif
- }
+ else
+ {
+ /* The time returned by pa_stream_get_time() might be smaller than
+ * flush_rt just after a flush (depending on transport_usec,
+ * sink_usec), but the current read index should always be superior or
+ * equal. */
+ assert(pa_bytes_to_usec(ti->read_index, ss) >= sys->flush_rt);
}
+#endif
}
@@ -411,15 +362,168 @@ static void stream_moved_cb(pa_stream *s, void *userdata)
static void stream_overflow_cb(pa_stream *s, void *userdata)
{
audio_output_t *aout = userdata;
+
+ msg_Err(aout, "overflow");
+ (void) s;
+}
+
+static void stream_drain(pa_stream *s, audio_output_t *aout)
+{
aout_sys_t *sys = aout->sys;
- pa_operation *op;
+ assert(sys->draining);
- msg_Err(aout, "overflow, flushing");
- op = pa_stream_flush(s, NULL, NULL);
- if (unlikely(op == NULL))
+ if (sys->drain_trigger != NULL)
return;
- pa_operation_unref(op);
- sys->last_date = VLC_TICK_INVALID;
+
+ pa_operation *op = pa_stream_drain(s, NULL, NULL);
+ if (op != NULL)
+ pa_operation_unref(op);
+
+ sys->flush_rt = 0;
+
+ /* XXX: Loosy drain emulation.
+ * See #18141: drain callback is never received */
+ vlc_tick_t delay =
+ stream_get_interpolated_latency(s, aout, vlc_tick_now());
+
+ delay += pa_rtclock_now();
+ sys->drain_trigger = pa_context_rttime_new(sys->context, delay,
+ drain_trigger_cb, aout);
+}
+
+static void data_free(void *data)
+{
+ block_Release(data);
+}
+
+static void noop_free(void *data)
+{
+ (void) data;
+}
+
+static size_t stream_write(pa_stream *s, audio_output_t *aout, size_t nbytes)
+{
+ aout_sys_t *sys = aout->sys;
+
+ size_t written = 0;
+ while (nbytes > 0)
+ {
+ block_t *first = sys->fifo.first;
+ if (unlikely(first == NULL))
+ return written;
+
+ const void *data;
+ size_t tocopy;
+ pa_free_cb_t free_cb;
+
+ if (nbytes >= first->i_buffer)
+ {
+ tocopy = first->i_buffer;
+ data = first->p_buffer;
+ free_cb = data_free;
+
+ sys->fifo.first = sys->fifo.first->p_next;
+ if (sys->fifo.first == NULL)
+ sys->fifo.last = &sys->fifo.first;
+ }
+ else
+ {
+ tocopy = nbytes;
+ data = first->p_buffer;
+ /* The block is not fully processed, free it only when finished */
+ free_cb = noop_free;
+
+ first->p_buffer += tocopy;
+ first->i_buffer -= tocopy;
+ }
+
+ if (pa_stream_write_ext_free(s, data, tocopy,
+ free_cb, first, 0, PA_SEEK_RELATIVE) < 0) {
+ vlc_pa_error(aout, "cannot write", sys->context);
+ free_cb(first);
+ }
+
+ nbytes -= tocopy;
+ written += tocopy;
+ sys->fifo.size -= tocopy;
+ }
+
+ return written;
+}
+
+static size_t stream_silence(pa_stream *s, audio_output_t *aout, size_t len)
+{
+ aout_sys_t *sys = aout->sys;
+
+ void *ptr;
+ if (pa_stream_begin_write(s, &ptr, &len))
+ {
+ vlc_pa_error(aout, "cannot begin write", sys->context);
+ return 0;
+ }
+
+ memset(ptr, 0, len);
+
+ if (pa_stream_write(s, ptr, len, NULL, 0, PA_SEEK_RELATIVE) < 0)
+ {
+ vlc_pa_error(aout, "cannot write", sys->context);
+ return 0;
+ }
+
+ return len;
+}
+
+static void stream_write_cb(pa_stream *s, size_t nbytes, void *userdata)
+{
+ audio_output_t *aout = userdata;
+ aout_sys_t *sys = aout->sys;
+
+ /* Strangely, the write callback can be called while corked, and it messes
+ * up the timings if we write silence in that state. */
+ if (unlikely(pa_stream_is_corked(s) != 0))
+ return;
+
+ if (!sys->start_date_reached)
+ {
+ /* Write 0s until we reach the start_date */
+ size_t silence_bytes;
+
+ if (likely(sys->start_date != VLC_TICK_INVALID))
+ {
+ const pa_sample_spec *ss = pa_stream_get_sample_spec(s);
+
+ vlc_tick_t now = vlc_tick_now();
+ vlc_tick_t latency = stream_get_interpolated_latency(s, aout, now);
+ vlc_tick_t silence = sys->start_date - now - latency;
+ if (silence <= 0)
+ silence_bytes = 0;
+ else
+ {
+ silence_bytes = pa_usec_to_bytes(silence, ss);
+ if (silence_bytes > nbytes)
+ silence_bytes = nbytes;
+ }
+ }
+ else
+ silence_bytes = nbytes;
+
+ if (silence_bytes != 0)
+ {
+ silence_bytes = stream_silence(s, aout, silence_bytes);
+ nbytes -= silence_bytes;
+ sys->total_silence_bytes += silence_bytes;
+ }
+
+ if (nbytes == 0)
+ return;
+
+ sys->start_date_reached = true;
+ }
+
+ stream_write(s, aout, nbytes);
+
+ if (sys->fifo.first == NULL && sys->draining)
+ stream_drain(s, aout);
}
static void stream_started_cb(pa_stream *s, void *userdata)
@@ -527,11 +631,6 @@ static void context_cb(pa_context *ctx, pa_subscription_event_type_t type,
/*** VLC audio output callbacks ***/
-static void data_free(void *data)
-{
- block_Release(data);
-}
-
/**
* Queue one audio frame to the playback stream
*/
@@ -547,17 +646,28 @@ static void Play(audio_output_t *aout, block_t *block, vlc_tick_t date)
* will take place, and sooner or later a deadlock. */
pa_threaded_mainloop_lock(sys->mainloop);
- sys->last_date = date;
+ const pa_sample_spec *ss = pa_stream_get_sample_spec(s);
- if (pa_stream_is_corked(s) > 0)
+ if (!sys->start_date_reached)
{
- /* Trigger a latency update, that will update the stream_start timer
- * using the last date. */
- pa_operation *op = pa_stream_update_timing_info(s, NULL, NULL);
- if (op != NULL)
- pa_operation_unref(op);
+ vlc_tick_t now = vlc_tick_now();
+ sys->start_date = date
+ - pa_bytes_to_usec(sys->fifo.size, ss);
+
+ if (sys->start_date > now)
+ msg_Dbg(aout, "deferring start (%"PRId64" us)",
+ sys->start_date - now);
+ else
+ msg_Dbg(aout, "starting late (%"PRId64" us)",
+ sys->start_date - now);
+
+ if (pa_stream_is_corked(s) > 0)
+ stream_start_now(s, aout);
}
+ block_ChainLastAppend(&sys->fifo.last, block);
+ sys->fifo.size += block->i_buffer;
+
#if 0 /* Fault injector to test underrun recovery */
static volatile unsigned u = 0;
if ((++u % 1000) == 0) {
@@ -565,13 +675,6 @@ static void Play(audio_output_t *aout, block_t *block, vlc_tick_t date)
pa_operation_unref(pa_stream_flush(s, NULL, NULL));
}
#endif
-
- if (pa_stream_write_ext_free(s, block->p_buffer, block->i_buffer,
- data_free, block, 0, PA_SEEK_RELATIVE) < 0) {
- vlc_pa_error(aout, "cannot write", sys->context);
- block_Release(block);
- }
-
pa_threaded_mainloop_unlock(sys->mainloop);
}
@@ -590,8 +693,7 @@ static void Pause(audio_output_t *aout, bool paused, vlc_tick_t date)
stream_stop(s, aout);
} else {
pa_stream_set_latency_update_callback(s, stream_latency_cb, aout);
- if (likely(sys->last_date != VLC_TICK_INVALID))
- stream_start_now(s, aout);
+ stream_start_now(s, aout);
}
pa_threaded_mainloop_unlock(sys->mainloop);
@@ -612,16 +714,25 @@ static void Flush(audio_output_t *aout)
{
vlc_pa_rttime_free(sys->mainloop, sys->drain_trigger);
sys->drain_trigger = NULL;
- sys->draining = false;
}
+ sys->draining = false;
pa_operation *op = pa_stream_flush(s, NULL, NULL);
if (op != NULL)
pa_operation_unref(op);
- sys->last_date = VLC_TICK_INVALID;
stream_stop(s, aout);
+ block_ChainRelease(sys->fifo.first);
+ sys->fifo.size = 0;
+ sys->fifo.first = NULL;
+ sys->fifo.last = &sys->fifo.first;
+
+ sys->start_date_reached = false;
+ sys->start_date = VLC_TICK_INVALID;
+ sys->total_silence_bytes = 0;
+ sys->timing_system_ts = VLC_TICK_INVALID;
+
const pa_sample_spec *ss = pa_stream_get_sample_spec(s);
const pa_timing_info *ti = pa_stream_get_timing_info(s);
if (ti != NULL && !ti->read_index_corrupt)
@@ -637,46 +748,9 @@ static void Drain(audio_output_t *aout)
pa_threaded_mainloop_lock(sys->mainloop);
- if (unlikely(pa_stream_is_corked(s) > 0))
- {
- /* Drain while the stream is corked. It happens with very small input
- * when the stream is drained while the start is still being deferred.
- * In that case, we need start the stream before we actually drain it.
- * */
- if (sys->trigger != NULL)
- {
- vlc_pa_rttime_free(sys->mainloop, sys->trigger);
- sys->trigger = NULL;
- }
- stream_start_now(s, aout);
- }
-
- pa_operation *op = pa_stream_drain(s, NULL, NULL);
- if (op != NULL)
- pa_operation_unref(op);
-
- if (sys->last_date == VLC_TICK_INVALID)
- aout_DrainedReport(aout);
- else
- {
- sys->last_date = VLC_TICK_INVALID;
- sys->flush_rt = 0;
-
- /* XXX: Loosy drain emulation.
- * See #18141: drain callback is never received */
- sys->draining = true;
-
- /* Trigger a latency update, that will update the drain timer */
- op = pa_stream_update_timing_info(s, NULL, NULL);
- if (op != NULL)
- pa_operation_unref(op);
- else
- {
- /* Timing failed, update the drain timer using the last known
- * latency */
- TriggerDrain(aout);
- }
- }
+ sys->draining = true;
+ if (sys->fifo.first == NULL)
+ stream_drain(s, aout);
pa_threaded_mainloop_unlock(sys->mainloop);
}
@@ -903,12 +977,20 @@ static int Start(audio_output_t *aout, audio_sample_format_t *restrict fmt)
pa_cvolume_set(cvolume, ss.channels, sys->volume_force);
}
- sys->trigger = sys->drain_trigger = NULL;
+ sys->drain_trigger = NULL;
sys->draining = false;
pa_cvolume_init(&sys->cvolume);
- sys->last_date = VLC_TICK_INVALID;
sys->flush_rt = 0;
+ sys->start_date_reached = false;
+ sys->start_date = VLC_TICK_INVALID;
+ sys->total_silence_bytes = 0;
+ sys->timing_system_ts = VLC_TICK_INVALID;
+
+ sys->fifo.size = 0;
+ sys->fifo.first = NULL;
+ sys->fifo.last = &sys->fifo.first;
+
pa_format_info *formatv = pa_format_info_new();
formatv->encoding = encoding;
pa_format_info_set_rate(formatv, ss.rate);
@@ -1031,6 +1113,7 @@ static int Start(audio_output_t *aout, audio_sample_format_t *restrict fmt)
pa_stream_set_latency_update_callback(s, stream_latency_cb, aout);
pa_stream_set_moved_callback(s, stream_moved_cb, aout);
pa_stream_set_overflow_callback(s, stream_overflow_cb, aout);
+ pa_stream_set_write_callback(s, stream_write_cb, aout);
pa_stream_set_started_callback(s, stream_started_cb, aout);
pa_stream_set_suspended_callback(s, stream_suspended_cb, aout);
pa_stream_set_underflow_callback(s, stream_underflow_cb, aout);
@@ -1076,8 +1159,6 @@ static void Stop(audio_output_t *aout)
pa_stream *s = sys->stream;
pa_threaded_mainloop_lock(sys->mainloop);
- if (unlikely(sys->trigger != NULL))
- vlc_pa_rttime_free(sys->mainloop, sys->trigger);
if (sys->drain_trigger != NULL)
vlc_pa_rttime_free(sys->mainloop, sys->drain_trigger);
pa_stream_disconnect(s);
@@ -1089,6 +1170,7 @@ static void Stop(audio_output_t *aout)
pa_stream_set_latency_update_callback(s, NULL, NULL);
pa_stream_set_moved_callback(s, NULL, NULL);
pa_stream_set_overflow_callback(s, NULL, NULL);
+ pa_stream_set_write_callback(s, NULL, NULL);
pa_stream_set_started_callback(s, NULL, NULL);
pa_stream_set_suspended_callback(s, NULL, NULL);
pa_stream_set_underflow_callback(s, NULL, NULL);
View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/54897d1ebfd22ccae2faa78a13aa706ab693e731...eeef59369010c984c2905bf2227592155a46d0db
--
View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/54897d1ebfd22ccae2faa78a13aa706ab693e731...eeef59369010c984c2905bf2227592155a46d0db
You're receiving this email because of your account on code.videolan.org.
VideoLAN code repository instance
More information about the vlc-commits
mailing list