mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2025-03-28 11:55:39 +00:00
pwg: fix events and base classes
This commit is contained in:
parent
98b133fe54
commit
94b801aaa9
2 changed files with 160 additions and 143 deletions
|
@ -21,26 +21,26 @@
|
|||
instantly, possibly not in the same thread as the streaming thread that
|
||||
is processing the buffers, skipping ahead of buffers being processed
|
||||
or queued in the pipeline). The most common downstream events
|
||||
(NEWSEGMENT, EOS, TAG) are all serialised with the buffer flow.
|
||||
(SEGMENT, CAPS, TAG, EOS) are all serialised with the buffer flow.
|
||||
</para>
|
||||
<para>
|
||||
Here is a typical event function:
|
||||
</para>
|
||||
<programlisting>
|
||||
static gboolean
|
||||
gst_my_filter_sink_event (GstPad *pad, GstEvent * event)
|
||||
gst_my_filter_sink_event (GstPad *pad, GstObject * parent, GstEvent * event)
|
||||
{
|
||||
GstMyFilter *filter;
|
||||
gboolean ret;
|
||||
|
||||
filter = GST_MY_FILTER (gst_pad_get_parent (pad));
|
||||
filter = GST_MY_FILTER (parent);
|
||||
...
|
||||
|
||||
switch (GST_EVENT_TYPE (event)) {
|
||||
case GST_EVENT_NEWSEGMENT:
|
||||
case GST_EVENT_SEGMENT:
|
||||
/* maybe save and/or update the current segment (e.g. for output
|
||||
* clipping) or convert the event into one in a different format
|
||||
* (e.g. BYTES to TIME) or drop it and set a flag to send a newsegment
|
||||
* (e.g. BYTES to TIME) or drop it and set a flag to send a segment
|
||||
* event in a different format later */
|
||||
ret = gst_pad_push_event (filter->src_pad, event);
|
||||
break;
|
||||
|
@ -54,19 +54,18 @@ gst_my_filter_sink_event (GstPad *pad, GstEvent * event)
|
|||
ret = gst_pad_push_event (filter->src_pad, event);
|
||||
break;
|
||||
default:
|
||||
ret = gst_pad_event_default (pad, event);
|
||||
ret = gst_pad_event_default (pad, parent, event);
|
||||
break;
|
||||
}
|
||||
|
||||
...
|
||||
gst_object_unref (filter);
|
||||
return ret;
|
||||
}
|
||||
</programlisting>
|
||||
<para>
|
||||
If your element is chain-based, you will almost always have to implement
|
||||
a sink event function, since that is how you are notified about
|
||||
new segments and the end of the stream.
|
||||
segments, caps and the end of the stream.
|
||||
</para>
|
||||
<para>
|
||||
If your element is exclusively loop-based, you may or may not want a
|
||||
|
@ -92,18 +91,19 @@ gst_my_filter_sink_event (GstPad *pad, GstEvent * event)
|
|||
will then in turn generate an upstream seek event.
|
||||
</para>
|
||||
<para>
|
||||
The most common upstream events are seek events and Quality-of-Service
|
||||
(QoS) events.
|
||||
The most common upstream events are seek events, Quality-of-Service
|
||||
(QoS) and reconfigure events.
|
||||
</para>
|
||||
<para>
|
||||
An upstream event can be sent using the
|
||||
<function>gst_pad_send_event</function> function. This
|
||||
function simply call the default event handler of that pad. The default
|
||||
event handler of pads is <function>gst_pad_event_default</function>, and
|
||||
it basically sends the event to the peer pad. So upstream events always
|
||||
arrive on the src pad of your element and are handled by the default event
|
||||
handler except if you override that handler to handle it yourself. There
|
||||
are some specific cases where you have to do that :
|
||||
it basically sends the event to the peer of the internally linked pad.
|
||||
So upstream events always arrive on the src pad of your element and are
|
||||
handled by the default event handler except if you override that handler
|
||||
to handle it yourself. There are some specific cases where you have to
|
||||
do that :
|
||||
</para>
|
||||
<itemizedlist mark="opencircle">
|
||||
<listitem>
|
||||
|
@ -130,8 +130,9 @@ gst_my_filter_sink_event (GstPad *pad, GstEvent * event)
|
|||
<itemizedlist mark="opencircle">
|
||||
<listitem>
|
||||
<para>
|
||||
Always forward events you won't handle upstream using the default
|
||||
<function>gst_pad_event_default</function> method.
|
||||
Always handle events you won't handle using the default
|
||||
<function>gst_pad_event_default</function> method. This method will
|
||||
depending on the event, forward the event or drop it.
|
||||
</para>
|
||||
</listitem>
|
||||
<listitem>
|
||||
|
@ -152,10 +153,7 @@ gst_my_filter_sink_event (GstPad *pad, GstEvent * event)
|
|||
<para>
|
||||
Remember that the event handler might be called from a different
|
||||
thread than the streaming thread, so make sure you use
|
||||
appropriate locking everywhere and at the beginning of the function
|
||||
obtain a reference to your element via the
|
||||
<function>gst_pad_get_parent()</function> (and release it again at
|
||||
the end of the function with <function>gst_object_unref ()</function>.
|
||||
appropriate locking everywhere.
|
||||
</para>
|
||||
</listitem>
|
||||
</itemizedlist>
|
||||
|
@ -173,13 +171,18 @@ gst_my_filter_sink_event (GstPad *pad, GstEvent * event)
|
|||
In this chapter, we will discuss the following events:
|
||||
</para>
|
||||
<itemizedlist>
|
||||
<listitem><para><xref linkend="section-events-stream-start"/></para></listitem>
|
||||
<listitem><para><xref linkend="section-events-caps"/></para></listitem>
|
||||
<listitem><para><xref linkend="section-events-segment"/></para></listitem>
|
||||
<listitem><para><xref linkend="section-events-tag"/></para></listitem>
|
||||
<listitem><para><xref linkend="section-events-eos"/></para></listitem>
|
||||
<listitem><para><xref linkend="section-events-toc"/></para></listitem>
|
||||
<listitem><para><xref linkend="section-events-gap"/></para></listitem>
|
||||
<listitem><para><xref linkend="section-events-flush-start"/></para></listitem>
|
||||
<listitem><para><xref linkend="section-events-flush-stop"/></para></listitem>
|
||||
<listitem><para><xref linkend="section-events-newsegment"/></para></listitem>
|
||||
<listitem><para><xref linkend="section-events-qos"/></para></listitem>
|
||||
<listitem><para><xref linkend="section-events-seek"/></para></listitem>
|
||||
<listitem><para><xref linkend="section-events-nav"/></para></listitem>
|
||||
<listitem><para><xref linkend="section-events-tag"/></para></listitem>
|
||||
</itemizedlist>
|
||||
<para>
|
||||
For more comprehensive information about events and how they should be
|
||||
|
@ -187,6 +190,88 @@ gst_my_filter_sink_event (GstPad *pad, GstEvent * event)
|
|||
design documentation. This section only gives a general overview.
|
||||
</para>
|
||||
|
||||
<sect2 id="section-events-stream-start" xreflabel="Stream Start">
|
||||
<title>Stream Start</title>
|
||||
<para>
|
||||
WRITEME
|
||||
</para>
|
||||
</sect2>
|
||||
|
||||
<sect2 id="section-events-caps" xreflabel="Caps">
|
||||
<title>Caps</title>
|
||||
<para>
|
||||
WRITEME
|
||||
</para>
|
||||
</sect2>
|
||||
|
||||
<sect2 id="section-events-segment" xreflabel="Segment">
|
||||
<title>Segment</title>
|
||||
<para>
|
||||
A segment event is sent downstream to announce the range of valid
|
||||
timestamps in the stream and how they should be transformed into
|
||||
running-time and stream-time. A segment event must always be sent
|
||||
before the first buffer of data and after a flush (see above).
|
||||
</para>
|
||||
<para>
|
||||
The first segment event is created by the element driving the
|
||||
pipeline, like a source operating in push-mode or a demuxer/decoder
|
||||
operating pull-based. This segment event then travels down the
|
||||
pipeline and may be transformed on the way (a decoder, for example,
|
||||
might receive a segment event in BYTES format and might transform
|
||||
this into a segment event in TIMES format based on the average
|
||||
bitrate).
|
||||
</para>
|
||||
<para>
|
||||
Depending on the element type, the event can simply be forwarded using
|
||||
<function>gst_pad_event_default ()</function>, or it should be parsed
|
||||
and a modified event should be sent on. The last is true for demuxers,
|
||||
which generally have a byte-to-time conversion concept. Their input
|
||||
is usually byte-based, so the incoming event will have an offset in
|
||||
byte units (<symbol>GST_FORMAT_BYTES</symbol>), too. Elements
|
||||
downstream, however, expect segment events in time units, so that
|
||||
it can be used to synchronize against the pipeline clock. Therefore,
|
||||
demuxers and similar elements should not forward the event, but parse
|
||||
it, free it and send a segment event (in time units,
|
||||
<symbol>GST_FORMAT_TIME</symbol>) further downstream.
|
||||
</para>
|
||||
<para>
|
||||
The segment event is created using the function
|
||||
<function>gst_event_new_segment ()</function>. See the API
|
||||
reference and design document for details about its parameters.
|
||||
</para>
|
||||
<para>
|
||||
Elements parsing this event can use gst_event_parse_segment()
|
||||
to extract the event details. Elements may find the GstSegment
|
||||
API useful to keep track of the current segment (if they want to use
|
||||
it for output clipping, for example).
|
||||
</para>
|
||||
</sect2>
|
||||
|
||||
<sect2 id="section-events-tag" xreflabel="Tag (metadata)">
|
||||
<title>Tag (metadata)</title>
|
||||
<para>
|
||||
Tagging events are being sent downstream to indicate the tags as parsed
|
||||
from the stream data. This is currently used to preserve tags during
|
||||
stream transcoding from one format to the other. Tags are discussed
|
||||
extensively in <xref linkend="chapter-advanced-tagging"/>. Most
|
||||
elements will simply forward the event by calling
|
||||
<function>gst_pad_event_default ()</function>.
|
||||
</para>
|
||||
<para>
|
||||
The tag event is created using the function
|
||||
<function>gst_event_new_tag ()</function>, but more often elements will
|
||||
send a tag event downstream that will be converted into a message
|
||||
on the bus by sink elements.
|
||||
All of these functions require a filled-in taglist as
|
||||
argument, which they will take ownership of.
|
||||
</para>
|
||||
<para>
|
||||
Elements parsing this event can use the function
|
||||
<function>gst_event_parse_tag ()</function> to acquire the
|
||||
taglist that the event contains.
|
||||
</para>
|
||||
</sect2>
|
||||
|
||||
<sect2 id="section-events-eos" xreflabel="End of Stream (EOS)">
|
||||
<title>End of Stream (EOS)</title>
|
||||
<para>
|
||||
|
@ -217,16 +302,31 @@ gst_my_filter_sink_event (GstPad *pad, GstEvent * event)
|
|||
on the bus depending on the mode of operation). If you are implementing
|
||||
your own source element, you also do not need to ever manually send
|
||||
an EOS event, you should also just return GST_FLOW_EOS in
|
||||
your create function (assuming your element derives from GstBaseSrc
|
||||
or GstPushSrc).
|
||||
your create or fill function (assuming your element derives from
|
||||
GstBaseSrc or GstPushSrc).
|
||||
</para>
|
||||
</sect2>
|
||||
|
||||
<sect2 id="section-events-toc" xreflabel="Table Of Contents">
|
||||
<title>Table Of Contents</title>
|
||||
<para>
|
||||
WRITEME
|
||||
</para>
|
||||
</sect2>
|
||||
|
||||
<sect2 id="section-events-gap" xreflabel="Gap">
|
||||
<title>Gap</title>
|
||||
<para>
|
||||
WRITEME
|
||||
</para>
|
||||
</sect2>
|
||||
|
||||
<sect2 id="section-events-flush-start" xreflabel="Flush Start">
|
||||
<title>Flush Start</title>
|
||||
<para>
|
||||
The flush start event is sent downstream if all buffers and caches
|
||||
in the pipeline should be emptied. <quote>Queue</quote> elements will
|
||||
The flush start event is sent downstream (in push mode) or upstream
|
||||
(in pull mode) if all buffers and caches in the pipeline should be
|
||||
emptied. <quote>Queue</quote> elements will
|
||||
empty their internal list of buffers when they receive this event, for
|
||||
example. File sink elements (e.g. <quote>filesink</quote>) will flush
|
||||
the kernel-to-disk cache (<function>fdatasync ()</function> or
|
||||
|
@ -259,7 +359,7 @@ gst_my_filter_sink_event (GstPad *pad, GstEvent * event)
|
|||
The flush-stop event is sent by an element driving the pipeline
|
||||
after a flush-start and tells pads and elements downstream that
|
||||
they should accept events and buffers again (there will be at
|
||||
least a NEWSEGMENT event before any buffers first though).
|
||||
least a SEGMENT event before any buffers first though).
|
||||
</para>
|
||||
<para>
|
||||
If your element keeps temporary caches of stream data, it should
|
||||
|
@ -268,58 +368,17 @@ gst_my_filter_sink_event (GstPad *pad, GstEvent * event)
|
|||
</para>
|
||||
<para>
|
||||
The flush-stop event is created with
|
||||
<function>gst_event_new_flush_stop ()</function>. Like the EOS event,
|
||||
it has no properties.
|
||||
<function>gst_event_new_flush_stop ()</function>. It has one
|
||||
parameter that controls if the running-time of the pipeline should
|
||||
be reset to 0 or not. Normally aftera flushing seek, the
|
||||
running_time is set back to 0.
|
||||
</para>
|
||||
</sect2>
|
||||
|
||||
<sect2 id="section-events-newsegment" xreflabel="New Segment">
|
||||
<title>New Segment</title>
|
||||
<sect2 id="section-events-qos" xreflabel="Quality Of Service (QOS)">
|
||||
<title>Quality Of Service (QOS)</title>
|
||||
<para>
|
||||
A new segment event is sent downstream to either announce a new
|
||||
segment of data in the data stream or to update the current segment
|
||||
with new values. A new segment event must always be sent before the
|
||||
first buffer of data and after a flush (see above).
|
||||
</para>
|
||||
<para>
|
||||
The first new segment event is created by the element driving the
|
||||
pipeline, like a source operating in push-mode or a demuxer/decoder
|
||||
operating pull-based. This new segment event then travels down the
|
||||
pipeline and may be transformed on the way (a decoder, for example,
|
||||
might receive a new-segment event in BYTES format and might transform
|
||||
this into a new-segment event in TIMES format based on the average
|
||||
bitrate).
|
||||
</para>
|
||||
<para>
|
||||
New segment events may also be used to indicate 'gaps' in the stream,
|
||||
like in a subtitle stream for example where there may not be any
|
||||
data at all for a considerable amount of (stream) time. This is done
|
||||
by updating the segment start of the current segment (see the design
|
||||
documentation for more details).
|
||||
</para>
|
||||
<para>
|
||||
Depending on the element type, the event can simply be forwarded using
|
||||
<function>gst_pad_event_default ()</function>, or it should be parsed
|
||||
and a modified event should be sent on. The last is true for demuxers,
|
||||
which generally have a byte-to-time conversion concept. Their input
|
||||
is usually byte-based, so the incoming event will have an offset in
|
||||
byte units (<symbol>GST_FORMAT_BYTES</symbol>), too. Elements
|
||||
downstream, however, expect new segment events in time units, so that
|
||||
it can be used to update the pipeline clock. Therefore, demuxers and
|
||||
similar elements should not forward the event, but parse it, free it
|
||||
and send a new newsegment event (in time units,
|
||||
<symbol>GST_FORMAT_TIME</symbol>) further downstream.
|
||||
</para>
|
||||
<para>
|
||||
The newsegment event is created using the function
|
||||
<function>gst_event_new_new_segment ()</function>. See the API
|
||||
reference and design document for details about its parameters.
|
||||
</para>
|
||||
<para>
|
||||
Elements parsing this event can use gst_event_parse_new_segment_full()
|
||||
to extract the event details. Elements may find the GstSegment
|
||||
API useful to keep track of the current segment (if they want to use
|
||||
it for output clipping, for example).
|
||||
WRITEME
|
||||
</para>
|
||||
</sect2>
|
||||
|
||||
|
@ -330,9 +389,9 @@ gst_my_filter_sink_event (GstPad *pad, GstEvent * event)
|
|||
This new position can be set in several formats (time, bytes or
|
||||
<quote>default units</quote> [a term indicating frames for video,
|
||||
channel-independent samples for audio, etc.]). Seeking can be done with
|
||||
respect to the end-of-file, start-of-file or current position, and
|
||||
respect to the end-of-file or start-of-file, and
|
||||
usually happens in upstream direction (downstream seeking is done by
|
||||
sending a NEWSEGMENT event with the appropriate offsets for elements
|
||||
sending a SEGMENT event with the appropriate offsets for elements
|
||||
that support that, like filesink).
|
||||
</para>
|
||||
<para>
|
||||
|
@ -347,10 +406,10 @@ gst_my_filter_sink_event (GstPad *pad, GstEvent * event)
|
|||
Seek events are built up using positions in specified formats (time,
|
||||
bytes, units). They are created using the function
|
||||
<function>gst_event_new_seek ()</function>. Note that many plugins do
|
||||
not support seeking from the end of the stream or from the current
|
||||
position. An element not driving the pipeline and forwarding a seek
|
||||
not support seeking from the end of the stream.
|
||||
An element not driving the pipeline and forwarding a seek
|
||||
request should not assume that the seek succeeded or actually happened,
|
||||
it should operate based on the NEWSEGMENT events it receives.
|
||||
it should operate based on the SEGMENT events it receives.
|
||||
</para>
|
||||
<para>
|
||||
Elements parsing this event can do this using
|
||||
|
@ -375,30 +434,5 @@ gst_my_filter_sink_event (GstPad *pad, GstEvent * event)
|
|||
</para>
|
||||
</sect2>
|
||||
|
||||
<sect2 id="section-events-tag" xreflabel="Tag (metadata)">
|
||||
<title>Tag (metadata)</title>
|
||||
<para>
|
||||
Tagging events are being sent downstream to indicate the tags as parsed
|
||||
from the stream data. This is currently used to preserve tags during
|
||||
stream transcoding from one format to the other. Tags are discussed
|
||||
extensively in <xref linkend="chapter-advanced-tagging"/>. Most
|
||||
elements will simply forward the event by calling
|
||||
<function>gst_pad_event_default ()</function>.
|
||||
</para>
|
||||
<para>
|
||||
The tag event is created using the function
|
||||
<function>gst_event_new_tag ()</function>, but more often elements will
|
||||
use either the <function>gst_element_found_tags ()</function> function
|
||||
or the <function>gst_element_found_tags_for_pad ()</function>, which
|
||||
will do both: post a tag message on the bus and send a tag event
|
||||
downstream. All of these functions require a filled-in taglist as
|
||||
argument, which they will take ownership of.
|
||||
</para>
|
||||
<para>
|
||||
Elements parsing this event can use the function
|
||||
<function>gst_event_parse_tag ()</function> to acquire the
|
||||
taglist that the event contains.
|
||||
</para>
|
||||
</sect2>
|
||||
</sect1>
|
||||
</chapter>
|
||||
|
|
|
@ -34,6 +34,10 @@
|
|||
needs to implement a bunch of virtual functions and will work
|
||||
automatically.
|
||||
</para>
|
||||
<para>
|
||||
The base class implement much of the synchronization logic that a
|
||||
sink has to perform.
|
||||
</para>
|
||||
<para>
|
||||
The <classname>GstBaseSink</classname> base-class specifies some
|
||||
limitations on elements, though:
|
||||
|
@ -42,26 +46,8 @@
|
|||
<listitem>
|
||||
<para>
|
||||
It requires that the sink only has one sinkpad. Sink elements that
|
||||
need more than one sinkpad, cannot use this base-class.
|
||||
</para>
|
||||
</listitem>
|
||||
<listitem>
|
||||
<para>
|
||||
The base-class owns the pad, and specifies caps negotiation, data
|
||||
handling, pad allocation and such functions. If you need more than
|
||||
the ones provided as virtual functions, then you cannot use this
|
||||
base-class.
|
||||
</para>
|
||||
</listitem>
|
||||
<listitem>
|
||||
<para>
|
||||
By implementing the <function>pad_allocate ()</function> function,
|
||||
it is possible for upstream elements to use special memory, such
|
||||
as memory on the X server side that only the sink can allocate, or
|
||||
even hardware memory <function>mmap ()</function>'ed from the kernel.
|
||||
Note that in almost all cases, you will want to subclass the
|
||||
<classname>GstBuffer</classname> object, so that your own set of
|
||||
functions will be called when the buffer loses its last reference.
|
||||
need more than one sinkpad, must make a manager element with
|
||||
multiple GstBaseSink elements inside.
|
||||
</para>
|
||||
</listitem>
|
||||
</itemizedlist>
|
||||
|
@ -109,13 +95,19 @@ gst_my_sink_class_init (GstMySinkClass * klass)
|
|||
<title>Writing an audio sink</title>
|
||||
<para>
|
||||
Essentially, audio sink implementations are just a special case of a
|
||||
general sink. There are two audio base classes that you can choose to
|
||||
general sink. An audio sink has the added complexity that it needs to
|
||||
schedule playback of samples. It must match the clock selected in the
|
||||
pipeline against the clock of the audio device and calculate and
|
||||
compensate for drift and jitter.
|
||||
</para>
|
||||
<para>
|
||||
There are two audio base classes that you can choose to
|
||||
derive from, depending on your needs:
|
||||
<classname>GstBaseAudiosink</classname> and
|
||||
<classname>GstAudioSink</classname>. The baseaudiosink provides full
|
||||
<classname>GstAudioBasesink</classname> and
|
||||
<classname>GstAudioSink</classname>. The audiobasesink provides full
|
||||
control over how synchronization and scheduling is handled, by using
|
||||
a ringbuffer that the derived class controls and provides. The
|
||||
audiosink base-class is a derived class of the baseaudiosink,
|
||||
audiosink base-class is a derived class of the audiobasesink,
|
||||
implementing a standard ringbuffer implementing default
|
||||
synchronization and providing a standard audio-sample clock. Derived
|
||||
classes of this base class merely need to provide a <function>_open
|
||||
|
@ -123,7 +115,7 @@ gst_my_sink_class_init (GstMySinkClass * klass)
|
|||
()</function> function implementation, and some optional functions.
|
||||
This should suffice for many sound-server output elements and even
|
||||
most interfaces. More demanding audio systems, such as Jack, would
|
||||
want to implement the <classname>GstBaseAudioSink</classname>
|
||||
want to implement the <classname>GstAudioBaseSink</classname>
|
||||
base-class.
|
||||
</para>
|
||||
<para>
|
||||
|
@ -243,15 +235,9 @@ gst_my_sink_class_init (GstMySinkClass * klass)
|
|||
<listitem>
|
||||
<para>
|
||||
There is one and only one sourcepad. Source elements requiring
|
||||
multiple sourcepads cannot use this base-class.
|
||||
</para>
|
||||
</listitem>
|
||||
<listitem>
|
||||
<para>
|
||||
Since the base-class owns the pad and derived classes can only
|
||||
control it as far as the virtual functions allow, you are limited
|
||||
to the functionality provided by the virtual functions. If you need
|
||||
more, you cannot use this base-class.
|
||||
multiple sourcepads must implement a manager bin and use multiple
|
||||
source elements internally or make a manager element that uses
|
||||
a source element and a demuxer inside.
|
||||
</para>
|
||||
</listitem>
|
||||
</itemizedlist>
|
||||
|
@ -259,9 +245,6 @@ gst_my_sink_class_init (GstMySinkClass * klass)
|
|||
It is possible to use special memory, such as X server memory pointers
|
||||
or <function>mmap ()</function>'ed memory areas, as data pointers in
|
||||
buffers returned from the <function>create()</function> virtual function.
|
||||
In almost all cases, you will want to subclass
|
||||
<classname>GstBuffer</classname> so that your own set of functions can
|
||||
be called when the buffer is destroyed.
|
||||
</para>
|
||||
|
||||
<sect2 id="section-base-audiosrc" xreflabel="Writing an audio source">
|
||||
|
@ -275,7 +258,7 @@ gst_my_sink_class_init (GstMySinkClass * klass)
|
|||
linkend="section-base-audiosink"/>; one is ringbuffer-based, and
|
||||
requires the derived class to take care of its own scheduling,
|
||||
synchronization and such. The other is based on this
|
||||
<classname>GstBaseAudioSrc</classname> and is called
|
||||
<classname>GstAudioBaseSrc</classname> and is called
|
||||
<classname>GstAudioSrc</classname>, and provides a simple
|
||||
<function>open ()</function>, <function>close ()</function> and
|
||||
<function>read ()</function> interface, which is rather simple to
|
||||
|
|
Loading…
Reference in a new issue