*
* AVTP mime type is pretty simple and has no fields.
*
- * ### PTP Clock
+ * ### gPTP Setup
*
- * The AVTP plugin elements require that GStreamer pipeline clock be in sync
- * with the network generalized PTP clock (gPTP). Applications using the AVTP
- * plugin elements can achieve that by using GstPtpClock as the pipeline clock.
+ * The Linuxptp project provides the ptp4l daemon, which synchronizes the PTP
+ * clock from NIC, and the pmc tool which communicates with ptp4l to get/set
+ * some runtime settings. The project also provides the phc2sys daemon which
+ * synchronizes the PTP clock and system clock.
*
- * Note that GstPtpClock is a UDP slave only clock, meaning that some other
- * endpoint needs to provide the gPTP master clock.
+ * The AVTP plugin requires system clock is synchronized with PTP clock and
+ * TAI offset is properly set in the kernel. ptp4l and phc2sys can be set up
+ * in many different ways, below we provide an example that fullfils the plugin
+ * requirements. For further information check ptp4l(8) and phc2sys(8).
*
- * One can use, on another endpoint on the network, Linuxptp project ptp4l
- * daemon to provide a gPTP master clock on the network over UDP:
+ * In the following instructions, replace $IFNAME by your PTP capable NIC
+ * interface. The gPTP.cfg file mentioned below can be found in /usr/share/
+ * doc/linuxptp/ (depending on your distro).
*
- * $ ptp4l -i $IFNAME
+ * Synchronize PTP clock with PTP time:
*
- * For further information check ptp4l(8).
+ * $ ptp4l -f gPTP.cfg -i $IFNAME
*
- * ### FQTSS Setup
+ * Enable TAI offset to be automatically set by phc2sys:
+ *
+ * $ pmc -u -t 1 -b 0 'SET GRANDMASTER_SETTINGS_NP \
+ * clockClass 248 clockAccuracy 0xfe \
+ * offsetScaledLogVariance 0xffff \
+ * currentUtcOffset 37 leap61 0 leap59 0 \
+ * currentUtcOffsetValid 1 ptpTimescale 1 \
+ * timeTraceable 1 frequencyTraceable 0 timeSource 0xa0'
+ *
+ * Synchronize system clock with PTP clock:
+ *
+ * $ phc2sys -f gPTP.cfg -s $IFNAME -c CLOCK_REALTIME -w
+ *
+ * The commands above should be run on both AVTP Talker and Listener hosts.
+ *
+ * With clocks properly synchronized, applications using the AVTP plugin
+ * should use GstSytemClock with GST_CLOCK_TYPE_REALTIME as the pipeline
+ * clock.
+ *
+ * ### Traffic Control Setup
*
* FQTSS (Forwarding and Queuing Enhancements for Time-Sensitive Streams) can be
* enabled on Linux with the help of the mqprio and cbs qdiscs provided by the
* On the host that will run as AVTP Talker (pipeline that generates the video
* stream), run the following commands:
*
- * Configure mpqrio qdisc (replace $HANDLE_ID by an unused handle ID):
+ * Configure mpqrio qdisc (replace $MQPRIO_HANDLE_ID by an unused handle ID):
*
- * $ tc qdisc add dev $IFNAME parent root handle $HANDLE_ID mqprio \
+ * $ tc qdisc add dev $IFNAME parent root handle $MQPRIO_HANDLE_ID mqprio \
* num_tc 3 map 2 2 1 0 2 2 2 2 2 2 2 2 2 2 2 2 \
* queues 1@0 1@1 2@2 hw 0
*
- * Configure cbs qdisc:
+ * Configure cbs qdisc (replace $CBS_HANDLE_ID by an unused handle ID):
+ *
+ * $ tc qdisc replace dev $IFNAME parent $MQPRIO_HANDLE_ID:1 \
+ * handle $CBS_HANDLE_ID cbs idleslope 27756 sendslope -972244 \
+ * hicredit 42 locredit -1499 offload 1
+ *
+ * Also, the plugin implements a transmission scheduling mechanism that relies
+ * on ETF qdisc so make sure it is properly configured in your system. It could
+ * be configured in many ways, below follows an example.
*
- * $ tc qdisc replace dev $IFNAME parent $HANDLE_ID:1 cbs idleslope 27756 \
- * sendslope -972244 hicredit 42 locredit -1499 offload 1
+ * $ tc qdisc add dev $IFNAME parent $CBS_HANDLE_ID:1 etf \
+ * clockid CLOCK_TAI delta 500000 offload
*
- * No FQTSS configuration is required at the host running as AVTP Listener.
+ * No Traffic Control configuration is required at the host running as AVTP
+ * Listener.
*
* ### Capabilities
*
#include <arpa/inet.h>
#include <linux/if_packet.h>
+#include <linux/net_tstamp.h>
#include <net/ethernet.h>
#include <net/if.h>
#include <stdio.h>
#define DEFAULT_ADDRESS "01:AA:AA:AA:AA:AA"
#define DEFAULT_PRIORITY 0
+#define NSEC_PER_SEC 1000000000
+#define TAI_OFFSET (37ULL * NSEC_PER_SEC)
+#define UTC_TO_TAI(t) (t + TAI_OFFSET)
+
enum
{
PROP_0,
static gboolean gst_avtp_sink_stop (GstBaseSink * basesink);
static GstFlowReturn gst_avtp_sink_render (GstBaseSink * basesink, GstBuffer *
buffer);
+static void gst_avtp_sink_get_times (GstBaseSink * bsink, GstBuffer * buffer,
+ GstClockTime * start, GstClockTime * end);
static void
gst_avtp_sink_class_init (GstAvtpSinkClass * klass)
basesink_class->start = GST_DEBUG_FUNCPTR (gst_avtp_sink_start);
basesink_class->stop = GST_DEBUG_FUNCPTR (gst_avtp_sink_stop);
basesink_class->render = GST_DEBUG_FUNCPTR (gst_avtp_sink_render);
+ basesink_class->get_times = GST_DEBUG_FUNCPTR (gst_avtp_sink_get_times);
GST_DEBUG_CATEGORY_INIT (avtpsink_debug, "avtpsink", 0, "AVTP Sink");
}
unsigned int index;
guint8 addr[ETH_ALEN];
struct sockaddr_ll sk_addr;
+ struct sock_txtime txtime_cfg;
index = if_nametoindex (avtpsink->ifname);
if (!index) {
goto err;
}
+ txtime_cfg.clockid = CLOCK_TAI;
+ txtime_cfg.flags = 0;
+ res = setsockopt (fd, SOL_SOCKET, SO_TXTIME, &txtime_cfg,
+ sizeof (txtime_cfg));
+ if (res < 0) {
+ GST_ERROR_OBJECT (avtpsink, "Failed to set SO_TXTIME: %s", strerror
+ (errno));
+ goto err;
+ }
+
res = sscanf (avtpsink->address, "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx",
&addr[0], &addr[1], &addr[2], &addr[3], &addr[4], &addr[5]);
if (res != 6) {
return FALSE;
}
+static void
+gst_avtp_sink_init_msghdr (GstAvtpSink * avtpsink)
+{
+ struct msghdr *msg;
+ struct cmsghdr *cmsg;
+
+ msg = g_malloc0 (sizeof (struct msghdr));
+ msg->msg_name = &avtpsink->sk_addr;
+ msg->msg_namelen = sizeof (avtpsink->sk_addr);
+ msg->msg_iovlen = 1;
+ msg->msg_iov = g_malloc0 (sizeof (struct iovec));
+ msg->msg_controllen = CMSG_SPACE (sizeof (__u64));
+ msg->msg_control = g_malloc0 (msg->msg_controllen);
+
+ cmsg = CMSG_FIRSTHDR (msg);
+ cmsg->cmsg_level = SOL_SOCKET;
+ cmsg->cmsg_type = SCM_TXTIME;
+ cmsg->cmsg_len = CMSG_LEN (sizeof (__u64));
+
+ avtpsink->msg = msg;
+}
+
static gboolean
gst_avtp_sink_start (GstBaseSink * basesink)
{
if (!gst_avtp_sink_init_socket (avtpsink))
return FALSE;
+ gst_avtp_sink_init_msghdr (avtpsink);
+
GST_DEBUG_OBJECT (avtpsink, "AVTP sink started");
return TRUE;
{
GstAvtpSink *avtpsink = GST_AVTP_SINK (basesink);
+ g_free (avtpsink->msg->msg_iov);
+ g_free (avtpsink->msg->msg_control);
+ g_free (avtpsink->msg);
close (avtpsink->sk_fd);
GST_DEBUG_OBJECT (avtpsink, "AVTP sink stopped");
return TRUE;
}
+/* This function was heavily inspired by gst_base_sink_adjust_time() from
+ * GstBaseSink.
+ */
+static GstClockTime
+gst_avtp_sink_adjust_time (GstBaseSink * basesink, GstClockTime time)
+{
+ GstClockTimeDiff ts_offset;
+ GstClockTime render_delay;
+
+ /* don't do anything funny with invalid timestamps */
+ if (G_UNLIKELY (!GST_CLOCK_TIME_IS_VALID (time)))
+ return time;
+
+ time += gst_base_sink_get_latency (basesink);
+
+ /* apply offset, be careful for underflows */
+ ts_offset = gst_base_sink_get_ts_offset (basesink);
+ if (ts_offset < 0) {
+ ts_offset = -ts_offset;
+ if (ts_offset < time)
+ time -= ts_offset;
+ else
+ time = 0;
+ } else
+ time += ts_offset;
+
+ /* subtract the render delay again, which was included in the latency */
+ render_delay = gst_base_sink_get_render_delay (basesink);
+ if (time > render_delay)
+ time -= render_delay;
+ else
+ time = 0;
+
+ return time;
+}
+
static GstFlowReturn
gst_avtp_sink_render (GstBaseSink * basesink, GstBuffer * buffer)
{
ssize_t n;
GstMapInfo info;
GstAvtpSink *avtpsink = GST_AVTP_SINK (basesink);
+ struct iovec *iov = avtpsink->msg->msg_iov;
+
+ if (G_LIKELY (basesink->sync)) {
+ GstClockTime base_time, running_time;
+ struct cmsghdr *cmsg = CMSG_FIRSTHDR (avtpsink->msg);
+
+ g_assert (GST_BUFFER_DTS_OR_PTS (buffer) != GST_CLOCK_TIME_NONE);
+
+ base_time = gst_element_get_base_time (GST_ELEMENT (avtpsink));
+ running_time = gst_segment_to_running_time (&basesink->segment,
+ basesink->segment.format, GST_BUFFER_DTS_OR_PTS (buffer));
+ running_time = gst_avtp_sink_adjust_time (basesink, running_time);
+ *(__u64 *) CMSG_DATA (cmsg) = UTC_TO_TAI (base_time + running_time);
+ }
if (!gst_buffer_map (buffer, &info, GST_MAP_READ)) {
GST_ERROR_OBJECT (avtpsink, "Failed to map buffer");
return GST_FLOW_ERROR;
}
- n = sendto (avtpsink->sk_fd, info.data, info.size, 0,
- (struct sockaddr *) &avtpsink->sk_addr, sizeof (avtpsink->sk_addr));
+ iov->iov_base = info.data;
+ iov->iov_len = info.size;
+
+ n = sendmsg (avtpsink->sk_fd, avtpsink->msg, 0);
if (n < 0) {
GST_INFO_OBJECT (avtpsink, "Failed to send AVTPDU: %s", strerror (errno));
goto out;
return GST_FLOW_OK;
}
+static void
+gst_avtp_sink_get_times (GstBaseSink * bsink, GstBuffer * buffer,
+ GstClockTime * start, GstClockTime * end)
+{
+ /* Rendering synchronization is handled by the GstAvtpSink class itself, not
+ * GstBaseSink so we set 'start' and 'end' to GST_CLOCK_TIME_NONE to signal
+ * that to the base class.
+ */
+ *start = GST_CLOCK_TIME_NONE;
+ *end = GST_CLOCK_TIME_NONE;
+}
+
gboolean
gst_avtp_sink_plugin_init (GstPlugin * plugin)
{