mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-09-09 05:38:58 +00:00
Original commit message from CVS: * ext/alsa/gstalsa.c: (gst_alsa_change_state), (gst_alsa_start), (gst_alsa_xrun_recovery): * ext/alsa/gstalsa.h: * ext/alsa/gstalsasink.c: (gst_alsa_sink_check_event), (gst_alsa_sink_loop), (gst_alsa_sink_get_time): * ext/alsa/gstalsasrc.c: (gst_alsa_src_init), (gst_alsa_src_get_time), (gst_alsa_src_update_avail), (gst_alsa_src_loop): Add clock to alsasrc. Take new capture timestamp when restarting after an overrun. Split up some functions between alsasrc ans alsasink.
553 lines
18 KiB
C
553 lines
18 KiB
C
/*
|
|
* Copyright (C) 2001 CodeFactory AB
|
|
* Copyright (C) 2001 Thomas Nyberg <thomas@codefactory.se>
|
|
* Copyright (C) 2001-2002 Andy Wingo <apwingo@eos.ncsu.edu>
|
|
* Copyright (C) 2003 Benjamin Otte <in7y118@public.uni-hamburg.de>
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Library General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2 of the License, or (at your option) any later version.
|
|
*
|
|
* This library is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* Library General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Library General Public
|
|
* License along with this library; if not, write to the Free
|
|
* Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
|
|
*/
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
#include "config.h"
|
|
#endif
|
|
|
|
#include "gstalsasink.h"
|
|
#include "gstalsaclock.h"
|
|
|
|
/* elementfactory information */
|
|
static GstElementDetails gst_alsa_sink_details =
|
|
GST_ELEMENT_DETAILS ("Alsa Sink",
|
|
"Sink/Audio",
|
|
"Output to a sound card via ALSA",
|
|
"Thomas Nyberg <thomas@codefactory.se>, "
|
|
"Andy Wingo <apwingo@eos.ncsu.edu>, "
|
|
"Benjamin Otte <in7y118@public.uni-hamburg.de>");
|
|
|
|
static GstPadTemplate *gst_alsa_sink_pad_factory (void);
|
|
static GstPadTemplate *gst_alsa_sink_request_pad_factory (void);
|
|
static void gst_alsa_sink_base_init (gpointer g_class);
|
|
static void gst_alsa_sink_class_init (gpointer g_klass, gpointer class_data);
|
|
static void gst_alsa_sink_init (GstAlsaSink * this);
|
|
static inline void gst_alsa_sink_flush_one_pad (GstAlsaSink * sink, gint i);
|
|
static void gst_alsa_sink_flush_pads (GstAlsaSink * sink);
|
|
static int gst_alsa_sink_mmap (GstAlsa * this, snd_pcm_sframes_t * avail);
|
|
static int gst_alsa_sink_write (GstAlsa * this, snd_pcm_sframes_t * avail);
|
|
static void gst_alsa_sink_loop (GstElement * element);
|
|
static gboolean gst_alsa_sink_check_event (GstAlsaSink * sink, gint pad_nr);
|
|
static GstElementStateReturn gst_alsa_sink_change_state (GstElement * element);
|
|
|
|
static GstClockTime gst_alsa_sink_get_time (GstAlsa * this);
|
|
|
|
static GstAlsa *sink_parent_class = NULL;
|
|
|
|
static GstPadTemplate *
|
|
gst_alsa_sink_pad_factory (void)
|
|
{
|
|
static GstPadTemplate *template = NULL;
|
|
|
|
if (!template)
|
|
template = gst_pad_template_new ("sink", GST_PAD_SINK, GST_PAD_ALWAYS,
|
|
gst_alsa_caps (SND_PCM_FORMAT_UNKNOWN, -1, -1));
|
|
|
|
return template;
|
|
}
|
|
static GstPadTemplate *
|
|
gst_alsa_sink_request_pad_factory (void)
|
|
{
|
|
static GstPadTemplate *template = NULL;
|
|
|
|
if (!template)
|
|
template =
|
|
gst_pad_template_new ("sink%d", GST_PAD_SINK, GST_PAD_REQUEST,
|
|
gst_alsa_caps (SND_PCM_FORMAT_UNKNOWN, -1, 1));
|
|
|
|
return template;
|
|
}
|
|
|
|
GType
|
|
gst_alsa_sink_get_type (void)
|
|
{
|
|
static GType alsa_sink_type = 0;
|
|
|
|
if (!alsa_sink_type) {
|
|
static const GTypeInfo alsa_sink_info = {
|
|
sizeof (GstAlsaSinkClass),
|
|
gst_alsa_sink_base_init,
|
|
NULL,
|
|
gst_alsa_sink_class_init,
|
|
NULL,
|
|
NULL,
|
|
sizeof (GstAlsaSink),
|
|
0,
|
|
(GInstanceInitFunc) gst_alsa_sink_init,
|
|
};
|
|
|
|
alsa_sink_type =
|
|
g_type_register_static (GST_TYPE_ALSA_MIXER, "GstAlsaSink",
|
|
&alsa_sink_info, 0);
|
|
}
|
|
return alsa_sink_type;
|
|
}
|
|
|
|
static void
|
|
gst_alsa_sink_base_init (gpointer g_class)
|
|
{
|
|
GstElementClass *element_class = GST_ELEMENT_CLASS (g_class);
|
|
|
|
gst_element_class_add_pad_template (element_class,
|
|
gst_alsa_sink_pad_factory ());
|
|
gst_element_class_add_pad_template (element_class,
|
|
gst_alsa_sink_request_pad_factory ());
|
|
|
|
gst_element_class_set_details (element_class, &gst_alsa_sink_details);
|
|
}
|
|
|
|
static void
|
|
gst_alsa_sink_class_init (gpointer g_class, gpointer class_data)
|
|
{
|
|
GObjectClass *object_class;
|
|
GstElementClass *element_class;
|
|
GstAlsaClass *alsa_class;
|
|
GstAlsaSinkClass *klass;
|
|
|
|
klass = (GstAlsaSinkClass *) g_class;
|
|
object_class = (GObjectClass *) klass;
|
|
element_class = (GstElementClass *) klass;
|
|
alsa_class = (GstAlsaClass *) klass;
|
|
|
|
if (sink_parent_class == NULL)
|
|
sink_parent_class = g_type_class_ref (GST_TYPE_ALSA_MIXER);
|
|
|
|
alsa_class->stream = SND_PCM_STREAM_PLAYBACK;
|
|
alsa_class->transmit_mmap = gst_alsa_sink_mmap;
|
|
alsa_class->transmit_rw = gst_alsa_sink_write;
|
|
|
|
element_class->change_state = gst_alsa_sink_change_state;
|
|
}
|
|
static void
|
|
gst_alsa_sink_init (GstAlsaSink * sink)
|
|
{
|
|
GstAlsa *this = GST_ALSA (sink);
|
|
|
|
this->pad[0] =
|
|
gst_pad_new_from_template (gst_alsa_sink_pad_factory (), "sink");
|
|
gst_pad_set_link_function (this->pad[0], gst_alsa_link);
|
|
gst_pad_set_getcaps_function (this->pad[0], gst_alsa_get_caps);
|
|
gst_pad_set_fixate_function (this->pad[0], gst_alsa_fixate);
|
|
gst_element_add_pad (GST_ELEMENT (this), this->pad[0]);
|
|
|
|
this->clock =
|
|
gst_alsa_clock_new ("alsasinkclock", gst_alsa_sink_get_time, this);
|
|
/* we hold a ref to our clock until we're disposed */
|
|
gst_object_ref (GST_OBJECT (this->clock));
|
|
gst_object_sink (GST_OBJECT (this->clock));
|
|
|
|
gst_element_set_loop_function (GST_ELEMENT (this), gst_alsa_sink_loop);
|
|
}
|
|
|
|
static inline void
|
|
gst_alsa_sink_flush_one_pad (GstAlsaSink * sink, gint i)
|
|
{
|
|
switch (sink->behaviour[i]) {
|
|
case 0:
|
|
if (sink->buf[i])
|
|
gst_data_unref (GST_DATA (sink->buf[i]));
|
|
sink->buf[i] = NULL;
|
|
sink->data[i] = NULL;
|
|
sink->behaviour[i] = 0;
|
|
sink->size[i] = 0;
|
|
break;
|
|
case 1:
|
|
g_free (sink->data[i]);
|
|
sink->data[i] = NULL;
|
|
sink->behaviour[i] = 0;
|
|
sink->size[i] = 0;
|
|
break;
|
|
default:
|
|
g_assert_not_reached ();
|
|
}
|
|
}
|
|
static void
|
|
gst_alsa_sink_flush_pads (GstAlsaSink * sink)
|
|
{
|
|
gint i;
|
|
|
|
for (i = 0; i < GST_ELEMENT (sink)->numpads; i++) {
|
|
/* flush twice to unref buffer when behaviour == 1 */
|
|
gst_alsa_sink_flush_one_pad (sink, i);
|
|
gst_alsa_sink_flush_one_pad (sink, i);
|
|
}
|
|
}
|
|
|
|
/* TRUE, if everything should continue */
|
|
static gboolean
|
|
gst_alsa_sink_check_event (GstAlsaSink * sink, gint pad_nr)
|
|
{
|
|
gboolean cont = TRUE;
|
|
GstEvent *event = GST_EVENT (sink->buf[pad_nr]);
|
|
GstAlsa *this = GST_ALSA (sink);
|
|
|
|
if (event) {
|
|
switch (GST_EVENT_TYPE (event)) {
|
|
case GST_EVENT_EOS:
|
|
gst_alsa_set_eos (this);
|
|
cont = FALSE;
|
|
break;
|
|
case GST_EVENT_INTERRUPT:
|
|
cont = FALSE;
|
|
break;
|
|
case GST_EVENT_DISCONTINUOUS:
|
|
{
|
|
GstClockTime value, delay;
|
|
|
|
/* only the first pad may seek */
|
|
if (pad_nr != 0) {
|
|
break;
|
|
}
|
|
delay = (this->format == NULL) ? 0 :
|
|
GST_SECOND * this->played / this->format->rate -
|
|
gst_alsa_sink_get_time (this);
|
|
if (gst_event_discont_get_value (event, GST_FORMAT_TIME, &value)) {
|
|
gst_element_set_time_delay (GST_ELEMENT (this), MIN (value, delay),
|
|
delay);
|
|
} else if (this->format
|
|
&& (gst_event_discont_get_value (event, GST_FORMAT_DEFAULT, &value)
|
|
|| gst_event_discont_get_value (event, GST_FORMAT_BYTES,
|
|
&value))) {
|
|
value = gst_alsa_samples_to_timestamp (this, value);
|
|
gst_element_set_time_delay (GST_ELEMENT (this), MIN (value, delay),
|
|
delay);
|
|
} else {
|
|
GST_WARNING_OBJECT (this,
|
|
"couldn't extract time from discont event. Bad things might happen!");
|
|
}
|
|
|
|
break;
|
|
}
|
|
default:
|
|
GST_INFO_OBJECT (this, "got an unknown event (Type: %d)",
|
|
GST_EVENT_TYPE (event));
|
|
break;
|
|
}
|
|
gst_event_unref (event);
|
|
sink->buf[pad_nr] = NULL;
|
|
} else {
|
|
/* the element at the top of the chain did not emit an event. */
|
|
g_assert_not_reached ();
|
|
}
|
|
return cont;
|
|
}
|
|
static int
|
|
gst_alsa_sink_mmap (GstAlsa * this, snd_pcm_sframes_t * avail)
|
|
{
|
|
snd_pcm_uframes_t offset;
|
|
const snd_pcm_channel_area_t *dst;
|
|
snd_pcm_channel_area_t *src;
|
|
GstAlsaSink *sink = GST_ALSA_SINK (this);
|
|
int i;
|
|
int err = -1;
|
|
int width = snd_pcm_format_physical_width (this->format->format);
|
|
|
|
/* areas points to the memory areas that belong to gstreamer. */
|
|
src = g_malloc0 (this->format->channels * sizeof (snd_pcm_channel_area_t));
|
|
|
|
if (((GstElement *) this)->numpads == 1) {
|
|
/* interleaved */
|
|
for (i = 0; i < this->format->channels; i++) {
|
|
src[i].addr = sink->data[0];
|
|
src[i].first = i * width;
|
|
src[i].step = this->format->channels * width;
|
|
}
|
|
} else {
|
|
/* noninterleaved */
|
|
for (i = 0; i < this->format->channels; i++) {
|
|
src[i].addr = sink->data[i];
|
|
src[i].first = 0;
|
|
src[i].step = width;
|
|
}
|
|
}
|
|
|
|
if ((err = snd_pcm_mmap_begin (this->handle, &dst, &offset, avail)) < 0) {
|
|
GST_ERROR_OBJECT (this, "mmap failed: %s", snd_strerror (err));
|
|
goto out;
|
|
}
|
|
|
|
if ((err =
|
|
snd_pcm_areas_copy (dst, offset, src, 0, this->format->channels,
|
|
*avail, this->format->format)) < 0) {
|
|
snd_pcm_mmap_commit (this->handle, offset, 0);
|
|
GST_ERROR_OBJECT (this, "data copy failed: %s", snd_strerror (err));
|
|
goto out;
|
|
}
|
|
if ((err = snd_pcm_mmap_commit (this->handle, offset, *avail)) < 0) {
|
|
GST_ERROR_OBJECT (this, "mmap commit failed: %s", snd_strerror (err));
|
|
goto out;
|
|
}
|
|
|
|
out:
|
|
g_free (src);
|
|
return err;
|
|
}
|
|
static int
|
|
gst_alsa_sink_write (GstAlsa * this, snd_pcm_sframes_t * avail)
|
|
{
|
|
GstAlsaSink *sink = GST_ALSA_SINK (this);
|
|
void *channels[this->format->channels];
|
|
int err, i;
|
|
|
|
if (((GstElement *) this)->numpads == 1) {
|
|
/* interleaved */
|
|
err = snd_pcm_writei (this->handle, sink->data[0], *avail);
|
|
} else {
|
|
/* noninterleaved */
|
|
for (i = 0; i < this->format->channels; i++) {
|
|
channels[i] = sink->data[i];
|
|
}
|
|
err = snd_pcm_writen (this->handle, channels, *avail);
|
|
}
|
|
/* error handling */
|
|
if (err < 0) {
|
|
if (err == -EPIPE) {
|
|
gst_alsa_xrun_recovery (this);
|
|
return 0;
|
|
}
|
|
GST_ERROR_OBJECT (this, "error on data access: %s", snd_strerror (err));
|
|
}
|
|
return err;
|
|
}
|
|
static void
|
|
gst_alsa_sink_loop (GstElement * element)
|
|
{
|
|
snd_pcm_sframes_t avail, avail2, copied, sample_diff, max_discont;
|
|
snd_pcm_uframes_t samplestamp, expected;
|
|
gint i;
|
|
guint bytes; /* per channel */
|
|
GstAlsa *this = GST_ALSA (element);
|
|
GstAlsaSink *sink = GST_ALSA_SINK (element);
|
|
|
|
g_return_if_fail (sink != NULL);
|
|
|
|
sink_restart:
|
|
|
|
avail = gst_alsa_update_avail (this);
|
|
if (avail == -EPIPE)
|
|
goto sink_restart;
|
|
if (avail < 0)
|
|
return;
|
|
if (avail > 0) {
|
|
|
|
/* Not enough space. We grab data nonetheless and sleep afterwards */
|
|
if (avail < this->period_size) {
|
|
avail = this->period_size;
|
|
}
|
|
|
|
/* check how many bytes we still have in all our bytestreams */
|
|
/* initialize this value to a somewhat sane state, we might alloc this much data below (which would be a bug, but who knows)... */
|
|
bytes = this->period_size * this->period_count * element->numpads * 8; /* must be > max sample size in bytes */
|
|
for (i = 0; i < element->numpads; i++) {
|
|
g_assert (this->pad[i] != NULL);
|
|
while (sink->size[i] == 0) {
|
|
if (!sink->buf[i])
|
|
sink->buf[i] = GST_BUFFER (gst_pad_pull (this->pad[i]));
|
|
if (GST_IS_EVENT (sink->buf[i])) {
|
|
if (gst_alsa_sink_check_event (sink, i))
|
|
continue;
|
|
return;
|
|
}
|
|
/* caps nego failed somewhere */
|
|
if (this->format == NULL) {
|
|
GST_ELEMENT_ERROR (this, CORE, NEGOTIATION, (NULL),
|
|
("ALSA format not negotiated"));
|
|
}
|
|
samplestamp =
|
|
gst_alsa_timestamp_to_samples (this,
|
|
GST_BUFFER_TIMESTAMP (sink->buf[i]));
|
|
max_discont = gst_alsa_timestamp_to_samples (this, this->max_discont);
|
|
/* optimization: check if we're using our own clock
|
|
* This optimization is important because if we're using our own clock
|
|
* gst_element_get_time calls snd_pcm_delay and the following code assumes
|
|
* that both calls return the same value. However they can be wildly
|
|
* different, since snd_pcm_delay goes deep into the kernel.
|
|
*/
|
|
if (gst_element_get_clock (element) == GST_CLOCK (this->clock)) {
|
|
/* FIXME: this is ugly because of the variables it uses but I don't know a
|
|
* better way to get this info */
|
|
if (element->base_time > this->clock->start_time) {
|
|
expected =
|
|
this->played - gst_alsa_timestamp_to_samples (this,
|
|
element->base_time - this->clock->start_time);
|
|
} else {
|
|
expected =
|
|
this->played + gst_alsa_timestamp_to_samples (this,
|
|
this->clock->start_time - element->base_time);
|
|
}
|
|
} else {
|
|
if (snd_pcm_delay (this->handle, &sample_diff) != 0) {
|
|
sample_diff = 0;
|
|
}
|
|
expected =
|
|
gst_alsa_timestamp_to_samples (this,
|
|
gst_element_get_time (GST_ELEMENT (this))) + sample_diff;
|
|
/* actual diff = buffer samplestamp - played - to_play */
|
|
}
|
|
sample_diff = samplestamp - expected;
|
|
|
|
if ((!GST_BUFFER_TIMESTAMP_IS_VALID (sink->buf[i])) ||
|
|
(-max_discont <= sample_diff && sample_diff <= max_discont)) {
|
|
|
|
/* difference between expected and current is < GST_ALSA_DEVIATION */
|
|
no_difference:
|
|
sink->size[i] = sink->buf[i]->size;
|
|
sink->data[i] = sink->buf[i]->data;
|
|
sink->behaviour[i] = 0;
|
|
} else if (sample_diff > 0) {
|
|
/* there are empty samples in front of us, fill them with silence */
|
|
int samples = MIN (bytes, sample_diff) *
|
|
(element->numpads == 1 ? this->format->channels : 1);
|
|
int size =
|
|
samples * snd_pcm_format_physical_width (this->format->format) /
|
|
8;
|
|
GST_INFO_OBJECT (this,
|
|
"Allocating %d bytes (%ld samples) now to resync: sample %lu expected, but got %ld",
|
|
size, MIN (bytes, sample_diff), expected, samplestamp);
|
|
sink->data[i] = g_try_malloc (size);
|
|
if (!sink->data[i]) {
|
|
GST_WARNING_OBJECT (this,
|
|
"error allocating %d bytes, buffers unsynced now.", size);
|
|
goto no_difference;
|
|
}
|
|
sink->size[i] = size;
|
|
if (0 != snd_pcm_format_set_silence (this->format->format,
|
|
sink->data[i], samples)) {
|
|
GST_WARNING_OBJECT (this,
|
|
"error silencing buffer, enjoy the noise.");
|
|
}
|
|
sink->behaviour[i] = 1;
|
|
} else if (gst_alsa_samples_to_bytes (this,
|
|
-sample_diff) >= sink->buf[i]->size) {
|
|
GST_INFO_OBJECT (this,
|
|
"Skipping %lu samples to resync (complete buffer): sample %lu expected, but got %ld",
|
|
gst_alsa_bytes_to_samples (this, sink->buf[i]->size), expected,
|
|
samplestamp);
|
|
/* this buffer is way behind */
|
|
gst_buffer_unref (sink->buf[i]);
|
|
sink->buf[i] = NULL;
|
|
continue;
|
|
} else if (sample_diff < 0) {
|
|
gint difference = gst_alsa_samples_to_bytes (this, -sample_diff);
|
|
|
|
GST_INFO_OBJECT (this,
|
|
"Skipping %lu samples to resync: sample %lu expected, but got %ld",
|
|
(gulong) - sample_diff, expected, samplestamp);
|
|
/* this buffer is only a bit behind */
|
|
sink->size[i] = sink->buf[i]->size - difference;
|
|
sink->data[i] = sink->buf[i]->data + difference;
|
|
sink->behaviour[i] = 0;
|
|
} else {
|
|
g_assert_not_reached ();
|
|
}
|
|
}
|
|
bytes = MIN (bytes, sink->size[i]);
|
|
}
|
|
|
|
avail = MIN (avail, gst_alsa_bytes_to_samples (this, bytes));
|
|
|
|
/* wait until the hw buffer has enough space */
|
|
while (gst_element_get_state (element) == GST_STATE_PLAYING
|
|
&& (avail2 = gst_alsa_update_avail (this)) < avail) {
|
|
if (avail2 <= -EPIPE)
|
|
goto sink_restart;
|
|
if (avail2 < 0)
|
|
return;
|
|
if (avail2 < avail
|
|
&& snd_pcm_state (this->handle) != SND_PCM_STATE_RUNNING)
|
|
if (!gst_alsa_start (this))
|
|
return;
|
|
if (gst_alsa_pcm_wait (this) == FALSE)
|
|
return;
|
|
}
|
|
|
|
/* FIXME: lotsa stuff can have happened while fetching data. Do we need to check something? */
|
|
|
|
/* put this data into alsa */
|
|
if ((copied = this->transmit (this, &avail)) < 0)
|
|
return;
|
|
/* update our clock */
|
|
this->played += copied;
|
|
/* flush the data */
|
|
bytes = gst_alsa_samples_to_bytes (this, copied);
|
|
for (i = 0; i < element->numpads; i++) {
|
|
if ((sink->size[i] -= bytes) == 0) {
|
|
gst_alsa_sink_flush_one_pad (sink, i);
|
|
continue;
|
|
}
|
|
g_assert (sink->size[i] > 0);
|
|
if (sink->behaviour[i] != 1)
|
|
sink->data[i] += bytes;
|
|
}
|
|
}
|
|
|
|
if (snd_pcm_state (this->handle) != SND_PCM_STATE_RUNNING
|
|
&& snd_pcm_avail_update (this->handle) == 0) {
|
|
gst_alsa_start (this);
|
|
}
|
|
|
|
}
|
|
|
|
static GstElementStateReturn
|
|
gst_alsa_sink_change_state (GstElement * element)
|
|
{
|
|
GstAlsaSink *sink;
|
|
|
|
g_return_val_if_fail (element != NULL, FALSE);
|
|
sink = GST_ALSA_SINK (element);
|
|
|
|
switch (GST_STATE_TRANSITION (element)) {
|
|
case GST_STATE_NULL_TO_READY:
|
|
case GST_STATE_READY_TO_PAUSED:
|
|
case GST_STATE_PAUSED_TO_PLAYING:
|
|
case GST_STATE_PLAYING_TO_PAUSED:
|
|
break;
|
|
case GST_STATE_PAUSED_TO_READY:
|
|
gst_alsa_sink_flush_pads (sink);
|
|
break;
|
|
case GST_STATE_READY_TO_NULL:
|
|
break;
|
|
default:
|
|
g_assert_not_reached ();
|
|
}
|
|
|
|
if (GST_ELEMENT_CLASS (sink_parent_class)->change_state)
|
|
return GST_ELEMENT_CLASS (sink_parent_class)->change_state (element);
|
|
|
|
return GST_STATE_SUCCESS;
|
|
}
|
|
|
|
static GstClockTime
|
|
gst_alsa_sink_get_time (GstAlsa * this)
|
|
{
|
|
snd_pcm_sframes_t delay;
|
|
|
|
if (!this->format)
|
|
return 0;
|
|
if (snd_pcm_delay (this->handle, &delay) != 0) {
|
|
return this->played / this->format->rate;
|
|
}
|
|
if (this->played <= delay) {
|
|
return 0;
|
|
}
|
|
|
|
return GST_SECOND * (this->played - delay) / this->format->rate;
|
|
}
|