mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-11-23 18:21:04 +00:00
gst-libs/gst/audio/: Do the delay calculation in the source/sink base classes as this is specific for the capture/pla...
Original commit message from CVS: * gst-libs/gst/audio/gstbaseaudiosink.c: (gst_base_audio_sink_get_time), (gst_base_audio_sink_callback): * gst-libs/gst/audio/gstbaseaudiosrc.c: (gst_base_audio_src_get_time), (gst_base_audio_src_fixate), (gst_base_audio_src_get_times), (gst_base_audio_src_get_offset), (gst_base_audio_src_create), (gst_base_audio_src_change_state): Do the delay calculation in the source/sink base classes as this is specific for the capture/playback mode. Try to fixate a bit better, like round depth up to a multiple of 8 bigger than width. Handle underruns correctly by marking DISCONT on buffers and adjusting timestamps to handle the gap. Set offset/offset_end correctly on buffers. * gst-libs/gst/audio/gstringbuffer.c: (gst_ring_buffer_pause), (gst_ring_buffer_samples_done), (gst_ring_buffer_commit), (gst_ring_buffer_read): Remove resync and underrun recovery from the ringbuffer. Fix ringbuffer read code on under/overrun.
This commit is contained in:
parent
102ec386b1
commit
65b1938b38
4 changed files with 163 additions and 65 deletions
22
ChangeLog
22
ChangeLog
|
@ -1,3 +1,25 @@
|
|||
2006-09-15 Wim Taymans <wim@fluendo.com>
|
||||
|
||||
* gst-libs/gst/audio/gstbaseaudiosink.c:
|
||||
(gst_base_audio_sink_get_time), (gst_base_audio_sink_callback):
|
||||
* gst-libs/gst/audio/gstbaseaudiosrc.c:
|
||||
(gst_base_audio_src_get_time), (gst_base_audio_src_fixate),
|
||||
(gst_base_audio_src_get_times), (gst_base_audio_src_get_offset),
|
||||
(gst_base_audio_src_create), (gst_base_audio_src_change_state):
|
||||
Do the delay calculation in the source/sink base classes as this is
|
||||
specific for the capture/playback mode.
|
||||
Try to fixate a bit better, like round depth up to a multiple of 8
|
||||
bigger than width.
|
||||
Handle underruns correctly by marking DISCONT on buffers and adjusting
|
||||
timestamps to handle the gap.
|
||||
Set offset/offset_end correctly on buffers.
|
||||
|
||||
* gst-libs/gst/audio/gstringbuffer.c: (gst_ring_buffer_pause),
|
||||
(gst_ring_buffer_samples_done), (gst_ring_buffer_commit),
|
||||
(gst_ring_buffer_read):
|
||||
Remove resync and underrun recovery from the ringbuffer.
|
||||
Fix ringbuffer read code on under/overrun.
|
||||
|
||||
2006-09-15 Wim Taymans <wim@fluendo.com>
|
||||
|
||||
* gst/playback/gstplaybasebin.c: (gst_play_base_bin_class_init),
|
||||
|
|
|
@ -91,7 +91,7 @@ static void gst_base_audio_sink_get_times (GstBaseSink * bsink,
|
|||
static gboolean gst_base_audio_sink_setcaps (GstBaseSink * bsink,
|
||||
GstCaps * caps);
|
||||
|
||||
//static guint gst_base_audio_sink_signals[LAST_SIGNAL] = { 0 };
|
||||
/* static guint gst_base_audio_sink_signals[LAST_SIGNAL] = { 0 }; */
|
||||
|
||||
static void
|
||||
gst_base_audio_sink_base_init (gpointer g_class)
|
||||
|
@ -217,18 +217,32 @@ clock_disabled:
|
|||
static GstClockTime
|
||||
gst_base_audio_sink_get_time (GstClock * clock, GstBaseAudioSink * sink)
|
||||
{
|
||||
guint64 samples;
|
||||
guint64 raw, samples;
|
||||
guint delay;
|
||||
GstClockTime result;
|
||||
|
||||
if (sink->ringbuffer == NULL || sink->ringbuffer->spec.rate == 0)
|
||||
return GST_CLOCK_TIME_NONE;
|
||||
|
||||
/* our processed samples are always increasing */
|
||||
samples = gst_ring_buffer_samples_done (sink->ringbuffer);
|
||||
raw = samples = gst_ring_buffer_samples_done (sink->ringbuffer);
|
||||
|
||||
/* the number of samples not yet processed, this is still queued in the
|
||||
* device (not played for playback). */
|
||||
delay = gst_ring_buffer_delay (sink->ringbuffer);
|
||||
|
||||
if (G_LIKELY (samples >= delay))
|
||||
samples -= delay;
|
||||
else
|
||||
samples = 0;
|
||||
|
||||
result = gst_util_uint64_scale_int (samples, GST_SECOND,
|
||||
sink->ringbuffer->spec.rate);
|
||||
|
||||
GST_DEBUG_OBJECT (sink,
|
||||
"processed samples: raw %llu, delay %u, real %llu, time %"
|
||||
GST_TIME_FORMAT, raw, delay, samples, GST_TIME_ARGS (result));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -707,7 +721,7 @@ static void
|
|||
gst_base_audio_sink_callback (GstRingBuffer * rbuf, guint8 * data, guint len,
|
||||
gpointer user_data)
|
||||
{
|
||||
//GstBaseAudioSink *sink = GST_BASE_AUDIO_SINK (data);
|
||||
/* GstBaseAudioSink *sink = GST_BASE_AUDIO_SINK (data); */
|
||||
}
|
||||
|
||||
/* should be called with the LOCK */
|
||||
|
|
|
@ -75,7 +75,7 @@ static void gst_base_audio_src_get_times (GstBaseSrc * bsrc,
|
|||
GstBuffer * buffer, GstClockTime * start, GstClockTime * end);
|
||||
static gboolean gst_base_audio_src_setcaps (GstBaseSrc * bsrc, GstCaps * caps);
|
||||
|
||||
//static guint gst_base_audio_src_signals[LAST_SIGNAL] = { 0 };
|
||||
/* static guint gst_base_audio_src_signals[LAST_SIGNAL] = { 0 }; */
|
||||
|
||||
static void
|
||||
gst_base_audio_src_base_init (gpointer g_class)
|
||||
|
@ -201,17 +201,28 @@ wrong_state:
|
|||
static GstClockTime
|
||||
gst_base_audio_src_get_time (GstClock * clock, GstBaseAudioSrc * src)
|
||||
{
|
||||
guint64 samples;
|
||||
guint64 raw, samples;
|
||||
guint delay;
|
||||
GstClockTime result;
|
||||
|
||||
if (G_UNLIKELY (src->ringbuffer == NULL || src->ringbuffer->spec.rate == 0))
|
||||
return GST_CLOCK_TIME_NONE;
|
||||
|
||||
samples = gst_ring_buffer_samples_done (src->ringbuffer);
|
||||
raw = samples = gst_ring_buffer_samples_done (src->ringbuffer);
|
||||
|
||||
/* the number of samples not yet processed, this is still queued in the
|
||||
* device (not yet read for capture). */
|
||||
delay = gst_ring_buffer_delay (src->ringbuffer);
|
||||
|
||||
samples += delay;
|
||||
|
||||
result = gst_util_uint64_scale_int (samples, GST_SECOND,
|
||||
src->ringbuffer->spec.rate);
|
||||
|
||||
GST_DEBUG_OBJECT (src,
|
||||
"processed samples: raw %llu, delay %u, real %llu, time %"
|
||||
GST_TIME_FORMAT, raw, delay, samples, GST_TIME_ARGS (result));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -271,14 +282,24 @@ static void
|
|||
gst_base_audio_src_fixate (GstPad * pad, GstCaps * caps)
|
||||
{
|
||||
GstStructure *s;
|
||||
gint width, depth;
|
||||
|
||||
s = gst_caps_get_structure (caps, 0);
|
||||
|
||||
/* fields for all formats */
|
||||
gst_structure_fixate_field_nearest_int (s, "rate", 44100);
|
||||
gst_structure_fixate_field_nearest_int (s, "channels", 2);
|
||||
gst_structure_fixate_field_nearest_int (s, "depth", 16);
|
||||
gst_structure_fixate_field_nearest_int (s, "width", 16);
|
||||
gst_structure_set (s, "signed", G_TYPE_BOOLEAN, TRUE, NULL);
|
||||
|
||||
/* fields for int */
|
||||
if (gst_structure_has_field (s, "depth")) {
|
||||
gst_structure_get_int (s, "width", &width);
|
||||
/* round width to nearest multiple of 8 for the depth */
|
||||
depth = GST_ROUND_UP_8 (width);
|
||||
gst_structure_fixate_field_nearest_int (s, "depth", depth);
|
||||
}
|
||||
if (gst_structure_has_field (s, "signed"))
|
||||
gst_structure_fixate_field_boolean (s, "signed", TRUE);
|
||||
if (gst_structure_has_field (s, "endianness"))
|
||||
gst_structure_fixate_field_nearest_int (s, "endianness", G_BYTE_ORDER);
|
||||
}
|
||||
|
@ -341,9 +362,8 @@ static void
|
|||
gst_base_audio_src_get_times (GstBaseSrc * bsrc, GstBuffer * buffer,
|
||||
GstClockTime * start, GstClockTime * end)
|
||||
{
|
||||
/* ne need to sync to a clock here, we schedule the samples based
|
||||
* on our own clock for the moment. FIXME, implement this when
|
||||
* we are not using our own clock */
|
||||
/* no need to sync to a clock here, we schedule the samples based
|
||||
* on our own clock for the moment. */
|
||||
*start = GST_CLOCK_TIME_NONE;
|
||||
*end = GST_CLOCK_TIME_NONE;
|
||||
}
|
||||
|
@ -369,6 +389,45 @@ gst_base_audio_src_event (GstBaseSrc * bsrc, GstEvent * event)
|
|||
return TRUE;
|
||||
}
|
||||
|
||||
/* get the next offset in the ringbuffer for reading samples.
|
||||
* If the next sample is too far away, this function will position itself to the
|
||||
* next most recent sample, creating discontinuity */
|
||||
static guint64
|
||||
gst_base_audio_src_get_offset (GstBaseAudioSrc * src)
|
||||
{
|
||||
guint64 sample;
|
||||
gint readseg, segdone, segtotal, sps;
|
||||
gint diff;
|
||||
|
||||
/* assume we can append to the previous sample */
|
||||
sample = src->next_sample;
|
||||
/* no previous sample, try to read from position 0 */
|
||||
if (sample == -1)
|
||||
sample = 0;
|
||||
|
||||
sps = src->ringbuffer->samples_per_seg;
|
||||
segtotal = src->ringbuffer->spec.segtotal;
|
||||
|
||||
/* figure out the segment and the offset inside the segment where
|
||||
* the sample should be read from. */
|
||||
readseg = sample / sps;
|
||||
|
||||
/* get the currently processed segment */
|
||||
segdone = g_atomic_int_get (&src->ringbuffer->segdone)
|
||||
- src->ringbuffer->segbase;
|
||||
|
||||
/* see how far away it is from the read segment, normally segdone (where new
|
||||
* data is written in the ringbuffer) is bigger than readseg (where we are
|
||||
* reading). */
|
||||
diff = segdone - readseg;
|
||||
if (diff >= segtotal) {
|
||||
/* sample would be dropped, position to next playable position */
|
||||
sample = (segdone - segtotal + 1) * sps;
|
||||
}
|
||||
|
||||
return sample;
|
||||
}
|
||||
|
||||
static GstFlowReturn
|
||||
gst_base_audio_src_create (GstBaseSrc * bsrc, guint64 offset, guint length,
|
||||
GstBuffer ** outbuf)
|
||||
|
@ -396,14 +455,17 @@ gst_base_audio_src_create (GstBaseSrc * bsrc, guint64 offset, guint length,
|
|||
/* make sure we round down to an integral number of samples */
|
||||
length -= length % bps;
|
||||
|
||||
/* calculate the sequentially next sample we need to read */
|
||||
sample = (src->next_sample != -1 ? src->next_sample : 0);
|
||||
|
||||
/* figure out the offset in the ringbuffer */
|
||||
if (G_UNLIKELY (offset != -1)) {
|
||||
/* if a specific offset was given it must be the next
|
||||
* sequential offset we expect or we fail. */
|
||||
if (offset / bps != sample)
|
||||
sample = offset / bps;
|
||||
/* if a specific offset was given it must be the next sequential
|
||||
* offset we expect or we fail for now. */
|
||||
if (src->next_sample != -1 && sample != src->next_sample)
|
||||
goto wrong_offset;
|
||||
} else {
|
||||
/* calculate the sequentially next sample we need to read. This can jump and
|
||||
* create a DISCONT. */
|
||||
sample = gst_base_audio_src_get_offset (src);
|
||||
}
|
||||
|
||||
/* get the number of samples to read */
|
||||
|
@ -413,10 +475,19 @@ gst_base_audio_src_create (GstBaseSrc * bsrc, guint64 offset, guint length,
|
|||
buf = gst_buffer_new_and_alloc (length);
|
||||
data = GST_BUFFER_DATA (buf);
|
||||
|
||||
/* read the sample */
|
||||
res = gst_ring_buffer_read (ringbuffer, sample, data, samples);
|
||||
if (G_UNLIKELY (res == -1))
|
||||
goto stopped;
|
||||
|
||||
/* mark discontinuity if needed */
|
||||
if (G_UNLIKELY (sample != src->next_sample) && src->next_sample != -1) {
|
||||
GST_WARNING_OBJECT (src,
|
||||
"create DISCONT of %" G_GUINT64_FORMAT " samples at sample %"
|
||||
G_GUINT64_FORMAT, sample - src->next_sample, sample);
|
||||
GST_BUFFER_FLAG_SET (buf, GST_BUFFER_FLAG_DISCONT);
|
||||
}
|
||||
|
||||
/* FIXME, we timestamp against our own clock, also handle the case
|
||||
* where we are slaved to another clock. We currently refuse to accept
|
||||
* any other clock than the one we provide, so this code is fine for
|
||||
|
@ -426,6 +497,8 @@ gst_base_audio_src_create (GstBaseSrc * bsrc, guint64 offset, guint length,
|
|||
src->next_sample = sample + samples;
|
||||
GST_BUFFER_DURATION (buf) = gst_util_uint64_scale_int (src->next_sample,
|
||||
GST_SECOND, ringbuffer->spec.rate) - GST_BUFFER_TIMESTAMP (buf);
|
||||
GST_BUFFER_OFFSET (buf) = sample;
|
||||
GST_BUFFER_OFFSET_END (buf) = sample + samples;
|
||||
|
||||
gst_buffer_set_caps (buf, GST_PAD_CAPS (GST_BASE_SRC_PAD (bsrc)));
|
||||
|
||||
|
@ -470,13 +543,6 @@ gst_base_audio_src_create_ringbuffer (GstBaseAudioSrc * src)
|
|||
return buffer;
|
||||
}
|
||||
|
||||
void
|
||||
gst_base_audio_src_callback (GstRingBuffer * rbuf, guint8 * data, guint len,
|
||||
gpointer user_data)
|
||||
{
|
||||
//GstBaseAudioSrc *src = GST_BASE_AUDIO_SRC (data);
|
||||
}
|
||||
|
||||
static GstStateChangeReturn
|
||||
gst_base_audio_src_change_state (GstElement * element,
|
||||
GstStateChange transition)
|
||||
|
@ -488,12 +554,10 @@ gst_base_audio_src_change_state (GstElement * element,
|
|||
case GST_STATE_CHANGE_NULL_TO_READY:
|
||||
if (src->ringbuffer == NULL) {
|
||||
src->ringbuffer = gst_base_audio_src_create_ringbuffer (src);
|
||||
gst_ring_buffer_set_callback (src->ringbuffer,
|
||||
gst_base_audio_src_callback, src);
|
||||
}
|
||||
if (!gst_ring_buffer_open_device (src->ringbuffer))
|
||||
return GST_STATE_CHANGE_FAILURE;
|
||||
src->next_sample = 0;
|
||||
src->next_sample = -1;
|
||||
break;
|
||||
case GST_STATE_CHANGE_READY_TO_PAUSED:
|
||||
gst_ring_buffer_set_flushing (src->ringbuffer, FALSE);
|
||||
|
@ -515,7 +579,7 @@ gst_base_audio_src_change_state (GstElement * element,
|
|||
case GST_STATE_CHANGE_PAUSED_TO_READY:
|
||||
gst_ring_buffer_set_flushing (src->ringbuffer, TRUE);
|
||||
gst_ring_buffer_release (src->ringbuffer);
|
||||
src->next_sample = 0;
|
||||
src->next_sample = -1;
|
||||
break;
|
||||
case GST_STATE_CHANGE_READY_TO_NULL:
|
||||
gst_ring_buffer_close_device (src->ringbuffer);
|
||||
|
|
|
@ -920,6 +920,7 @@ gst_ring_buffer_pause (GstRingBuffer * buf)
|
|||
|
||||
return res;
|
||||
|
||||
/* ERRORS */
|
||||
flushing:
|
||||
{
|
||||
GST_OBJECT_UNLOCK (buf);
|
||||
|
@ -989,6 +990,12 @@ done:
|
|||
* implementation uses another internal buffer between the audio
|
||||
* device.
|
||||
*
|
||||
* For playback ringbuffers this is the amount of samples transfered from the
|
||||
* ringbuffer to the device but still not played.
|
||||
*
|
||||
* For capture ringbuffers this is the amount of samples in the device that are
|
||||
* not yet transfered to the ringbuffer.
|
||||
*
|
||||
* Returns: The number of samples queued in the audio device.
|
||||
*
|
||||
* MT safe.
|
||||
|
@ -1020,7 +1027,8 @@ done:
|
|||
* @buf: the #GstRingBuffer to query
|
||||
*
|
||||
* Get the number of samples that were processed by the ringbuffer
|
||||
* since it was last started.
|
||||
* since it was last started. This does not include the number of samples not
|
||||
* yet processed (see gst_ring_buffer_delay()).
|
||||
*
|
||||
* Returns: The number of samples processed by the ringbuffer.
|
||||
*
|
||||
|
@ -1038,18 +1046,8 @@ gst_ring_buffer_samples_done (GstRingBuffer * buf)
|
|||
/* get the amount of segments we processed */
|
||||
segdone = g_atomic_int_get (&buf->segdone);
|
||||
|
||||
/* and the number of samples not yet processed */
|
||||
delay = gst_ring_buffer_delay (buf);
|
||||
|
||||
raw = samples = ((guint64) segdone) * buf->samples_per_seg;
|
||||
|
||||
if (G_LIKELY (samples >= delay))
|
||||
samples -= delay;
|
||||
else
|
||||
samples = 0;
|
||||
|
||||
GST_DEBUG_OBJECT (buf, "processed samples: raw %llu, delay %u, real %llu",
|
||||
raw, delay, samples);
|
||||
/* convert to samples */
|
||||
samples = ((guint64) segdone) * buf->samples_per_seg;
|
||||
|
||||
return samples;
|
||||
}
|
||||
|
@ -1237,9 +1235,10 @@ gst_ring_buffer_commit (GstRingBuffer * buf, guint64 sample, guchar * data,
|
|||
|
||||
GST_DEBUG
|
||||
("pointer at %d, sample %llu, write to %d-%d, to_write %d, diff %d, segtotal %d, segsize %d",
|
||||
segdone, sample, writeseg, sampleoff, to_write, diff, segtotal, sps);
|
||||
segdone, sample, writeseg, sampleoff, to_write, diff, segtotal,
|
||||
segsize);
|
||||
|
||||
/* segment too far ahead, we need to drop, hopefully UNLIKELY */
|
||||
/* segment too far ahead, writer too slow, we need to drop, hopefully UNLIKELY */
|
||||
if (G_UNLIKELY (diff < 0)) {
|
||||
/* we need to drop one segment at a time, pretend we wrote a
|
||||
* segment. */
|
||||
|
@ -1310,6 +1309,7 @@ gst_ring_buffer_read (GstRingBuffer * buf, guint64 sample, guchar * data,
|
|||
gint segdone;
|
||||
gint segsize, segtotal, bps, sps;
|
||||
guint8 *dest;
|
||||
guint to_read;
|
||||
|
||||
g_return_val_if_fail (GST_IS_RING_BUFFER (buf), -1);
|
||||
g_return_val_if_fail (buf->data != NULL, -1);
|
||||
|
@ -1321,13 +1321,14 @@ gst_ring_buffer_read (GstRingBuffer * buf, guint64 sample, guchar * data,
|
|||
bps = buf->spec.bytes_per_sample;
|
||||
sps = buf->samples_per_seg;
|
||||
|
||||
to_read = len;
|
||||
/* read enough samples */
|
||||
while (len > 0) {
|
||||
while (to_read > 0) {
|
||||
gint sampleslen;
|
||||
gint readseg, sampleoff;
|
||||
|
||||
/* figure out the segment and the offset inside the segment where
|
||||
* the sample should be written. */
|
||||
* the sample should be read from. */
|
||||
readseg = sample / sps;
|
||||
sampleoff = (sample % sps);
|
||||
|
||||
|
@ -1337,33 +1338,29 @@ gst_ring_buffer_read (GstRingBuffer * buf, guint64 sample, guchar * data,
|
|||
/* get the currently processed segment */
|
||||
segdone = g_atomic_int_get (&buf->segdone) - buf->segbase;
|
||||
|
||||
/* see how far away it is from the read segment */
|
||||
/* see how far away it is from the read segment, normally segdone (where
|
||||
* the hardware is writing) is bigger than readseg (where software is
|
||||
* reading) */
|
||||
diff = segdone - readseg;
|
||||
|
||||
GST_DEBUG
|
||||
("pointer at %d, sample %llu, read from %d-%d, len %d, diff %d, segtotal %d, segsize %d",
|
||||
segdone, sample, readseg, sampleoff, len, diff, segtotal, segsize);
|
||||
("pointer at %d, sample %llu, read from %d-%d, to_read %d, diff %d, segtotal %d, segsize %d",
|
||||
segdone, sample, readseg, sampleoff, to_read, diff, segtotal,
|
||||
segsize);
|
||||
|
||||
/* segment too far ahead, we need to drop */
|
||||
if (diff < 0) {
|
||||
/* we need to drop one segment at a time, pretend we read an
|
||||
* empty segment. */
|
||||
sampleslen = MIN (sps, len);
|
||||
/* segment too far ahead, reader too slow */
|
||||
if (G_UNLIKELY (diff >= segtotal)) {
|
||||
/* pretend we read an empty segment. */
|
||||
sampleslen = MIN (sps, to_read);
|
||||
memcpy (data, buf->empty_seg, sampleslen * bps);
|
||||
goto next;
|
||||
}
|
||||
|
||||
/* read segment is within readable range, we can break the loop and
|
||||
* start reading the data. */
|
||||
if (diff > 0 && diff < segtotal)
|
||||
if (diff > 0)
|
||||
break;
|
||||
|
||||
/* flush if diff has grown bigger than ringbuffer */
|
||||
if (diff >= segtotal) {
|
||||
gst_ring_buffer_clear_all (buf);
|
||||
buf->segdone = readseg;
|
||||
}
|
||||
|
||||
/* else we need to wait for the segment to become readable. */
|
||||
if (!wait_segment (buf))
|
||||
goto not_started;
|
||||
|
@ -1371,26 +1368,27 @@ gst_ring_buffer_read (GstRingBuffer * buf, guint64 sample, guchar * data,
|
|||
|
||||
/* we can read now */
|
||||
readseg = readseg % segtotal;
|
||||
sampleslen = MIN (sps - sampleoff, len);
|
||||
sampleslen = MIN (sps - sampleoff, to_read);
|
||||
|
||||
GST_DEBUG_OBJECT (buf, "read @%p seg %d, off %d, len %d",
|
||||
GST_DEBUG_OBJECT (buf, "read @%p seg %d, off %d, sampleslen %d",
|
||||
dest + readseg * segsize, readseg, sampleoff, sampleslen);
|
||||
|
||||
memcpy (data, dest + (readseg * segsize) + (sampleoff * bps),
|
||||
(sampleslen * bps));
|
||||
|
||||
next:
|
||||
len -= sampleslen;
|
||||
to_read -= sampleslen;
|
||||
sample += sampleslen;
|
||||
data += sampleslen * bps;
|
||||
}
|
||||
|
||||
return len;
|
||||
return len - to_read;
|
||||
|
||||
/* ERRORS */
|
||||
not_started:
|
||||
{
|
||||
GST_DEBUG_OBJECT (buf, "stopped processing");
|
||||
/* FIXME, return len - to_read after fixing caller */
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue