diff options
author | Sebastian Dröge <sebastian@centricular.com> | 2015-05-14 12:18:25 +0200 |
---|---|---|
committer | Sebastian Dröge <sebastian@centricular.com> | 2015-06-03 13:55:28 +0200 |
commit | 8d7775983460e605c46628644fecb1839bde1ffe (patch) | |
tree | dd3d2861169bbfe5d0d956124d28ce1f7368f818 | |
parent | fe3249b0e1092c442e0889640295569eb012177d (diff) |
ptp: Initial implementation of a PTP clock
GstPtpClock implements a PTP (IEEE1588:2008) ordinary clock in
slave-only mode, that allows a GStreamer pipeline to synchronize
to a PTP network clock in some specific domain.
The PTP subsystem can be initialized with gst_ptp_init(), which then
starts a helper process to do the actual communication via the PTP
ports. This is required as PTP listens on ports < 1024 and thus
requires special privileges. Once this helper process is started, the
main process will synchronize to all PTP domains that are detected on
the selected interfaces.
gst_ptp_clock_new() then allows to create a GstClock that provides the
PTP time from a master clock inside a specific PTP domain. This clock
will only return valid timestamps once the timestamps in the PTP domain
are known. To check this, the GstPtpClock::internal-clock property and
the related notify::clock signal can be used. Once the internal clock
is not NULL, the PTP domain's time is known. Alternatively you can wait
for this with gst_ptp_clock_wait_ready().
To gather statistics about the PTP clock synchronization,
gst_ptp_statistics_callback_add() can be used. This gives the
application the possibility to collect all kinds of statistics
from the clock synchronization.
https://bugzilla.gnome.org/show_bug.cgi?id=749391
-rw-r--r-- | configure.ac | 107 | ||||
-rw-r--r-- | docs/libs/gstreamer-libs-docs.sgml | 1 | ||||
-rw-r--r-- | docs/libs/gstreamer-libs-sections.txt | 26 | ||||
-rw-r--r-- | libs/gst/helpers/Makefile.am | 32 | ||||
-rw-r--r-- | libs/gst/helpers/gst-ptp-helper.c | 560 | ||||
-rw-r--r-- | libs/gst/net/Makefile.am | 23 | ||||
-rw-r--r-- | libs/gst/net/gstptp_private.h | 19 | ||||
-rw-r--r-- | libs/gst/net/gstptpclock.c | 2431 | ||||
-rw-r--r-- | libs/gst/net/gstptpclock.h | 142 | ||||
-rw-r--r-- | libs/gst/net/net.h | 1 | ||||
-rw-r--r-- | tests/examples/Makefile.am | 1 | ||||
-rw-r--r-- | tests/examples/ptp/.gitignore | 1 | ||||
-rw-r--r-- | tests/examples/ptp/Makefile.am | 7 | ||||
-rw-r--r-- | tests/examples/ptp/ptp-print-times.c | 100 | ||||
-rw-r--r-- | win32/common/libgstnet.def | 8 |
15 files changed, 3449 insertions, 10 deletions
diff --git a/configure.ac b/configure.ac index 42fe32ac0..c64855be2 100644 --- a/configure.ac +++ b/configure.ac @@ -258,6 +258,105 @@ if test "x$USE_POISONING" = xyes; then [Define if we should poison deallocated memory]) fi +dnl PTP support parts +AC_MSG_CHECKING([whether PTP support can be enabled]) +case "$host_os" in + *android*) + dnl Can't run on Android because of permissions + HAVE_PTP=no + ;; + mingw*|pw32*|cygwin*) + dnl Not ported to Windows yet + HAVE_PTP=no + ;; + darwin*) + dnl Can't run on iOS because of permissions + AC_CHECK_HEADER(MobileCoreServices/MobileCoreServices.h, HAVE_PTP="no", HAVE_PTP="yes", [-]) + ;; + linux*|darwin*|solaris*|netbsd*|freebsd*|openbsd*|kfreebsd*|dragonfly*|gnu*) + HAVE_PTP=yes + ;; + *) + HAVE_PTP=no + ;; +esac +AC_MSG_RESULT([$HAVE_PTP]) + +dnl user/group to change to in gst-ptp-helper +AC_ARG_WITH([ptp-helper-setuid-user], + AS_HELP_STRING([--with-ptp-helper-setuid-user],[User to switch to when installing gst-ptp-helper setuid root]), + [ + if test "x$withval" != "x" + then + AC_DEFINE_UNQUOTED(HAVE_PTP_HELPER_SETUID_USER, "$withval", [PTP helper setuid user]) + fi + ], [] +) + +dnl group/group to change to in gst-ptp-helper +AC_ARG_WITH([ptp-helper-setuid-group], + AS_HELP_STRING([--with-ptp-helper-setuid-group],[Group to switch to when installing gst-ptp-helper setuid root]), + [ + if test "x$withval" != "x" + then + AC_DEFINE_UNQUOTED(HAVE_PTP_HELPER_SETUID_GROUP, "$withval", [PTP helper setuid group]) + fi + ], [] +) + +AC_ARG_WITH( + ptp-helper-permissions, + AC_HELP_STRING( + [--with-ptp-helper-permissions], + [how to gain PTP permissions (none, setuid-root, capabilities, auto)]), + [], + [with_ptp_helper_permissions=auto]) + +gst_ptp_have_cap=no +AG_GST_CHECK_LIBHEADER(CAP, cap, + cap_init, , + sys/capability.h, + CAP_LIBS="-lcap" + AC_SUBST(CAP_LIBS) + gst_ptp_have_cap=yes) + +AC_PATH_PROG([SETCAP], [setcap], [no], [$PATH:/usr/bin:/bin:/usr/sbin:/sbin]) + +if test "x$HAVE_PTP" = "xyes"; then +AC_DEFINE(HAVE_PTP, 1, [PTP support available]) + +AC_MSG_CHECKING([how to install gst-ptp-helper]) +if test "x$with_ptp_helper_permissions" = "xauto"; then + if test "x$gst_ptp_have_cap" = "xyes" -a "x$SETCAP" != "xno"; then + with_ptp_helper_permissions="capabilities" + else + with_ptp_helper_permissions="setuid-root" + fi +fi +AC_MSG_RESULT([$with_ptp_helper_permissions]) + +case "$with_ptp_helper_permissions" in + none) + ;; + setuid-root) + AC_DEFINE(HAVE_PTP_HELPER_SETUID, 1, + [Use setuid-root for permissions in PTP helper]) + ;; + capabilities) + AC_DEFINE(HAVE_PTP_HELPER_CAPABILITIES, 1, + [Use capabilities for permissions in PTP helper]) + ;; + *) + AC_MSG_ERROR(Invalid parameter [$with_ptp_helper_permissions]) + ;; +esac + +fi + +AM_CONDITIONAL(HAVE_PTP, test "x$HAVE_PTP" = "xyes") +AM_CONDITIONAL(HAVE_PTP_HELPER_SETUID, test "x$with_ptp_helper_permissions" = "xsetuid-root") +AM_CONDITIONAL(HAVE_PTP_HELPER_CAPABILITIES, test "x$with_ptp_helper_permissions" = "xcapabilities") + dnl *** checks for platform *** dnl * hardware/architecture * @@ -806,6 +905,12 @@ AC_DEFINE_UNQUOTED(GST_PLUGIN_SCANNER_INSTALLED, "$GST_PLUGIN_SCANNER_INSTALLED", [location of the installed gst-plugin-scanner]) AC_SUBST(GST_PLUGIN_SCANNER_INSTALLED) +dnl ptp helper locations +AS_AC_EXPAND(GST_PTP_HELPER_INSTALLED,${libexecdir}/gstreamer-$GST_API_VERSION/gst-ptp-helper) +AC_DEFINE_UNQUOTED(GST_PTP_HELPER_INSTALLED, + "$GST_PTP_HELPER_INSTALLED", [location of the installed gst-ptp-helper]) +AC_SUBST(GST_PTP_HELPER_INSTALLED) + dnl things for our internal libcheck (must be called even if building dnl libcheck is disabled because it defines conditionals) AG_GST_CHECK_CHECKS() @@ -842,6 +947,7 @@ tests/examples/helloworld/Makefile tests/examples/manual/Makefile tests/examples/memory/Makefile tests/examples/netclock/Makefile +tests/examples/ptp/Makefile tests/examples/streamiddemux/Makefile tests/examples/streams/Makefile tools/Makefile @@ -945,6 +1051,7 @@ Configuration Plugin support : ${enable_plugin} Static plugins : ${enable_static_plugins} Unit testing support : ${BUILD_CHECK} + PTP clock support : ${HAVE_PTP} Debug : ${USE_DEBUG} Profiling : ${USE_PROFILING} diff --git a/docs/libs/gstreamer-libs-docs.sgml b/docs/libs/gstreamer-libs-docs.sgml index 0070771da..406d6064e 100644 --- a/docs/libs/gstreamer-libs-docs.sgml +++ b/docs/libs/gstreamer-libs-docs.sgml @@ -76,6 +76,7 @@ <xi:include href="xml/gstnetclientclock.xml" /> <xi:include href="xml/gstnettimepacket.xml" /> <xi:include href="xml/gstnettimeprovider.xml" /> + <xi:include href="xml/gstptpclock.xml" /> </chapter> <chapter id="gstreamer-check"> diff --git a/docs/libs/gstreamer-libs-sections.txt b/docs/libs/gstreamer-libs-sections.txt index f1db3a097..1dfc53b44 100644 --- a/docs/libs/gstreamer-libs-sections.txt +++ b/docs/libs/gstreamer-libs-sections.txt @@ -942,6 +942,32 @@ gst_net_time_provider_get_type </SECTION> <SECTION> +<FILE>gstptpclock</FILE> +<TITLE>GstPtpClock</TITLE> +<INCLUDE>gst/net/net.h</INCLUDE> +gst_ptp_init +gst_ptp_deinit +gst_ptp_is_initialized +gst_ptp_is_supported + +GstPtpClock +gst_ptp_clock_new + +gst_ptp_statistics_callback_add +gst_ptp_statistics_callback_remove +<SUBSECTION Standard> +GstPtpClockClass +GstPtpClockPrivate +GST_PTP_CLOCK +GST_IS_PTP_CLOCK +GST_TYPE_PTP_CLOCK +GST_PTP_CLOCK_CLASS +GST_IS_PTP_CLOCK_CLASS +<SUBSECTION Private> +gst_ptp_clock_get_type +</SECTION> + +<SECTION> <FILE>gstcheck</FILE> <TITLE>GstCheck</TITLE> <INCLUDE>gst/check/gstcheck.h</INCLUDE> diff --git a/libs/gst/helpers/Makefile.am b/libs/gst/helpers/Makefile.am index 1f1eacca6..141c2c135 100644 --- a/libs/gst/helpers/Makefile.am +++ b/libs/gst/helpers/Makefile.am @@ -7,8 +7,33 @@ gst_completion_helper_@GST_API_VERSION@_LDADD = $(GST_OBJ_LIBS) bashhelpersdir = $(BASH_HELPERS_DIR) dist_bashhelpers_DATA = gst +endif + +helpers_PROGRAMS = gst-plugin-scanner +helpersdir=$(libexecdir)/gstreamer-$(GST_API_VERSION) + +gst_plugin_scanner_SOURCES = gst-plugin-scanner.c +gst_plugin_scanner_CFLAGS = $(GST_OBJ_CFLAGS) +gst_plugin_scanner_LDADD = $(GST_OBJ_LIBS) + +if HAVE_PTP +helpers_PROGRAMS += gst-ptp-helper +gst_ptp_helper_SOURCES = gst-ptp-helper.c +gst_ptp_helper_CFLAGS = $(GST_OBJ_CFLAGS) $(GIO_CFLAGS) +gst_ptp_helper_LDADD = $(GST_OBJ_LIBS) $(GIO_LIBS) $(CAP_LIBS) +endif install-exec-hook: +if HAVE_PTP +if HAVE_PTP_HELPER_SETUID + chown root $(DESTDIR)$(helpersdir)/gst-ptp-helper + chmod u+s $(DESTDIR)$(helpersdir)/gst-ptp-helper +endif +if HAVE_PTP_HELPER_CAPABILITIES + $(SETCAP) cap_net_bind_service,cap_net_admin+ep $(DESTDIR)$(helpersdir)/gst-ptp-helper +endif +endif +if ENABLE_BASH_COMPLETION $(MKDIR_P) $(DESTDIR)$(BASH_HELPERS_DIR) && \ cd $(DESTDIR)$(bindir) && \ $(INSTALL) `echo "gst-completion-helper-" | sed '$(transform)'`@GST_API_VERSION@$(EXEEXT) \ @@ -19,13 +44,6 @@ uninstall-hook: rm -f $(DESTDIR)$(BASH_HELPERS_DIR)/gst-completion-helper-@GST_API_VERSION@$(EXEEXT) endif -helpers_PROGRAMS = gst-plugin-scanner -helpersdir=$(libexecdir)/gstreamer-$(GST_API_VERSION) - -gst_plugin_scanner_SOURCES = gst-plugin-scanner.c -gst_plugin_scanner_CFLAGS = $(GST_OBJ_CFLAGS) -gst_plugin_scanner_LDADD = $(GST_OBJ_LIBS) - # clean out the old one to make sure everything is udpated correctly # remove again after release CLEANFILES = plugin-scanner diff --git a/libs/gst/helpers/gst-ptp-helper.c b/libs/gst/helpers/gst-ptp-helper.c new file mode 100644 index 000000000..2f1063947 --- /dev/null +++ b/libs/gst/helpers/gst-ptp-helper.c @@ -0,0 +1,560 @@ +/* GStreamer + * Copyright (C) 2015 Sebastian Dröge <sebastian@centricular.com> + * + * + * 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., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +/* Helper process that runs setuid root or with appropriate privileges to + * listen on ports < 1024, do multicast operations and get MAC addresses of + * interfaces. Privileges are dropped after these operations are done. + * + * It listens on the PTP multicast group on port 319 and 320 and forwards + * everything received there to stdout, while forwarding everything received + * on stdout to those sockets. + * Additionally it provides the MAC address of a network interface via stdout + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include <stdlib.h> +#include <unistd.h> +#include <sys/types.h> +#include <errno.h> +#include <sys/ioctl.h> +#include <net/if.h> +#include <netinet/in.h> +#include <string.h> + +#ifdef HAVE_PTP_HELPER_SETUID +#include <grp.h> +#include <pwd.h> +#endif + +#ifdef HAVE_PTP_HELPER_CAPABILITIES +#include <sys/capability.h> +#endif + +#include <glib.h> +#include <gio/gio.h> + +#include <gst/gst.h> +#include <gst/net/gstptp_private.h> + +#define PTP_MULTICAST_GROUP "224.0.1.129" +#define PTP_EVENT_PORT 319 +#define PTP_GENERAL_PORT 320 + +static gchar **ifaces = NULL; +static gboolean verbose = FALSE; +static guint64 clock_id = (guint64) - 1; +static guint8 clock_id_array[8]; + +static GOptionEntry opt_entries[] = { + {"interface", 'i', 0, G_OPTION_ARG_STRING_ARRAY, &ifaces, + "Interface to listen on", NULL}, + {"clock-id", 'c', 0, G_OPTION_ARG_INT64, &clock_id, + "PTP clock id", NULL}, + {"verbose", 'v', 0, G_OPTION_ARG_NONE, &verbose, + "Be verbose", NULL}, + {NULL} +}; + +static GSocketAddress *event_saddr, *general_saddr; +static GSocket *socket_event, *socket_general; +static GIOChannel *stdin_channel, *stdout_channel; + +static gboolean +have_socket_data_cb (GSocket * socket, GIOCondition condition, + gpointer user_data) +{ + gchar buffer[8192]; + gssize read; + gsize written; + GError *err = NULL; + GIOStatus status; + StdIOHeader header = { 0, }; + + read = g_socket_receive (socket, buffer, sizeof (buffer), NULL, &err); + if (read == -1) + g_error ("Failed to read from socket: %s", err->message); + + if (verbose) + g_message ("Received %" G_GSSIZE_FORMAT " bytes from %s socket", read, + (socket == socket_event ? "event" : "general")); + + header.size = read; + header.type = (socket == socket_event) ? TYPE_EVENT : TYPE_GENERAL; + + status = + g_io_channel_write_chars (stdout_channel, (gchar *) & header, + sizeof (header), &written, &err); + if (status == G_IO_STATUS_ERROR) { + g_error ("Failed to write to stdout: %s", err->message); + } else if (status == G_IO_STATUS_EOF) { + g_message ("EOF on stdout"); + exit (0); + } else if (status != G_IO_STATUS_NORMAL) { + g_error ("Unexpected stdout write status: %d", status); + } else if (written != sizeof (header)) { + g_error ("Unexpected write size: %" G_GSIZE_FORMAT, written); + } + + status = + g_io_channel_write_chars (stdout_channel, buffer, read, &written, &err); + if (status == G_IO_STATUS_ERROR) { + g_error ("Failed to write to stdout: %s", err->message); + } else if (status == G_IO_STATUS_EOF) { + g_message ("EOF on stdout"); + exit (0); + } else if (status != G_IO_STATUS_NORMAL) { + g_error ("Unexpected stdout write status: %d", status); + } else if (written != read) { + g_error ("Unexpected write size: %" G_GSIZE_FORMAT, written); + } + + return G_SOURCE_CONTINUE; +} + +static gboolean +have_stdin_data_cb (GIOChannel * channel, GIOCondition condition, + gpointer user_data) +{ + GIOStatus status; + StdIOHeader header = { 0, }; + gchar buffer[8192]; + GError *err = NULL; + gsize read; + gssize written; + + if ((condition & G_IO_STATUS_EOF)) { + g_message ("EOF on stdin"); + exit (0); + } + + status = + g_io_channel_read_chars (channel, (gchar *) & header, sizeof (header), + &read, &err); + if (status == G_IO_STATUS_ERROR) { + g_error ("Failed to read from stdin: %s", err->message); + } else if (status == G_IO_STATUS_EOF) { + g_message ("EOF on stdin"); + exit (0); + } else if (status != G_IO_STATUS_NORMAL) { + g_error ("Unexpected stdin read status: %d", status); + } else if (read != sizeof (header)) { + g_error ("Unexpected read size: %" G_GSIZE_FORMAT, read); + } else if (header.size > 8192) { + g_error ("Unexpected size: %u", header.size); + } + + status = g_io_channel_read_chars (channel, buffer, header.size, &read, &err); + if (status == G_IO_STATUS_ERROR) { + g_error ("Failed to read from stdin: %s", err->message); + } else if (status == G_IO_STATUS_EOF) { + g_message ("EOF on stdin"); + exit (0); + } else if (status != G_IO_STATUS_NORMAL) { + g_error ("Unexpected stdin read status: %d", status); + } else if (read != header.size) { + g_error ("Unexpected read size: %" G_GSIZE_FORMAT, read); + } + + switch (header.type) { + case TYPE_EVENT: + case TYPE_GENERAL: + written = + g_socket_send_to (header.type == + TYPE_EVENT ? socket_event : socket_general, + (header.type == TYPE_EVENT ? event_saddr : general_saddr), buffer, + header.size, NULL, &err); + if (written == -1) + g_error ("Failed to write to socket: %s", err->message); + else if (written != header.size) + g_error ("Unexpected write size: %" G_GSSIZE_FORMAT, written); + + if (verbose) + g_message ("Sent %" G_GSSIZE_FORMAT " bytes to %s socket", read, + (header.type == TYPE_EVENT ? "event" : "general")); + break; + default: + break; + } + + return G_SOURCE_CONTINUE; +} + +static void +setup_sockets (void) +{ + GInetAddress *bind_addr, *mcast_addr; + GSocketAddress *bind_saddr; + GSource *socket_event_source, *socket_general_source; + gchar **probed_ifaces = NULL; + GError *err = NULL; + + /* Create sockets */ + socket_event = + g_socket_new (G_SOCKET_FAMILY_IPV4, G_SOCKET_TYPE_DATAGRAM, + G_SOCKET_PROTOCOL_UDP, &err); + if (!socket_event) + g_error ("Couldn't create event socket: %s", err->message); + + socket_general = + g_socket_new (G_SOCKET_FAMILY_IPV4, G_SOCKET_TYPE_DATAGRAM, + G_SOCKET_PROTOCOL_UDP, &err); + if (!socket_general) + g_error ("Couldn't create general socket: %s", err->message); + + /* Bind sockets */ + bind_addr = g_inet_address_new_any (G_SOCKET_FAMILY_IPV4); + bind_saddr = g_inet_socket_address_new (bind_addr, PTP_EVENT_PORT); + if (!g_socket_bind (socket_event, bind_saddr, TRUE, &err)) + g_error ("Couldn't bind event socket: %s", err->message); + g_object_unref (bind_saddr); + bind_saddr = g_inet_socket_address_new (bind_addr, PTP_GENERAL_PORT); + if (!g_socket_bind (socket_general, bind_saddr, TRUE, &err)) + g_error ("Couldn't bind general socket: %s", err->message); + g_object_unref (bind_saddr); + g_object_unref (bind_addr); + + /* Probe all non-loopback interfaces */ + if (!ifaces) { + struct ifreq ifr; + struct ifconf ifc; + gchar buf[8192]; + + ifc.ifc_len = sizeof (buf); + ifc.ifc_buf = buf; + if (ioctl (g_socket_get_fd (socket_event), SIOCGIFCONF, &ifc) != -1) { + struct ifreq *it = ifc.ifc_req; + const struct ifreq *const end = + it + (ifc.ifc_len / sizeof (struct ifreq)); + guint idx = 0; + + probed_ifaces = g_new0 (gchar *, ifc.ifc_len + 1); + + for (; it != end; ++it) { + strcpy (ifr.ifr_name, it->ifr_name); + if (ioctl (g_socket_get_fd (socket_event), SIOCGIFFLAGS, &ifr) == 0) { + if ((ifr.ifr_flags & IFF_LOOPBACK)) + continue; + probed_ifaces[idx] = g_strdup (it->ifr_name); + idx++; + } else { + g_warning ("can't get flags of interface '%s'", it->ifr_name); + probed_ifaces[idx] = g_strdup (it->ifr_name); + idx++; + } + } + + if (idx != 0) + ifaces = probed_ifaces; + } + } + + /* Get a clock id from the MAC address if none was given */ + if (clock_id == (guint64) - 1) { + struct ifreq ifr; + gboolean success = FALSE; + + if (ifaces) { + gchar **ptr = ifaces; + + while (*ptr) { + strcpy (ifr.ifr_name, *ptr); + if (ioctl (g_socket_get_fd (socket_event), SIOCGIFHWADDR, &ifr) == 0) { + clock_id_array[0] = ifr.ifr_hwaddr.sa_data[0]; + clock_id_array[1] = ifr.ifr_hwaddr.sa_data[1]; + clock_id_array[2] = ifr.ifr_hwaddr.sa_data[2]; + clock_id_array[3] = 0xff; + clock_id_array[4] = 0xfe; + clock_id_array[5] = ifr.ifr_hwaddr.sa_data[3]; + clock_id_array[6] = ifr.ifr_hwaddr.sa_data[4]; + clock_id_array[7] = ifr.ifr_hwaddr.sa_data[5]; + success = TRUE; + break; + } + } + + ptr++; + } else { + struct ifconf ifc; + gchar buf[8192]; + + ifc.ifc_len = sizeof (buf); + ifc.ifc_buf = buf; + if (ioctl (g_socket_get_fd (socket_event), SIOCGIFCONF, &ifc) != -1) { + struct ifreq *it = ifc.ifc_req; + const struct ifreq *const end = + it + (ifc.ifc_len / sizeof (struct ifreq)); + + for (; it != end; ++it) { + strcpy (ifr.ifr_name, it->ifr_name); + if (ioctl (g_socket_get_fd (socket_event), SIOCGIFFLAGS, &ifr) == 0) { + if ((ifr.ifr_flags & IFF_LOOPBACK)) + continue; + + if (ioctl (g_socket_get_fd (socket_event), SIOCGIFHWADDR, + &ifr) == 0) { + clock_id_array[0] = ifr.ifr_hwaddr.sa_data[0]; + clock_id_array[1] = ifr.ifr_hwaddr.sa_data[1]; + clock_id_array[2] = ifr.ifr_hwaddr.sa_data[2]; + clock_id_array[3] = 0xff; + clock_id_array[4] = 0xfe; + clock_id_array[5] = ifr.ifr_hwaddr.sa_data[3]; + clock_id_array[6] = ifr.ifr_hwaddr.sa_data[4]; + clock_id_array[7] = ifr.ifr_hwaddr.sa_data[5]; + success = TRUE; + break; + } + } else { + g_warning ("can't get flags of interface '%s'", it->ifr_name); + } + } + } + } + + if (!success) { + g_warning ("can't get any MAC address, using random clock id"); + clock_id = (((guint64) g_random_int ()) << 32) | (g_random_int ()); + GST_WRITE_UINT64_BE (clock_id_array, clock_id); + clock_id_array[3] = 0xff; + clock_id_array[4] = 0xfe; + } + } else { + GST_WRITE_UINT64_BE (clock_id_array, clock_id); + } + + /* Join multicast groups */ + mcast_addr = g_inet_address_new_from_string (PTP_MULTICAST_GROUP); + if (ifaces) { + gchar **ptr = ifaces; + gboolean success = FALSE; + + while (*ptr) { + gint c = 0; + if (!g_socket_join_multicast_group (socket_event, mcast_addr, FALSE, *ptr, + &err)) + g_warning ("Couldn't join multicast group on interface '%s': %s", + *ptr, err->message); + else + c++; + + if (!g_socket_join_multicast_group (socket_general, mcast_addr, FALSE, + *ptr, &err)) + g_warning ("Couldn't join multicast group on interface '%s': %s", + *ptr, err->message); + else + c++; + + if (c == 2) + success = TRUE; + ptr++; + } + + if (!success) { + /* Join multicast group without any interface */ + if (!g_socket_join_multicast_group (socket_event, mcast_addr, FALSE, NULL, + &err)) + g_error ("Couldn't join multicast group: %s", err->message); + if (!g_socket_join_multicast_group (socket_general, mcast_addr, FALSE, + NULL, &err)) + g_error ("Couldn't join multicast group: %s", err->message); + } + } else { + /* Join multicast group without any interface */ + if (!g_socket_join_multicast_group (socket_event, mcast_addr, FALSE, NULL, + &err)) + g_error ("Couldn't join multicast group: %s", err->message); + if (!g_socket_join_multicast_group (socket_general, mcast_addr, FALSE, NULL, + &err)) + g_error ("Couldn't join multicast group: %s", err->message); + } + + event_saddr = g_inet_socket_address_new (mcast_addr, PTP_EVENT_PORT); + general_saddr = g_inet_socket_address_new (mcast_addr, PTP_GENERAL_PORT); + + /* Create socket sources */ + socket_event_source = + g_socket_create_source (socket_event, G_IO_IN | G_IO_PRI, NULL); + g_source_set_priority (socket_event_source, G_PRIORITY_HIGH); + g_source_set_callback (socket_event_source, (GSourceFunc) have_socket_data_cb, + NULL, NULL); + g_source_attach (socket_event_source, NULL); + socket_general_source = + g_socket_create_source (socket_general, G_IO_IN | G_IO_PRI, NULL); + g_source_set_priority (socket_general_source, G_PRIORITY_DEFAULT); + g_source_set_callback (socket_general_source, + (GSourceFunc) have_socket_data_cb, NULL, NULL); + g_source_attach (socket_general_source, NULL); + + g_strfreev (probed_ifaces); +} + +static void +drop_privileges (void) +{ +#ifdef HAVE_PTP_HELPER_SETUID + /* Switch to the given user/group */ +#ifdef HAVE_PTP_HELPER_SETUID_GROUP + { + struct group *grp; + + grp = getgrnam (HAVE_PTP_HELPER_SETUID_GROUP); + if (!grp) + g_error ("Failed to get group information '%s': %s", + HAVE_PTP_HELPER_SETUID_GROUP, g_strerror (errno)); + + if (setgid (grp->gr_gid) != 0) + g_error ("Failed to change to group '%s': %s", + HAVE_PTP_HELPER_SETUID_GROUP, g_strerror (errno)); + } +#endif + +#ifdef HAVE_PTP_HELPER_SETUID_USER + { + struct passwd *pwd; + + pwd = getpwnam (HAVE_PTP_HELPER_SETUID_USER); + if (!pwd) + g_error ("Failed to get user information '%s': %s", + HAVE_PTP_HELPER_SETUID_USER, g_strerror (errno)); + +#ifndef HAVE_PTP_HELPER_SETUID_GROUP + if (setgid (pwd->pw_gid) != 0) + g_error ("Failed to change to user group '%s': %s", + HAVE_PTP_HELPER_SETUID_USER, g_strerror (errno)); +#endif + + if (setuid (pwd->pw_uid) != 0) + g_error ("Failed to change to user '%s': %s", HAVE_PTP_HELPER_SETUID_USER, + g_strerror (errno)); + } +#endif +#endif +#ifdef HAVE_PTP_HELPER_CAPABILITIES + /* Drop all capabilities */ + { + cap_t caps; + + caps = cap_get_proc (); + if (caps == 0) + g_error ("Failed to get process caps: %s", g_strerror (errno)); + if (cap_clear (caps) != 0) + g_error ("Failed to clear caps: %s", g_strerror (errno)); + if (cap_set_proc (caps) != 0) + g_error ("Failed to set process caps: %s", g_strerror (errno)); + } +#endif +} + +static void +setup_stdio_channels (void) +{ + GSource *stdin_source; + + /* Create stdin source */ + stdin_channel = g_io_channel_unix_new (STDIN_FILENO); + if (g_io_channel_set_encoding (stdin_channel, NULL, + NULL) == G_IO_STATUS_ERROR) + g_error ("Failed to set stdin to binary encoding"); + g_io_channel_set_buffered (stdin_channel, FALSE); + stdin_source = + g_io_create_watch (stdin_channel, G_IO_IN | G_IO_PRI | G_IO_HUP); + g_source_set_priority (stdin_source, G_PRIORITY_DEFAULT); + g_source_set_callback (stdin_source, (GSourceFunc) have_stdin_data_cb, NULL, + NULL); + g_source_attach (stdin_source, NULL); + + /* Create stdout channel */ + stdout_channel = g_io_channel_unix_new (STDOUT_FILENO); + if (g_io_channel_set_encoding (stdout_channel, NULL, + NULL) == G_IO_STATUS_ERROR) + g_error ("Failed to set stdout to binary encoding"); + g_io_channel_set_buffered (stdout_channel, FALSE); +} + +static void +write_clock_id (void) +{ + GError *err = NULL; + GIOStatus status; + StdIOHeader header = { 0, }; + gsize written; + + /* Write clock id to stdout */ + + header.type = TYPE_CLOCK_ID; + header.size = 8; + status = + g_io_channel_write_chars (stdout_channel, (gchar *) & header, + sizeof (header), &written, &err); + if (status == G_IO_STATUS_ERROR) { + g_error ("Failed to write to stdout: %s", err->message); + } else if (status == G_IO_STATUS_EOF) { + g_message ("EOF on stdout"); + exit (0); + } else if (status != G_IO_STATUS_NORMAL) { + g_error ("Unexpected stdout write status: %d", status); + } else if (written != sizeof (header)) { + g_error ("Unexpected write size: %" G_GSIZE_FORMAT, written); + } + + status = + g_io_channel_write_chars (stdout_channel, + (const gchar *) clock_id_array, sizeof (clock_id_array), &written, &err); + if (status == G_IO_STATUS_ERROR) { + g_error ("Failed to write to stdout: %s", err->message); + } else if (status == G_IO_STATUS_EOF) { + g_message ("EOF on stdout"); + exit (0); + } else if (status != G_IO_STATUS_NORMAL) { + g_error ("Unexpected stdout write status: %d", status); + } else if (written != sizeof (clock_id_array)) { + g_error ("Unexpected write size: %" G_GSIZE_FORMAT, written); + } +} + +gint +main (gint argc, gchar ** argv) +{ + GOptionContext *opt_ctx; + GMainLoop *loop; + GError *err = NULL; + + opt_ctx = g_option_context_new ("- GStreamer PTP helper process"); + g_option_context_add_main_entries (opt_ctx, opt_entries, NULL); + if (!g_option_context_parse (opt_ctx, &argc, &argv, &err)) + g_error ("Error parsing options: %s", err->message); + g_option_context_free (opt_ctx); + + setup_sockets (); + drop_privileges (); + setup_stdio_channels (); + write_clock_id (); + + /* Get running */ + loop = g_main_loop_new (NULL, FALSE); + g_main_loop_run (loop); + + /* We never exit cleanly, so don't do cleanup */ + g_assert_not_reached (); + + return 0; +} diff --git a/libs/gst/net/Makefile.am b/libs/gst/net/Makefile.am index f78f679f6..cc81cef44 100644 --- a/libs/gst/net/Makefile.am +++ b/libs/gst/net/Makefile.am @@ -8,19 +8,36 @@ libgstnet_@GST_API_VERSION@_include_HEADERS = \ gstnetclientclock.h \ gstnetcontrolmessagemeta.h \ gstnettimepacket.h \ - gstnettimeprovider.h + gstnettimeprovider.h \ + gstptpclock.h libgstnet_@GST_API_VERSION@_la_SOURCES = \ gstnetaddressmeta.c \ gstnetclientclock.c \ gstnetcontrolmessagemeta.c \ gstnettimepacket.c \ - gstnettimeprovider.c + gstnettimeprovider.c \ + gstptpclock.c + +noinst_HEADERS = gstptp_private.h libgstnet_@GST_API_VERSION@_la_CFLAGS = $(GST_OBJ_CFLAGS) $(GIO_CFLAGS) -libgstnet_@GST_API_VERSION@_la_LIBADD = $(GST_OBJ_LIBS) $(GIO_LIBS) +libgstnet_@GST_API_VERSION@_la_LIBADD = $(GST_OBJ_LIBS) $(GIO_LIBS) \ + $(top_builddir)/libs/gst/base/libgstbase-@GST_API_VERSION@.la libgstnet_@GST_API_VERSION@_la_LDFLAGS = $(GST_LIB_LDFLAGS) $(GST_ALL_LDFLAGS) $(GST_LT_LDFLAGS) +# try to prevent packaging errors +check-libexecdir-consistency: + @if test "${GST_PTP_HELPER_INSTALLED}" != "${libexecdir}/gstreamer-$(GST_API_VERSION)/gst-ptp-helper"; then \ + echo "*** Inconsistent libexecdir! Please use ./configure --libexecdir=/foo/bar"; \ + echo "*** to set the libexecdir and not make libexecdir=/foo/bar or the like."; \ + echo "*** The same goes for prefix, libdir etc."; \ + echo "*** ${GST_PTP_HELPER_INSTALLED} != ${libexecdir}/gstreamer-$(GST_API_VERSION)/gst-ptp-helper"; \ + exit 1; \ + fi + +all-local: check-libexecdir-consistency + CLEANFILES = *.gcno *.gcda *.gcov %.c.gcov: .libs/libgstnet_@GST_API_VERSION@_la-%.gcda %.c diff --git a/libs/gst/net/gstptp_private.h b/libs/gst/net/gstptp_private.h new file mode 100644 index 000000000..18e0e07b3 --- /dev/null +++ b/libs/gst/net/gstptp_private.h @@ -0,0 +1,19 @@ +#ifndef __GST_PTP_PRIVATE_H__ +#define __GST_PTP_PRIVATE_H__ + +#include <glib.h> + +enum +{ + TYPE_EVENT, + TYPE_GENERAL, + TYPE_CLOCK_ID +}; + +typedef struct +{ + guint16 size; + guint8 type; +} StdIOHeader; + +#endif /* __GST_PTP_PRIVATE_H__ */ diff --git a/libs/gst/net/gstptpclock.c b/libs/gst/net/gstptpclock.c new file mode 100644 index 000000000..b4fc7e405 --- /dev/null +++ b/libs/gst/net/gstptpclock.c @@ -0,0 +1,2431 @@ +/* GStreamer + * Copyright (C) 2015 Sebastian Dröge <sebastian@centricular.com> + * + * + * 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., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ +/** + * SECTION:gstptpclock + * @short_description: Special clock that synchronizes to a remote time + * provider via PTP (IEEE1588:2008). + * @see_also: #GstClock, #GstNetClientClock, #GstPipeline + * + * GstPtpClock implements a PTP (IEEE1588:2008) ordinary clock in slave-only + * mode, that allows a GStreamer pipeline to synchronize to a PTP network + * clock in some specific domain. + * + * The PTP subsystem can be initialized with gst_ptp_init(), which then starts + * a helper process to do the actual communication via the PTP ports. This is + * required as PTP listens on ports < 1024 and thus requires special + * privileges. Once this helper process is started, the main process will + * synchronize to all PTP domains that are detected on the selected + * interfaces. + * + * gst_ptp_clock_new() then allows to create a GstClock that provides the PTP + * time from a master clock inside a specific PTP domain. This clock will only + * return valid timestamps once the timestamps in the PTP domain are known. To + * check this, the GstPtpClock::internal-clock property and the related + * notify::clock signal can be used. Once the internal clock is not NULL, the + * PTP domain's time is known. Alternatively you can wait for this with + * gst_ptp_clock_wait_ready(). + * + * + * To gather statistics about the PTP clock synchronization, + * gst_ptp_statistics_callback_add() can be used. This gives the application + * the possibility to collect all kinds of statistics from the clock + * synchronization. + * + * Since: 1.6 + * + */ +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "gstptpclock.h" + +#ifdef HAVE_PTP + +#include "gstptp_private.h" + +#include <sys/wait.h> +#include <sys/types.h> +#include <unistd.h> + +#include <gst/base/base.h> + +GST_DEBUG_CATEGORY_STATIC (ptp_debug); +#define GST_CAT_DEFAULT (ptp_debug) + +/* IEEE 1588 7.7.3.1 */ +#define PTP_ANNOUNCE_RECEIPT_TIMEOUT 4 + +#define MAX_SKIPPED_UPDATES 5 + +typedef enum +{ + PTP_MESSAGE_TYPE_SYNC = 0x0, + PTP_MESSAGE_TYPE_DELAY_REQ = 0x1, + PTP_MESSAGE_TYPE_PDELAY_REQ = 0x2, + PTP_MESSAGE_TYPE_PDELAY_RESP = 0x3, + PTP_MESSAGE_TYPE_FOLLOW_UP = 0x8, + PTP_MESSAGE_TYPE_DELAY_RESP = 0x9, + PTP_MESSAGE_TYPE_PDELAY_RESP_FOLLOW_UP = 0xA, + PTP_MESSAGE_TYPE_ANNOUNCE = 0xB, + PTP_MESSAGE_TYPE_SIGNALING = 0xC, + PTP_MESSAGE_TYPE_MANAGEMENT = 0xD +} PtpMessageType; + +typedef struct +{ + guint64 seconds_field; /* 48 bits valid */ + guint32 nanoseconds_field; +} PtpTimestamp; + +#define PTP_TIMESTAMP_TO_GST_CLOCK_TIME(ptp) (ptp.seconds_field * GST_SECOND + ptp.nanoseconds_field) +#define GST_CLOCK_TIME_TO_PTP_TIMESTAMP_SECONDS(gst) (((GstClockTime) gst) / GST_SECOND) +#define GST_CLOCK_TIME_TO_PTP_TIMESTAMP_NANOSECONDS(gst) (((GstClockTime) gst) % GST_SECOND) + +typedef struct +{ + guint64 clock_identity; + guint16 port_number; +} PtpClockIdentity; + +static gint +compare_clock_identity (const PtpClockIdentity * a, const PtpClockIdentity * b) +{ + if (a->clock_identity < b->clock_identity) + return -1; + else if (a->clock_identity > b->clock_identity) + return 1; + + if (a->port_number < b->port_number) + return -1; + else if (a->port_number > b->port_number) + return 1; + + return 0; +} + +typedef struct +{ + guint8 clock_class; + guint8 clock_accuracy; + guint16 offset_scaled_log_variance; +} PtpClockQuality; + +typedef struct +{ + guint8 transport_specific; + PtpMessageType message_type; + /* guint8 reserved; */ + guint8 version_ptp; + guint16 message_length; + guint8 domain_number; + /* guint8 reserved; */ + guint16 flag_field; + gint64 correction_field; /* 48.16 fixed point nanoseconds */ + /* guint32 reserved; */ + PtpClockIdentity source_port_identity; + guint16 sequence_id; + guint8 control_field; + gint8 log_message_interval; + + union + { + struct + { + PtpTimestamp origin_timestamp; + gint16 current_utc_offset; + /* guint8 reserved; */ + guint8 grandmaster_priority_1; + PtpClockQuality grandmaster_clock_quality; + guint8 grandmaster_priority_2; + guint64 grandmaster_identity; + guint16 steps_removed; + guint8 time_source; + } announce; + + struct + { + PtpTimestamp origin_timestamp; + } sync; + + struct + { + PtpTimestamp precise_origin_timestamp; + } follow_up; + + struct + { + PtpTimestamp origin_timestamp; + } delay_req; + + struct + { + PtpTimestamp receive_timestamp; + PtpClockIdentity requesting_port_identity; + } delay_resp; + + } message_specific; +} PtpMessage; + +static GMutex ptp_lock; +static GCond ptp_cond; +static gboolean initted = FALSE; +static gboolean supported = TRUE; +static GPid ptp_helper_pid; +static GThread *ptp_helper_thread; +static GMainContext *main_context; +static GMainLoop *main_loop; +static GIOChannel *stdin_channel, *stdout_channel; +static GRand *delay_req_rand; +static PtpClockIdentity ptp_clock_id = { GST_PTP_CLOCK_ID_NONE, 0 }; + +typedef struct +{ + GstClockTime receive_time; + + PtpClockIdentity master_clock_identity; + + guint8 grandmaster_priority_1; + PtpClockQuality grandmaster_clock_quality; + guint8 grandmaster_priority_2; + guint64 grandmaster_identity; + guint16 steps_removed; + guint8 time_source; + + guint16 sequence_id; +} PtpAnnounceMessage; + +typedef struct +{ + PtpClockIdentity master_clock_identity; + + GstClockTime announce_interval; /* last interval we received */ + GQueue announce_messages; +} PtpAnnounceSender; + +typedef struct +{ + guint domain; + PtpClockIdentity master_clock_identity; + + guint16 sync_seqnum; + GstClockTime sync_recv_time_local; /* t2 */ + GstClockTime sync_send_time_remote; /* t1, might be -1 if FOLLOW_UP pending */ + GstClockTime follow_up_recv_time_local; + + GSource *timeout_source; + guint16 delay_req_seqnum; + GstClockTime delay_req_send_time_local; /* t3, -1 if we wait for FOLLOW_UP */ + GstClockTime delay_req_recv_time_remote; /* t4, -1 if we wait */ + GstClockTime delay_resp_recv_time_local; + + gint64 correction_field_sync; /* sum of the correction fields of SYNC/FOLLOW_UP */ + gint64 correction_field_delay; /* sum of the correction fields of DELAY_RESP */ +} PtpPendingSync; + +static void +ptp_pending_sync_free (PtpPendingSync * sync) +{ + if (sync->timeout_source) + g_source_destroy (sync->timeout_source); + g_free (sync); +} + +typedef struct +{ + guint domain; + + GstClockTime last_ptp_time; + GstClockTime last_local_time; + gint skipped_updates; + + /* Used for selecting the master/grandmaster */ + GList *announce_senders; + + /* Last selected master clock */ + gboolean have_master_clock; + PtpClockIdentity master_clock_identity; + guint64 grandmaster_identity; + + /* Last SYNC or FOLLOW_UP timestamp we received */ + GstClockTime last_ptp_sync_time; + GstClockTime sync_interval; + + GstClockTime mean_path_delay; + GstClockTime last_delay_req, min_delay_req_interval; + guint16 last_delay_req_seqnum; + + GQueue pending_syncs; + + GstClock *domain_clock; +} PtpDomainData; + +static GList *domain_data; +static GMutex domain_clocks_lock; +static GList *domain_clocks; + +/* Protected by PTP lock */ +static void emit_ptp_statistics (guint8 domain, const GstStructure * stats); +static GHookList domain_stats_hooks; +static gint domain_stats_n_hooks; +static gboolean domain_stats_hooks_initted = FALSE; + +/* Converts log2 seconds to GstClockTime */ +static GstClockTime +log2_to_clock_time (gint l) +{ + if (l < 0) + return GST_SECOND >> (-l); + else + return GST_SECOND << l; +} + +static void +dump_ptp_message (PtpMessage * msg) +{ + GST_TRACE ("PTP message:"); + GST_TRACE ("\ttransport_specific: %u", msg->transport_specific); + GST_TRACE ("\tmessage_type: 0x%01x", msg->message_type); + GST_TRACE ("\tversion_ptp: %u", msg->version_ptp); + GST_TRACE ("\tmessage_length: %u", msg->message_length); + GST_TRACE ("\tdomain_number: %u", msg->domain_number); + GST_TRACE ("\tflag_field: 0x%04x", msg->flag_field); + GST_TRACE ("\tcorrection_field: %" G_GINT64_FORMAT ".%03u", + (msg->correction_field / 65536), + (guint) ((msg->correction_field & 0xffff) * 1000) / 65536); + GST_TRACE ("\tsource_port_identity: 0x%016" G_GINT64_MODIFIER "x %u", + msg->source_port_identity.clock_identity, + msg->source_port_identity.port_number); + GST_TRACE ("\tsequence_id: %u", msg->sequence_id); + GST_TRACE ("\tcontrol_field: 0x%02x", msg->control_field); + GST_TRACE ("\tmessage_interval: %" GST_TIME_FORMAT, + GST_TIME_ARGS (log2_to_clock_time (msg->log_message_interval))); + + switch (msg->message_type) { + case PTP_MESSAGE_TYPE_ANNOUNCE: + GST_TRACE ("\tANNOUNCE:"); + GST_TRACE ("\t\torigin_timestamp: %" G_GUINT64_FORMAT ".%09u", + msg->message_specific.announce.origin_timestamp.seconds_field, + msg->message_specific.announce.origin_timestamp.nanoseconds_field); + GST_TRACE ("\t\tcurrent_utc_offset: %d", + msg->message_specific.announce.current_utc_offset); + GST_TRACE ("\t\tgrandmaster_priority_1: %u", + msg->message_specific.announce.grandmaster_priority_1); + GST_TRACE ("\t\tgrandmaster_clock_quality: 0x%02x 0x%02x %u", + msg->message_specific.announce.grandmaster_clock_quality.clock_class, + msg->message_specific.announce. + grandmaster_clock_quality.clock_accuracy, + msg->message_specific.announce. + grandmaster_clock_quality.offset_scaled_log_variance); + GST_TRACE ("\t\tgrandmaster_priority_2: %u", + msg->message_specific.announce.grandmaster_priority_2); + GST_TRACE ("\t\tgrandmaster_identity: 0x%016" G_GINT64_MODIFIER "x", + msg->message_specific.announce.grandmaster_identity); + GST_TRACE ("\t\tsteps_removed: %u", + msg->message_specific.announce.steps_removed); + GST_TRACE ("\t\ttime_source: 0x%02x", + msg->message_specific.announce.time_source); + break; + case PTP_MESSAGE_TYPE_SYNC: + GST_TRACE ("\tSYNC:"); + GST_TRACE ("\t\torigin_timestamp: %" G_GUINT64_FORMAT ".%09u", + msg->message_specific.sync.origin_timestamp.seconds_field, + msg->message_specific.sync.origin_timestamp.nanoseconds_field); + break; + case PTP_MESSAGE_TYPE_FOLLOW_UP: + GST_TRACE ("\tFOLLOW_UP:"); + GST_TRACE ("\t\tprecise_origin_timestamp: %" G_GUINT64_FORMAT ".%09u", + msg->message_specific.follow_up. + precise_origin_timestamp.seconds_field, + msg->message_specific.follow_up. + precise_origin_timestamp.nanoseconds_field); + break; + case PTP_MESSAGE_TYPE_DELAY_REQ: + GST_TRACE ("\tDELAY_REQ:"); + GST_TRACE ("\t\torigin_timestamp: %" G_GUINT64_FORMAT ".%09u", + msg->message_specific.delay_req.origin_timestamp.seconds_field, + msg->message_specific.delay_req.origin_timestamp.nanoseconds_field); + break; + case PTP_MESSAGE_TYPE_DELAY_RESP: + GST_TRACE ("\tDELAY_RESP:"); + GST_TRACE ("\t\treceive_timestamp: %" G_GUINT64_FORMAT ".%09u", + msg->message_specific.delay_resp.receive_timestamp.seconds_field, + msg->message_specific.delay_resp.receive_timestamp.nanoseconds_field); + GST_TRACE ("\t\trequesting_port_identity: 0x%016" G_GINT64_MODIFIER + "x %u", + msg->message_specific.delay_resp. + requesting_port_identity.clock_identity, + msg->message_specific.delay_resp. + requesting_port_identity.port_number); + break; + default: + break; + } + GST_TRACE (" "); +} + +/* IEEE 1588-2008 5.3.3 */ +static gboolean +parse_ptp_timestamp (PtpTimestamp * timestamp, GstByteReader * reader) +{ + g_return_val_if_fail (gst_byte_reader_get_remaining (reader) >= 10, FALSE); + + timestamp->seconds_field = + (((guint64) gst_byte_reader_get_uint32_be_unchecked (reader)) << 16) | + gst_byte_reader_get_uint16_be_unchecked (reader); + timestamp->nanoseconds_field = + gst_byte_reader_get_uint32_be_unchecked (reader); + + if (timestamp->nanoseconds_field >= 1000000000) + return FALSE; + + return TRUE; +} + +/* IEEE 1588-2008 13.3 */ +static gboolean +parse_ptp_message_header (PtpMessage * msg, GstByteReader * reader) +{ + guint8 b; + + g_return_val_if_fail (gst_byte_reader_get_remaining (reader) >= 34, FALSE); + + b = gst_byte_reader_get_uint8_unchecked (reader); + msg->transport_specific = b >> 4; + msg->message_type = b & 0x0f; + + b = gst_byte_reader_get_uint8_unchecked (reader); + msg->version_ptp = b & 0x0f; + if (msg->version_ptp != 2) { + GST_WARNING ("Unsupported PTP message version (%u != 2)", msg->version_ptp); + return FALSE; + } + + msg->message_length = gst_byte_reader_get_uint16_be_unchecked (reader); + if (gst_byte_reader_get_remaining (reader) + 4 < msg->message_length) { + GST_WARNING ("Not enough data (%u < %u)", + gst_byte_reader_get_remaining (reader) + 4, msg->message_length); + return FALSE; + } + + msg->domain_number = gst_byte_reader_get_uint8_unchecked (reader); + gst_byte_reader_skip_unchecked (reader, 1); + + msg->flag_field = gst_byte_reader_get_uint16_be_unchecked (reader); + msg->correction_field = gst_byte_reader_get_uint64_be_unchecked (reader); + gst_byte_reader_skip_unchecked (reader, 4); + + msg->source_port_identity.clock_identity = + gst_byte_reader_get_uint64_be_unchecked (reader); + msg->source_port_identity.port_number = + gst_byte_reader_get_uint16_be_unchecked (reader); + + msg->sequence_id = gst_byte_reader_get_uint16_be_unchecked (reader); + msg->control_field = gst_byte_reader_get_uint8_unchecked (reader); + msg->log_message_interval = gst_byte_reader_get_uint8_unchecked (reader); + + return TRUE; +} + +/* IEEE 1588-2008 13.5 */ +static gboolean +parse_ptp_message_announce (PtpMessage * msg, GstByteReader * reader) +{ + g_return_val_if_fail (msg->message_type == PTP_MESSAGE_TYPE_ANNOUNCE, FALSE); + + if (gst_byte_reader_get_remaining (reader) < 20) + return FALSE; + + if (!parse_ptp_timestamp (&msg->message_specific.announce.origin_timestamp, + reader)) + return FALSE; + + msg->message_specific.announce.current_utc_offset = + gst_byte_reader_get_uint16_be_unchecked (reader); + gst_byte_reader_skip_unchecked (reader, 1); + + msg->message_specific.announce.grandmaster_priority_1 = + gst_byte_reader_get_uint8_unchecked (reader); + msg->message_specific.announce.grandmaster_clock_quality.clock_class = + gst_byte_reader_get_uint8_unchecked (reader); + msg->message_specific.announce.grandmaster_clock_quality.clock_accuracy = + gst_byte_reader_get_uint8_unchecked (reader); + msg->message_specific.announce. + grandmaster_clock_quality.offset_scaled_log_variance = + gst_byte_reader_get_uint16_be_unchecked (reader); + msg->message_specific.announce.grandmaster_priority_2 = + gst_byte_reader_get_uint8_unchecked (reader); + msg->message_specific.announce.grandmaster_identity = + gst_byte_reader_get_uint64_be_unchecked (reader); + msg->message_specific.announce.steps_removed = + gst_byte_reader_get_uint16_be_unchecked (reader); + msg->message_specific.announce.time_source = + gst_byte_reader_get_uint8_unchecked (reader); + + return TRUE; +} + +/* IEEE 1588-2008 13.6 */ +static gboolean +parse_ptp_message_sync (PtpMessage * msg, GstByteReader * reader) +{ + g_return_val_if_fail (msg->message_type == PTP_MESSAGE_TYPE_SYNC, FALSE); + + if (gst_byte_reader_get_remaining (reader) < 10) + return FALSE; + + if (!parse_ptp_timestamp (&msg->message_specific.sync.origin_timestamp, + reader)) + return FALSE; + + return TRUE; +} + +/* IEEE 1588-2008 13.6 */ +static gboolean +parse_ptp_message_delay_req (PtpMessage * msg, GstByteReader * reader) +{ + g_return_val_if_fail (msg->message_type == PTP_MESSAGE_TYPE_DELAY_REQ, FALSE); + + if (gst_byte_reader_get_remaining (reader) < 10) + return FALSE; + + if (!parse_ptp_timestamp (&msg->message_specific.delay_req.origin_timestamp, + reader)) + return FALSE; + + return TRUE; +} + +/* IEEE 1588-2008 13.7 */ +static gboolean +parse_ptp_message_follow_up (PtpMessage * msg, GstByteReader * reader) +{ + g_return_val_if_fail (msg->message_type == PTP_MESSAGE_TYPE_FOLLOW_UP, FALSE); + + if (gst_byte_reader_get_remaining (reader) < 10) + return FALSE; + + if (!parse_ptp_timestamp (&msg->message_specific. + follow_up.precise_origin_timestamp, reader)) + return FALSE; + + return TRUE; +} + +/* IEEE 1588-2008 13.8 */ +static gboolean +parse_ptp_message_delay_resp (PtpMessage * msg, GstByteReader * reader) +{ + g_return_val_if_fail (msg->message_type == PTP_MESSAGE_TYPE_DELAY_RESP, + FALSE); + + if (gst_byte_reader_get_remaining (reader) < 20) + return FALSE; + + if (!parse_ptp_timestamp (&msg->message_specific.delay_resp.receive_timestamp, + reader)) + return FALSE; + + msg->message_specific.delay_resp.requesting_port_identity.clock_identity = + gst_byte_reader_get_uint64_be_unchecked (reader); + msg->message_specific.delay_resp.requesting_port_identity.port_number = + gst_byte_reader_get_uint16_be_unchecked (reader); + + return TRUE; +} + +static gboolean +parse_ptp_message (PtpMessage * msg, const guint8 * data, gsize size) +{ + GstByteReader reader; + gboolean ret = FALSE; + + gst_byte_reader_init (&reader, data, size); + + if (!parse_ptp_message_header (msg, &reader)) { + GST_WARNING ("Failed to parse PTP message header"); + return FALSE; + } + + switch (msg->message_type) { + case PTP_MESSAGE_TYPE_SYNC: + ret = parse_ptp_message_sync (msg, &reader); + break; + case PTP_MESSAGE_TYPE_FOLLOW_UP: + ret = parse_ptp_message_follow_up (msg, &reader); + break; + case PTP_MESSAGE_TYPE_DELAY_REQ: + ret = parse_ptp_message_delay_req (msg, &reader); + break; + case PTP_MESSAGE_TYPE_DELAY_RESP: + ret = parse_ptp_message_delay_resp (msg, &reader); + break; + case PTP_MESSAGE_TYPE_ANNOUNCE: + ret = parse_ptp_message_announce (msg, &reader); + break; + default: + /* ignore for now */ + break; + } + + return ret; +} + +static gint +compare_announce_message (const PtpAnnounceMessage * a, + const PtpAnnounceMessage * b) +{ + /* IEEE 1588 Figure 27 */ + if (a->grandmaster_identity == b->grandmaster_identity) { + if (a->steps_removed + 1 < b->steps_removed) + return -1; + else if (a->steps_removed > b->steps_removed + 1) + return 1; + + /* Error cases are filtered out earlier */ + if (a->steps_removed < b->steps_removed) + return -1; + else if (a->steps_removed > b->steps_removed) + return 1; + + /* Error cases are filtered out earlier */ + if (a->master_clock_identity.clock_identity < + b->master_clock_identity.clock_identity) + return -1; + else if (a->master_clock_identity.clock_identity > + b->master_clock_identity.clock_identity) + return 1; + + /* Error cases are filtered out earlier */ + if (a->master_clock_identity.port_number < + b->master_clock_identity.port_number) + return -1; + else if (a->master_clock_identity.port_number > + b->master_clock_identity.port_number) + return 1; + else + g_assert_not_reached (); + + return 0; + } + + if (a->grandmaster_priority_1 < b->grandmaster_priority_1) + return -1; + else if (a->grandmaster_priority_1 > b->grandmaster_priority_1) + return 1; + + if (a->grandmaster_clock_quality.clock_class < + b->grandmaster_clock_quality.clock_class) + return -1; + else if (a->grandmaster_clock_quality.clock_class > + b->grandmaster_clock_quality.clock_class) + return 1; + + if (a->grandmaster_clock_quality.clock_accuracy < + b->grandmaster_clock_quality.clock_accuracy) + return -1; + else if (a->grandmaster_clock_quality.clock_accuracy > + b->grandmaster_clock_quality.clock_accuracy) + return 1; + + if (a->grandmaster_clock_quality.offset_scaled_log_variance < + b->grandmaster_clock_quality.offset_scaled_log_variance) + return -1; + else if (a->grandmaster_clock_quality.offset_scaled_log_variance > + b->grandmaster_clock_quality.offset_scaled_log_variance) + return 1; + + if (a->grandmaster_priority_2 < b->grandmaster_priority_2) + return -1; + else if (a->grandmaster_priority_2 > b->grandmaster_priority_2) + return 1; + + if (a->grandmaster_identity < b->grandmaster_identity) + return -1; + else if (a->grandmaster_identity > b->grandmaster_identity) + return 1; + else + g_assert_not_reached (); + + return 0; +} + +static void +select_best_master_clock (PtpDomainData * domain, GstClockTime now) +{ + GList *qualified_messages = NULL; + GList *l, *m; + PtpAnnounceMessage *best = NULL; + + /* IEEE 1588 9.3.2.5 */ + for (l = domain->announce_senders; l; l = l->next) { + PtpAnnounceSender *sender = l->data; + GstClockTime window = 4 * sender->announce_interval; + gint count = 0; + + for (m = sender->announce_messages.head; m; m = m->next) { + PtpAnnounceMessage *msg = m->data; + + if (now - msg->receive_time <= window) + count++; + } + + /* Only include the newest message of announce senders that had at least 2 + * announce messages in the last 4 announce intervals. Which also means + * that we wait at least 4 announce intervals before we select a master + * clock. Until then we just report based on the newest SYNC we received + */ + if (count >= 2) { + qualified_messages = + g_list_prepend (qualified_messages, + g_queue_peek_tail (&sender->announce_messages)); + } + } + + if (!qualified_messages) { + GST_DEBUG + ("No qualified announce messages for domain %u, can't select a master clock", + domain->domain); + domain->have_master_clock = FALSE; + return; + } + + for (l = qualified_messages; l; l = l->next) { + PtpAnnounceMessage *msg = l->data; + + if (!best || compare_announce_message (msg, best) < 0) + best = msg; + } + + if (domain->have_master_clock + && compare_clock_identity (&domain->master_clock_identity, + &best->master_clock_identity) == 0) { + GST_DEBUG ("Master clock in domain %u did not change", domain->domain); + } else { + GST_DEBUG ("Selected master clock for domain %u: 0x%016" G_GINT64_MODIFIER + "x %u with grandmaster clock 0x%016" G_GINT64_MODIFIER "x", + domain->domain, best->master_clock_identity.clock_identity, + best->master_clock_identity.port_number, best->grandmaster_identity); + + domain->have_master_clock = TRUE; + domain->grandmaster_identity = best->grandmaster_identity; + + /* Opportunistic master clock selection likely gave us the same master + * clock before, no need to reset all statistics */ + if (compare_clock_identity (&domain->master_clock_identity, + &best->master_clock_identity) != 0) { + memcpy (&domain->master_clock_identity, &best->master_clock_identity, + sizeof (PtpClockIdentity)); + domain->mean_path_delay = 0; + domain->last_delay_req = 0; + domain->min_delay_req_interval = 0; + domain->sync_interval = 0; + domain->last_ptp_sync_time = 0; + domain->skipped_updates = 0; + g_queue_foreach (&domain->pending_syncs, (GFunc) ptp_pending_sync_free, + NULL); + g_queue_clear (&domain->pending_syncs); + } + + if (g_atomic_int_get (&domain_stats_n_hooks)) { + GstStructure *stats = + gst_structure_new (GST_PTP_STATISTICS_BEST_MASTER_CLOCK_SELECTED, + "domain", G_TYPE_UINT, domain->domain, + "master-clock-id", G_TYPE_UINT64, + domain->master_clock_identity.clock_identity, + "master-clock-port", G_TYPE_UINT, + domain->master_clock_identity.port_number, + "grandmaster-clock-id", G_TYPE_UINT64, domain->grandmaster_identity, + NULL); + emit_ptp_statistics (domain->domain, stats); + gst_structure_free (stats); + } + } +} + +static void +handle_announce_message (PtpMessage * msg, GstClockTime receive_time) +{ + GList *l; + PtpDomainData *domain = NULL; + PtpAnnounceSender *sender = NULL; + PtpAnnounceMessage *announce; + + /* IEEE1588 9.3.2.2 e) + * Don't consider messages with the alternate master flag set + */ + if ((msg->flag_field & 0x0100)) + return; + + /* IEEE 1588 9.3.2.5 d) + * Don't consider announce messages with steps_removed>=255 + */ + if (msg->message_specific.announce.steps_removed >= 255) + return; + + for (l = domain_data; l; l = l->next) { + PtpDomainData *tmp = l->data; + + if (tmp->domain == msg->domain_number) { + domain = tmp; + break; + } + } + + if (!domain) { + gchar *clock_name; + + domain = g_new0 (PtpDomainData, 1); + domain->domain = msg->domain_number; + clock_name = g_strdup_printf ("ptp-clock-%u", domain->domain); + domain->domain_clock = + g_object_new (GST_TYPE_SYSTEM_CLOCK, "name", clock_name, NULL); + g_free (clock_name); + g_queue_init (&domain->pending_syncs); + domain_data = g_list_prepend (domain_data, domain); + + g_mutex_lock (&domain_clocks_lock); + domain_clocks = g_list_prepend (domain_clocks, domain); + g_mutex_unlock (&domain_clocks_lock); + + if (g_atomic_int_get (&domain_stats_n_hooks)) { + GstStructure *stats = + gst_structure_new (GST_PTP_STATISTICS_NEW_DOMAIN_FOUND, "domain", + G_TYPE_UINT, domain->domain, "clock", GST_TYPE_CLOCK, + domain->domain_clock, NULL); + emit_ptp_statistics (domain->domain, stats); + gst_structure_free (stats); + } + } + + for (l = domain->announce_senders; l; l = l->next) { + PtpAnnounceSender *tmp = l->data; + + if (compare_clock_identity (&tmp->master_clock_identity, + &msg->source_port_identity) == 0) { + sender = tmp; + break; + } + } + + if (!sender) { + sender = g_new0 (PtpAnnounceSender, 1); + + memcpy (&sender->master_clock_identity, &msg->source_port_identity, + sizeof (PtpClockIdentity)); + g_queue_init (&sender->announce_messages); + domain->announce_senders = + g_list_prepend (domain->announce_senders, sender); + } + + for (l = sender->announce_messages.head; l; l = l->next) { + PtpAnnounceMessage *tmp = l->data; + + /* IEEE 1588 9.3.2.5 c) + * Don't consider identical messages, i.e. duplicates + */ + if (tmp->sequence_id == msg->sequence_id) + return; + } + + sender->announce_interval = log2_to_clock_time (msg->log_message_interval); + + announce = g_new0 (PtpAnnounceMessage, 1); + announce->receive_time = receive_time; + announce->sequence_id = msg->sequence_id; + memcpy (&announce->master_clock_identity, &msg->source_port_identity, + sizeof (PtpClockIdentity)); + announce->grandmaster_identity = + msg->message_specific.announce.grandmaster_identity; + announce->grandmaster_priority_1 = + msg->message_specific.announce.grandmaster_priority_1; + announce->grandmaster_clock_quality.clock_class = + msg->message_specific.announce.grandmaster_clock_quality.clock_class; + announce->grandmaster_clock_quality.clock_accuracy = + msg->message_specific.announce.grandmaster_clock_quality.clock_accuracy; + announce->grandmaster_clock_quality.offset_scaled_log_variance = + msg->message_specific.announce. + grandmaster_clock_quality.offset_scaled_log_variance; + announce->grandmaster_priority_2 = + msg->message_specific.announce.grandmaster_priority_2; + announce->steps_removed = msg->message_specific.announce.steps_removed; + announce->time_source = msg->message_specific.announce.time_source; + g_queue_push_tail (&sender->announce_messages, announce); + + select_best_master_clock (domain, receive_time); +} + +static gboolean +send_delay_req_timeout (PtpPendingSync * sync) +{ + StdIOHeader header = { 0, }; + guint8 delay_req[44]; + GstByteWriter writer; + GIOStatus status; + gsize written; + GError *err = NULL; + + header.type = TYPE_EVENT; + header.size = 44; + + gst_byte_writer_init_with_data (&writer, delay_req, 44, FALSE); + gst_byte_writer_put_uint8_unchecked (&writer, PTP_MESSAGE_TYPE_DELAY_REQ); + gst_byte_writer_put_uint8_unchecked (&writer, 2); + gst_byte_writer_put_uint16_be_unchecked (&writer, 44); + gst_byte_writer_put_uint8_unchecked (&writer, sync->domain); + gst_byte_writer_put_uint8_unchecked (&writer, 0); + gst_byte_writer_put_uint16_be_unchecked (&writer, 0); + gst_byte_writer_put_uint64_be_unchecked (&writer, 0); + gst_byte_writer_put_uint32_be_unchecked (&writer, 0); + gst_byte_writer_put_uint64_be_unchecked (&writer, + ptp_clock_id.clock_identity); + gst_byte_writer_put_uint16_be_unchecked (&writer, ptp_clock_id.port_number); + gst_byte_writer_put_uint16_be_unchecked (&writer, sync->delay_req_seqnum); + gst_byte_writer_put_uint8_unchecked (&writer, 0x01); + gst_byte_writer_put_uint8_unchecked (&writer, 0x7f); + gst_byte_writer_put_uint64_be_unchecked (&writer, 0); + gst_byte_writer_put_uint16_be_unchecked (&writer, 0); + + status = + g_io_channel_write_chars (stdout_channel, (gchar *) & header, + sizeof (header), &written, &err); + if (status == G_IO_STATUS_ERROR) { + g_warning ("Failed to write to stdout: %s", err->message); + return G_SOURCE_REMOVE; + } else if (status == G_IO_STATUS_EOF) { + g_message ("EOF on stdout"); + g_main_loop_quit (main_loop); + return G_SOURCE_REMOVE; + } else if (status != G_IO_STATUS_NORMAL) { + g_warning ("Unexpected stdout write status: %d", status); + g_main_loop_quit (main_loop); + return G_SOURCE_REMOVE; + } else if (written != sizeof (header)) { + g_warning ("Unexpected write size: %" G_GSIZE_FORMAT, written); + g_main_loop_quit (main_loop); + return G_SOURCE_REMOVE; + } + + sync->delay_req_send_time_local = gst_util_get_timestamp (); + + status = + g_io_channel_write_chars (stdout_channel, + (const gchar *) delay_req, 44, &written, &err); + if (status == G_IO_STATUS_ERROR) { + g_warning ("Failed to write to stdout: %s", err->message); + g_main_loop_quit (main_loop); + return G_SOURCE_REMOVE; + } else if (status == G_IO_STATUS_EOF) { + g_message ("EOF on stdout"); + g_main_loop_quit (main_loop); + return G_SOURCE_REMOVE; + } else if (status != G_IO_STATUS_NORMAL) { + g_warning ("Unexpected stdout write status: %d", status); + g_main_loop_quit (main_loop); + return G_SOURCE_REMOVE; + } else if (written != 44) { + g_warning ("Unexpected write size: %" G_GSIZE_FORMAT, written); + g_main_loop_quit (main_loop); + return G_SOURCE_REMOVE; + } + + return G_SOURCE_REMOVE; +} + +static gboolean +send_delay_req (PtpDomainData * domain, PtpPendingSync * sync) +{ + GstClockTime now = gst_util_get_timestamp (); + guint timeout; + GSource *timeout_source; + + if (domain->last_delay_req != 0 + && domain->last_delay_req + domain->min_delay_req_interval > now) + return FALSE; + + domain->last_delay_req = now; + sync->delay_req_seqnum = domain->last_delay_req_seqnum++; + + /* IEEE 1588 9.5.11.2 */ + if (domain->last_delay_req == 0 || domain->min_delay_req_interval == 0) + timeout = 0; + else + timeout = + g_rand_int_range (delay_req_rand, 0, + (domain->min_delay_req_interval * 2) / GST_MSECOND); + + sync->timeout_source = timeout_source = g_timeout_source_new (timeout); + g_source_set_priority (timeout_source, G_PRIORITY_DEFAULT); + g_source_set_callback (timeout_source, (GSourceFunc) send_delay_req_timeout, + sync, NULL); + g_source_attach (timeout_source, main_context); + + return TRUE; +} + +/* Filtering of outliers for RTT and time calculations inspired + * by the code from gstnetclientclock.c + */ +static void +update_ptp_time (PtpDomainData * domain, PtpPendingSync * sync) +{ + GstClockTime internal_time, external_time, rate_num, rate_den; + GstClockTime orig_internal_time, orig_external_time, orig_rate_num, + orig_rate_den; + GstClockTime corrected_ptp_time, corrected_local_time; + GstClockTime max_discont, estimated_ptp_time_min, estimated_ptp_time_max; + gdouble r_squared; + gboolean synced, now_synced; + GstClockTimeDiff discont = 0; + GstClockTime estimated_ptp_time = GST_CLOCK_TIME_NONE, new_estimated_ptp_time; + + /* We check this here and when updating the mean path delay, because + * we can get here without a delay response too */ + if (sync->follow_up_recv_time_local != GST_CLOCK_TIME_NONE + && sync->follow_up_recv_time_local > + sync->sync_recv_time_local + 2 * domain->mean_path_delay) { + GST_WARNING ("Sync-follow-up delay for domain %u too big: %" GST_TIME_FORMAT + " > 2 * %" GST_TIME_FORMAT, domain->domain, + GST_TIME_ARGS (sync->follow_up_recv_time_local), + GST_TIME_ARGS (domain->mean_path_delay)); + goto out; + } + + /* IEEE 1588 11.2 */ + corrected_ptp_time = + sync->sync_send_time_remote + + (sync->correction_field_sync + 32768) / 65536; + corrected_local_time = sync->sync_recv_time_local - domain->mean_path_delay; + + /* Set an initial local-remote relation */ + if (domain->last_ptp_time == 0) + gst_clock_set_calibration (domain->domain_clock, corrected_local_time, + corrected_ptp_time, 1, 1); + + /* Check if the corrected PTP time is +/- 3/4 RTT around what we would + * estimate with our present knowledge about the clock + */ + /* Store what the clock produced as 'now' before this update */ + gst_clock_get_calibration (GST_CLOCK_CAST (domain->domain_clock), + &orig_internal_time, &orig_external_time, &orig_rate_num, &orig_rate_den); + internal_time = orig_internal_time; + external_time = orig_external_time; + rate_num = orig_rate_num; + rate_den = orig_rate_den; + + /* 3/4 RTT window around the estimation */ + max_discont = domain->mean_path_delay * 3 / 2; + + /* Check if the estimated sync time is inside our window */ + estimated_ptp_time_min = corrected_local_time - max_discont; + estimated_ptp_time_min = + gst_clock_adjust_with_calibration (GST_CLOCK_CAST (domain->domain_clock), + estimated_ptp_time_min, internal_time, external_time, rate_num, rate_den); + estimated_ptp_time_max = corrected_local_time + max_discont; + estimated_ptp_time_max = + gst_clock_adjust_with_calibration (GST_CLOCK_CAST (domain->domain_clock), + estimated_ptp_time_max, internal_time, external_time, rate_num, rate_den); + + synced = (estimated_ptp_time_min < corrected_ptp_time + && corrected_ptp_time < estimated_ptp_time_max); + + GST_DEBUG ("Adding observation for domain %u: %" GST_TIME_FORMAT " - %" + GST_TIME_FORMAT, domain->domain, + GST_TIME_ARGS (corrected_ptp_time), GST_TIME_ARGS (corrected_local_time)); + + GST_DEBUG ("Synced %d: %" GST_TIME_FORMAT " < %" GST_TIME_FORMAT " < %" + GST_TIME_FORMAT, synced, GST_TIME_ARGS (estimated_ptp_time_min), + GST_TIME_ARGS (corrected_ptp_time), + GST_TIME_ARGS (estimated_ptp_time_max)); + + if (gst_clock_add_observation_unapplied (domain->domain_clock, + corrected_local_time, corrected_ptp_time, &r_squared, + &internal_time, &external_time, &rate_num, &rate_den)) { + GST_DEBUG ("Regression gave r_squared: %f", r_squared); + + /* Old estimated PTP time based on receive time and path delay */ + estimated_ptp_time = corrected_local_time; + estimated_ptp_time = + gst_clock_adjust_with_calibration (GST_CLOCK_CAST + (domain->domain_clock), estimated_ptp_time, orig_internal_time, + orig_external_time, orig_rate_num, orig_rate_den); + + /* New estimated PTP time based on receive time and path delay */ + new_estimated_ptp_time = corrected_local_time; + new_estimated_ptp_time = + gst_clock_adjust_with_calibration (GST_CLOCK_CAST + (domain->domain_clock), new_estimated_ptp_time, internal_time, + external_time, rate_num, rate_den); + + discont = GST_CLOCK_DIFF (estimated_ptp_time, new_estimated_ptp_time); + if (synced && ABS (discont) > max_discont) { + GstClockTimeDiff offset; + GST_DEBUG ("Too large a discont %s%" GST_TIME_FORMAT + ", clamping to 1/4 average RTT = %" GST_TIME_FORMAT, + (discont < 0 ? "-" : ""), GST_TIME_ARGS (ABS (discont)), + GST_TIME_ARGS (max_discont)); + if (discont > 0) { /* Too large a forward step - add a -ve offset */ + offset = max_discont - discont; + if (-offset > external_time) + external_time = 0; + else + external_time += offset; + } else { /* Too large a backward step - add a +ve offset */ + offset = -(max_discont + discont); + external_time += offset; + } + + discont += offset; + } else { + GST_DEBUG ("Discont %s%" GST_TIME_FORMAT " (max: %" GST_TIME_FORMAT ")", + (discont < 0 ? "-" : ""), GST_TIME_ARGS (ABS (discont)), + GST_TIME_ARGS (max_discont)); + } + + /* Check if the estimated sync time is now (still) inside our window */ + estimated_ptp_time_min = corrected_local_time - max_discont; + estimated_ptp_time_min = + gst_clock_adjust_with_calibration (GST_CLOCK_CAST + (domain->domain_clock), estimated_ptp_time_min, internal_time, + external_time, rate_num, rate_den); + estimated_ptp_time_max = corrected_local_time + max_discont; + estimated_ptp_time_max = + gst_clock_adjust_with_calibration (GST_CLOCK_CAST + (domain->domain_clock), estimated_ptp_time_max, internal_time, + external_time, rate_num, rate_den); + + now_synced = (estimated_ptp_time_min < corrected_ptp_time + && corrected_ptp_time < estimated_ptp_time_max); + + GST_DEBUG ("Now synced %d: %" GST_TIME_FORMAT " < %" GST_TIME_FORMAT " < %" + GST_TIME_FORMAT, now_synced, GST_TIME_ARGS (estimated_ptp_time_min), + GST_TIME_ARGS (corrected_ptp_time), + GST_TIME_ARGS (estimated_ptp_time_max)); + + if (synced || now_synced || domain->skipped_updates > MAX_SKIPPED_UPDATES) { + gst_clock_set_calibration (GST_CLOCK_CAST (domain->domain_clock), + internal_time, external_time, rate_num, rate_den); + domain->skipped_updates = 0; + + domain->last_ptp_time = corrected_ptp_time; + domain->last_local_time = corrected_local_time; + } else { + domain->skipped_updates++; + } + } else { + domain->last_ptp_time = corrected_ptp_time; + domain->last_local_time = corrected_local_time; + } + +out: + + if (g_atomic_int_get (&domain_stats_n_hooks)) { + GstStructure *stats = gst_structure_new (GST_PTP_STATISTICS_TIME_UPDATED, + "domain", G_TYPE_UINT, domain->domain, + "mean-path-delay-avg", GST_TYPE_CLOCK_TIME, domain->mean_path_delay, + "local-time", GST_TYPE_CLOCK_TIME, corrected_local_time, + "ptp-time", GST_TYPE_CLOCK_TIME, corrected_ptp_time, + "estimated-ptp-time", GST_TYPE_CLOCK_TIME, estimated_ptp_time, + "discontinuity", G_TYPE_INT64, discont, + "synced", G_TYPE_BOOLEAN, synced, + "r-squared", G_TYPE_DOUBLE, r_squared, + "internal-time", GST_TYPE_CLOCK_TIME, internal_time, + "external-time", GST_TYPE_CLOCK_TIME, external_time, + "rate-num", G_TYPE_UINT64, rate_num, + "rate-den", G_TYPE_UINT64, rate_den, + "rate", G_TYPE_DOUBLE, (gdouble) (rate_num) / rate_den, + NULL); + emit_ptp_statistics (domain->domain, stats); + gst_structure_free (stats); + } + +} + +static gboolean +update_mean_path_delay (PtpDomainData * domain, PtpPendingSync * sync) +{ + GstClockTime mean_path_delay, delay_req_delay; + gboolean ret; + + /* IEEE 1588 11.3 */ + mean_path_delay = + (sync->delay_req_recv_time_remote - sync->sync_send_time_remote + + sync->sync_recv_time_local - sync->delay_req_send_time_local - + (sync->correction_field_sync + sync->correction_field_delay + + 32768) / 65536) / 2; + + /* Track an average round trip time, for a bit of smoothing */ + /* Always update before discarding a sample, so genuine changes in + * the network get picked up, eventually */ + if (domain->mean_path_delay == 0) + domain->mean_path_delay = mean_path_delay; + else if (mean_path_delay < domain->mean_path_delay) /* Shorter RTTs carry more weight than longer */ + domain->mean_path_delay = + (3 * domain->mean_path_delay + mean_path_delay) / 4; + else + domain->mean_path_delay = + (15 * domain->mean_path_delay + mean_path_delay) / 16; + + if (sync->follow_up_recv_time_local != GST_CLOCK_TIME_NONE && + domain->mean_path_delay != 0 + && sync->follow_up_recv_time_local > + sync->sync_recv_time_local + 2 * domain->mean_path_delay) { + GST_WARNING ("Sync-follow-up delay for domain %u too big: %" GST_TIME_FORMAT + " > 2 * %" GST_TIME_FORMAT, domain->domain, + GST_TIME_ARGS (sync->follow_up_recv_time_local), + GST_TIME_ARGS (domain->mean_path_delay)); + ret = FALSE; + goto out; + } + + if (mean_path_delay > 2 * domain->mean_path_delay) { + GST_WARNING ("Mean path delay for domain %u too big: %" GST_TIME_FORMAT + " > 2 * %" GST_TIME_FORMAT, domain->domain, + GST_TIME_ARGS (mean_path_delay), + GST_TIME_ARGS (domain->mean_path_delay)); + ret = FALSE; + goto out; + } + + delay_req_delay = + sync->delay_resp_recv_time_local - sync->delay_req_send_time_local; + /* delay_req_delay is a RTT, so 2 times the path delay */ + if (delay_req_delay > 4 * domain->mean_path_delay) { + GST_WARNING ("Delay-request-response delay for domain %u too big: %" + GST_TIME_FORMAT " > 4 * %" GST_TIME_FORMAT, domain->domain, + GST_TIME_ARGS (delay_req_delay), + GST_TIME_ARGS (domain->mean_path_delay)); + ret = FALSE; + goto out; + } + + ret = TRUE; + + GST_DEBUG ("Got mean path delay for domain %u: %" GST_TIME_FORMAT " (new: %" + GST_TIME_FORMAT ")", domain->domain, + GST_TIME_ARGS (domain->mean_path_delay), GST_TIME_ARGS (mean_path_delay)); + GST_DEBUG ("Delay request delay for domain %u: %" GST_TIME_FORMAT, + domain->domain, GST_TIME_ARGS (delay_req_delay)); + +out: + if (g_atomic_int_get (&domain_stats_n_hooks)) { + GstStructure *stats = + gst_structure_new (GST_PTP_STATISTICS_PATH_DELAY_MEASURED, + "domain", G_TYPE_UINT, domain->domain, + "mean-path-delay-avg", GST_TYPE_CLOCK_TIME, domain->mean_path_delay, + "mean-path-delay", GST_TYPE_CLOCK_TIME, mean_path_delay, + "delay-request-delay", GST_TYPE_CLOCK_TIME, delay_req_delay, NULL); + emit_ptp_statistics (domain->domain, stats); + gst_structure_free (stats); + } + + return ret; +} + +static void +handle_sync_message (PtpMessage * msg, GstClockTime receive_time) +{ + GList *l; + PtpDomainData *domain = NULL; + PtpPendingSync *sync = NULL; + + /* Don't consider messages with the alternate master flag set */ + if ((msg->flag_field & 0x0100)) + return; + + for (l = domain_data; l; l = l->next) { + PtpDomainData *tmp = l->data; + + if (msg->domain_number == tmp->domain) { + domain = tmp; + break; + } + } + + if (!domain) { + gchar *clock_name; + domain = g_new0 (PtpDomainData, 1); + domain->domain = msg->domain_number; + clock_name = g_strdup_printf ("ptp-clock-%u", domain->domain); + domain->domain_clock = + g_object_new (GST_TYPE_SYSTEM_CLOCK, "name", clock_name, NULL); + g_free (clock_name); + g_queue_init (&domain->pending_syncs); + domain_data = g_list_prepend (domain_data, domain); + + g_mutex_lock (&domain_clocks_lock); + domain_clocks = g_list_prepend (domain_clocks, domain); + g_mutex_unlock (&domain_clocks_lock); + } + + /* If we have a master clock, ignore this message if it's not coming from there */ + if (domain->have_master_clock + && compare_clock_identity (&domain->master_clock_identity, + &msg->source_port_identity) != 0) + return; + + /* Opportunistic selection of master clock */ + if (!domain->have_master_clock) + memcpy (&domain->master_clock_identity, &msg->source_port_identity, + sizeof (PtpClockIdentity)); + + domain->sync_interval = log2_to_clock_time (msg->log_message_interval); + + /* Check if duplicated */ + for (l = domain->pending_syncs.head; l; l = l->next) { + PtpPendingSync *tmp = l->data; + + if (tmp->sync_seqnum == msg->sequence_id) + return; + } + + if (msg->message_specific.sync.origin_timestamp.seconds_field > + GST_CLOCK_TIME_NONE / GST_SECOND) { + GST_FIXME ("Unsupported sync message seconds field value: %" + G_GUINT64_FORMAT " > %" G_GUINT64_FORMAT, + msg->message_specific.sync.origin_timestamp.seconds_field, + GST_CLOCK_TIME_NONE / GST_SECOND); + return; + } + + sync = g_new0 (PtpPendingSync, 1); + sync->domain = domain->domain; + sync->sync_seqnum = msg->sequence_id; + sync->sync_recv_time_local = receive_time; + sync->sync_send_time_remote = GST_CLOCK_TIME_NONE; + sync->follow_up_recv_time_local = GST_CLOCK_TIME_NONE; + sync->delay_req_send_time_local = GST_CLOCK_TIME_NONE; + sync->delay_req_recv_time_remote = GST_CLOCK_TIME_NONE; + sync->delay_resp_recv_time_local = GST_CLOCK_TIME_NONE; + + /* 0.5 correction factor for division later */ + sync->correction_field_sync = msg->correction_field; + + if ((msg->flag_field & 0x0200)) { + /* Wait for FOLLOW_UP */ + } else { + sync->sync_send_time_remote = + PTP_TIMESTAMP_TO_GST_CLOCK_TIME (msg->message_specific. + sync.origin_timestamp); + + if (domain->last_ptp_sync_time != 0 + && domain->last_ptp_sync_time >= sync->sync_send_time_remote) { + GST_WARNING ("Backwards PTP times in domain %u: %" GST_TIME_FORMAT " >= %" + GST_TIME_FORMAT, domain->domain, + GST_TIME_ARGS (domain->last_ptp_sync_time), + GST_TIME_ARGS (sync->sync_send_time_remote)); + ptp_pending_sync_free (sync); + sync = NULL; + return; + } + domain->last_ptp_sync_time = sync->sync_send_time_remote; + + if (send_delay_req (domain, sync)) { + /* Sent delay request */ + } else { + update_ptp_time (domain, sync); + ptp_pending_sync_free (sync); + sync = NULL; + } + } + + if (sync) + g_queue_push_tail (&domain->pending_syncs, sync); +} + +static void +handle_follow_up_message (PtpMessage * msg, GstClockTime receive_time) +{ + GList *l; + PtpDomainData *domain = NULL; + PtpPendingSync *sync = NULL; + + /* Don't consider messages with the alternate master flag set */ + if ((msg->flag_field & 0x0100)) + return; + + for (l = domain_data; l; l = l->next) { + PtpDomainData *tmp = l->data; + + if (msg->domain_number == tmp->domain) { + domain = tmp; + break; + } + } + + if (!domain) + return; + + /* If we have a master clock, ignore this message if it's not coming from there */ + if (domain->have_master_clock + && compare_clock_identity (&domain->master_clock_identity, + &msg->source_port_identity) != 0) + return; + + /* Check if we know about this one */ + for (l = domain->pending_syncs.head; l; l = l->next) { + PtpPendingSync *tmp = l->data; + + if (tmp->sync_seqnum == msg->sequence_id) { + sync = tmp; + break; + } + } + + if (!sync) + return; + + /* Got a FOLLOW_UP for this already */ + if (sync->sync_send_time_remote != GST_CLOCK_TIME_NONE) + return; + + if (sync->sync_recv_time_local >= receive_time) { + GST_ERROR ("Got bogus follow up in domain %u: %" GST_TIME_FORMAT " > %" + GST_TIME_FORMAT, domain->domain, + GST_TIME_ARGS (sync->sync_recv_time_local), + GST_TIME_ARGS (receive_time)); + g_queue_remove (&domain->pending_syncs, sync); + ptp_pending_sync_free (sync); + return; + } + + sync->correction_field_sync += msg->correction_field; + sync->sync_send_time_remote = + PTP_TIMESTAMP_TO_GST_CLOCK_TIME (msg->message_specific. + follow_up.precise_origin_timestamp); + sync->follow_up_recv_time_local = receive_time; + + if (domain->last_ptp_sync_time >= sync->sync_send_time_remote) { + GST_WARNING ("Backwards PTP times in domain %u: %" GST_TIME_FORMAT " >= %" + GST_TIME_FORMAT, domain->domain, + GST_TIME_ARGS (domain->last_ptp_sync_time), + GST_TIME_ARGS (sync->sync_send_time_remote)); + g_queue_remove (&domain->pending_syncs, sync); + ptp_pending_sync_free (sync); + sync = NULL; + return; + } + domain->last_ptp_sync_time = sync->sync_send_time_remote; + + if (send_delay_req (domain, sync)) { + /* Sent delay request */ + } else { + update_ptp_time (domain, sync); + g_queue_remove (&domain->pending_syncs, sync); + ptp_pending_sync_free (sync); + sync = NULL; + } +} + +static void +handle_delay_resp_message (PtpMessage * msg, GstClockTime receive_time) +{ + GList *l; + PtpDomainData *domain = NULL; + PtpPendingSync *sync = NULL; + + /* Don't consider messages with the alternate master flag set */ + if ((msg->flag_field & 0x0100)) + return; + + for (l = domain_data; l; l = l->next) { + PtpDomainData *tmp = l->data; + + if (msg->domain_number == tmp->domain) { + domain = tmp; + break; + } + } + + if (!domain) + return; + + /* If we have a master clock, ignore this message if it's not coming from there */ + if (domain->have_master_clock + && compare_clock_identity (&domain->master_clock_identity, + &msg->source_port_identity) != 0) + return; + + /* Not for us */ + if (msg->message_specific.delay_resp. + requesting_port_identity.clock_identity != ptp_clock_id.clock_identity + || msg->message_specific.delay_resp. + requesting_port_identity.port_number != ptp_clock_id.port_number) + return; + + domain->min_delay_req_interval = + log2_to_clock_time (msg->log_message_interval); + + /* Check if we know about this one */ + for (l = domain->pending_syncs.head; l; l = l->next) { + PtpPendingSync *tmp = l->data; + + if (tmp->delay_req_seqnum == msg->sequence_id) { + sync = tmp; + break; + } + } + + if (!sync) + return; + + /* Got a DELAY_RESP for this already */ + if (sync->delay_req_recv_time_remote != GST_CLOCK_TIME_NONE) + return; + + if (sync->delay_req_send_time_local > receive_time) { + GST_ERROR ("Got bogus delay response in domain %u: %" GST_TIME_FORMAT " > %" + GST_TIME_FORMAT, domain->domain, + GST_TIME_ARGS (sync->delay_req_send_time_local), + GST_TIME_ARGS (receive_time)); + g_queue_remove (&domain->pending_syncs, sync); + ptp_pending_sync_free (sync); + return; + } + + sync->correction_field_delay = msg->correction_field; + + sync->delay_req_recv_time_remote = + PTP_TIMESTAMP_TO_GST_CLOCK_TIME (msg->message_specific. + delay_resp.receive_timestamp); + sync->delay_resp_recv_time_local = receive_time; + + if (domain->mean_path_delay != 0 + && sync->sync_send_time_remote > sync->delay_req_recv_time_remote) { + GST_WARNING ("Sync send time after delay req receive time for domain %u: %" + GST_TIME_FORMAT " > %" GST_TIME_FORMAT, domain->domain, + GST_TIME_ARGS (sync->sync_send_time_remote), + GST_TIME_ARGS (sync->delay_req_recv_time_remote)); + g_queue_remove (&domain->pending_syncs, sync); + ptp_pending_sync_free (sync); + return; + } + + if (update_mean_path_delay (domain, sync)) + update_ptp_time (domain, sync); + g_queue_remove (&domain->pending_syncs, sync); + ptp_pending_sync_free (sync); +} + +static void +handle_ptp_message (PtpMessage * msg, GstClockTime receive_time) +{ + /* Ignore our own messages */ + if (msg->source_port_identity.clock_identity == ptp_clock_id.clock_identity && + msg->source_port_identity.port_number == ptp_clock_id.port_number) + return; + + switch (msg->message_type) { + case PTP_MESSAGE_TYPE_ANNOUNCE: + handle_announce_message (msg, receive_time); + break; + case PTP_MESSAGE_TYPE_SYNC: + handle_sync_message (msg, receive_time); + break; + case PTP_MESSAGE_TYPE_FOLLOW_UP: + handle_follow_up_message (msg, receive_time); + break; + case PTP_MESSAGE_TYPE_DELAY_RESP: + handle_delay_resp_message (msg, receive_time); + break; + default: + break; + } +} + +static gboolean +have_stdin_data_cb (GIOChannel * channel, GIOCondition condition, + gpointer user_data) +{ + GIOStatus status; + StdIOHeader header; + gchar buffer[8192]; + GError *err = NULL; + gsize read; + + if ((condition & G_IO_STATUS_EOF)) { + GST_ERROR ("Got EOF on stdin"); + g_main_loop_quit (main_loop); + return G_SOURCE_REMOVE; + } + + status = + g_io_channel_read_chars (channel, (gchar *) & header, sizeof (header), + &read, &err); + if (status == G_IO_STATUS_ERROR) { + GST_ERROR ("Failed to read from stdin: %s", err->message); + g_main_loop_quit (main_loop); + return G_SOURCE_REMOVE; + } else if (status == G_IO_STATUS_EOF) { + GST_ERROR ("Got EOF on stdin"); + g_main_loop_quit (main_loop); + return G_SOURCE_REMOVE; + } else if (status != G_IO_STATUS_NORMAL) { + GST_ERROR ("Unexpected stdin read status: %d", status); + g_main_loop_quit (main_loop); + return G_SOURCE_REMOVE; + } else if (read != sizeof (header)) { + GST_ERROR ("Unexpected read size: %" G_GSIZE_FORMAT, read); + g_main_loop_quit (main_loop); + return G_SOURCE_REMOVE; + } else if (header.size > 8192) { + GST_ERROR ("Unexpected size: %u", header.size); + g_main_loop_quit (main_loop); + return G_SOURCE_REMOVE; + } + + status = g_io_channel_read_chars (channel, buffer, header.size, &read, &err); + if (status == G_IO_STATUS_ERROR) { + GST_ERROR ("Failed to read from stdin: %s", err->message); + g_main_loop_quit (main_loop); + return G_SOURCE_REMOVE; + } else if (status == G_IO_STATUS_EOF) { + GST_ERROR ("EOF on stdin"); + g_main_loop_quit (main_loop); + return G_SOURCE_REMOVE; + } else if (status != G_IO_STATUS_NORMAL) { + GST_ERROR ("Unexpected stdin read status: %d", status); + g_main_loop_quit (main_loop); + return G_SOURCE_REMOVE; + } else if (read != header.size) { + GST_ERROR ("Unexpected read size: %" G_GSIZE_FORMAT, read); + g_main_loop_quit (main_loop); + return G_SOURCE_REMOVE; + } + + switch (header.type) { + case TYPE_EVENT: + case TYPE_GENERAL:{ + GstClockTime receive_time = gst_util_get_timestamp (); + PtpMessage msg; + + if (parse_ptp_message (&msg, (const guint8 *) buffer, header.size)) { + dump_ptp_message (&msg); + handle_ptp_message (&msg, receive_time); + } + break; + } + default: + case TYPE_CLOCK_ID:{ + if (header.size != 8) { + GST_ERROR ("Unexpected clock id size (%u != 8)", header.size); + g_main_loop_quit (main_loop); + return G_SOURCE_REMOVE; + } + g_mutex_lock (&ptp_lock); + ptp_clock_id.clock_identity = GST_READ_UINT64_BE (buffer); + ptp_clock_id.port_number = getpid (); + GST_DEBUG ("Got clock id 0x%016" G_GINT64_MODIFIER "x %u", + ptp_clock_id.clock_identity, ptp_clock_id.port_number); + g_cond_signal (&ptp_cond); + g_mutex_unlock (&ptp_lock); + break; + } + } + + return G_SOURCE_CONTINUE; +} + +/* Cleanup all announce messages and announce message senders + * that are timed out by now, and clean up all pending syncs + * that are missing their FOLLOW_UP or DELAY_RESP */ +static gboolean +cleanup_cb (gpointer data) +{ + GstClockTime now = gst_util_get_timestamp (); + GList *l, *m, *n; + + for (l = domain_data; l; l = l->next) { + PtpDomainData *domain = l->data; + + for (n = domain->announce_senders; n;) { + PtpAnnounceSender *sender = n->data; + gboolean timed_out = TRUE; + + /* Keep only 5 messages per sender around */ + while (g_queue_get_length (&sender->announce_messages) > 5) { + PtpAnnounceMessage *msg = g_queue_pop_head (&sender->announce_messages); + g_free (msg); + } + + for (m = sender->announce_messages.head; m; m = m->next) { + PtpAnnounceMessage *msg = m->data; + + if (msg->receive_time + + sender->announce_interval * PTP_ANNOUNCE_RECEIPT_TIMEOUT > now) { + timed_out = FALSE; + break; + } + } + + if (timed_out) { + GST_DEBUG ("Announce sender 0x%016" G_GINT64_MODIFIER "x %u timed out", + sender->master_clock_identity.clock_identity, + sender->master_clock_identity.port_number); + g_queue_foreach (&sender->announce_messages, (GFunc) g_free, NULL); + g_queue_clear (&sender->announce_messages); + } + + if (g_queue_get_length (&sender->announce_messages) == 0) { + GList *tmp = n->next; + + if (compare_clock_identity (&sender->master_clock_identity, + &domain->master_clock_identity) == 0) + GST_WARNING ("currently selected master clock timed out"); + g_free (sender); + domain->announce_senders = + g_list_delete_link (domain->announce_senders, n); + n = tmp; + } else { + n = n->next; + } + } + select_best_master_clock (domain, now); + + /* Clean up any pending syncs */ + for (n = domain->pending_syncs.head; n;) { + PtpPendingSync *sync = n->data; + gboolean timed_out = FALSE; + + /* Time out pending syncs after 4 sync intervals or 10 seconds, + * and pending delay reqs after 4 delay req intervals or 10 seconds + */ + if (sync->delay_req_send_time_local != GST_CLOCK_TIME_NONE && + ((domain->min_delay_req_interval != 0 + && sync->delay_req_send_time_local + + 4 * domain->min_delay_req_interval < now) + || (sync->delay_req_send_time_local + 10 * GST_SECOND < now))) { + timed_out = TRUE; + } else if ((domain->sync_interval != 0 + && sync->sync_recv_time_local + 4 * domain->sync_interval < now) + || (sync->sync_recv_time_local + 10 * GST_SECOND < now)) { + timed_out = TRUE; + } + + if (timed_out) { + GList *tmp = n->next; + ptp_pending_sync_free (sync); + g_queue_delete_link (&domain->pending_syncs, n); + n = tmp; + } else { + n = n->next; + } + } + } + + return G_SOURCE_CONTINUE; +} + +static gpointer +ptp_helper_main (gpointer data) +{ + GSource *cleanup_source; + + GST_DEBUG ("Starting PTP helper loop"); + + /* Check all 5 seconds, if we have to cleanup ANNOUNCE or pending syncs message */ + cleanup_source = g_timeout_source_new_seconds (5); + g_source_set_priority (cleanup_source, G_PRIORITY_DEFAULT); + g_source_set_callback (cleanup_source, (GSourceFunc) cleanup_cb, NULL, NULL); + g_source_attach (cleanup_source, main_context); + g_source_unref (cleanup_source); + + g_main_loop_run (main_loop); + GST_DEBUG ("Stopped PTP helper loop"); + + g_mutex_lock (&ptp_lock); + ptp_clock_id.clock_identity = GST_PTP_CLOCK_ID_NONE; + ptp_clock_id.port_number = 0; + initted = FALSE; + g_cond_signal (&ptp_cond); + g_mutex_unlock (&ptp_lock); + + return NULL; +} + +/** + * gst_ptp_is_supported: + * + * Check if PTP clocks are generally supported on this system, and if previous + * initializations did not fail. + * + * Returns: %TRUE if PTP clocks are generally supported on this system, and + * previous initializations did not fail. + * + * Since: 1.6 + */ +gboolean +gst_ptp_is_supported (void) +{ + return supported; +} + +/** + * gst_ptp_is_initialized: + * + * Check if the GStreamer PTP clock subsystem is initialized. + * + * Returns: %TRUE if the GStreamer PTP clock subsystem is intialized. + * + * Since: 1.6 + */ +gboolean +gst_ptp_is_initialized (void) +{ + return initted; +} + +/** + * gst_ptp_init: + * @clock_id: PTP clock id of this process' clock or %GST_PTP_CLOCK_ID_NONE + * @interfaces: (transfer none) (array zero-terminated=1): network interfaces to run the clock on + * + * Initialize the GStreamer PTP subsystem and create a PTP ordinary clock in + * slave-only mode for all domains on the given @interfaces with the + * given @clock_id. + * + * If @clock_id is %GST_PTP_CLOCK_ID_NONE, a clock id is automatically + * generated from the MAC address of the first network interface. + * + * + * This function is automatically called by gst_ptp_clock_new() with default + * parameters if it wasn't called before. + * + * Returns: %TRUE if the GStreamer PTP clock subsystem could be initialized. + * + * Since: 1.6 + */ +gboolean +gst_ptp_init (guint64 clock_id, gchar ** interfaces) +{ + gboolean ret; + const gchar *env; + gchar **argv = NULL; + gint argc, argc_c; + gint fd_r, fd_w; + GError *err = NULL; + GSource *stdin_source; + + GST_DEBUG_CATEGORY_INIT (ptp_debug, "ptp", 0, "PTP clock"); + + g_mutex_lock (&ptp_lock); + if (!supported) { + GST_ERROR ("PTP not supported"); + ret = FALSE; + goto done; + } + + if (initted) { + GST_DEBUG ("PTP already initialized"); + ret = TRUE; + goto done; + } + + if (ptp_helper_pid) { + GST_DEBUG ("PTP currently initializing"); + goto wait; + } + + if (!domain_stats_hooks_initted) { + g_hook_list_init (&domain_stats_hooks, sizeof (GHook)); + domain_stats_hooks_initted = TRUE; + } + + argc = 1; + if (clock_id != GST_PTP_CLOCK_ID_NONE) + argc += 2; + if (interfaces != NULL) + argc += 2 * g_strv_length (interfaces); + + argv = g_new0 (gchar *, argc + 2); + argc_c = 0; + + env = g_getenv ("GST_PTP_HELPER_1_0"); + if (env == NULL) + env = g_getenv ("GST_PTP_HELPER"); + if (env != NULL && *env != '\0') { + GST_LOG ("Trying GST_PTP_HELPER env var: %s", env); + argv[argc_c++] = g_strdup (env); + } else { + argv[argc_c++] = g_strdup (GST_PTP_HELPER_INSTALLED); + } + + if (clock_id != GST_PTP_CLOCK_ID_NONE) { + argv[argc_c++] = g_strdup ("-c"); + argv[argc_c++] = g_strdup_printf ("0x%016" G_GINT64_MODIFIER "x", clock_id); + } + + if (interfaces != NULL) { + gchar **ptr = interfaces; + + while (*ptr) { + argv[argc_c++] = g_strdup ("-i"); + argv[argc_c++] = g_strdup (*ptr); + ptr++; + } + } + + main_context = g_main_context_new (); + main_loop = g_main_loop_new (main_context, FALSE); + + ptp_helper_thread = + g_thread_try_new ("ptp-helper-thread", ptp_helper_main, NULL, &err); + if (!ptp_helper_thread) { + GST_ERROR ("Failed to start PTP helper thread: %s", err->message); + g_clear_error (&err); + ret = FALSE; + goto done; + } + + if (!g_spawn_async_with_pipes (NULL, argv, NULL, 0, NULL, NULL, + &ptp_helper_pid, &fd_w, &fd_r, NULL, &err)) { + GST_ERROR ("Failed to start ptp helper process: %s", err->message); + g_clear_error (&err); + ret = FALSE; + supported = FALSE; + goto done; + } + + stdin_channel = g_io_channel_unix_new (fd_r); + g_io_channel_set_encoding (stdin_channel, NULL, NULL); + g_io_channel_set_buffered (stdin_channel, FALSE); + g_io_channel_set_close_on_unref (stdin_channel, TRUE); + stdin_source = + g_io_create_watch (stdin_channel, G_IO_IN | G_IO_PRI | G_IO_HUP); + g_source_set_priority (stdin_source, G_PRIORITY_DEFAULT); + g_source_set_callback (stdin_source, (GSourceFunc) have_stdin_data_cb, NULL, + NULL); + g_source_attach (stdin_source, main_context); + g_source_unref (stdin_source); + + /* Create stdout channel */ + stdout_channel = g_io_channel_unix_new (fd_w); + g_io_channel_set_encoding (stdout_channel, NULL, NULL); + g_io_channel_set_close_on_unref (stdout_channel, TRUE); + g_io_channel_set_buffered (stdout_channel, FALSE); + + delay_req_rand = g_rand_new (); + + initted = TRUE; + +wait: + GST_DEBUG ("Waiting for PTP to be initialized"); + + while (ptp_clock_id.clock_identity == GST_PTP_CLOCK_ID_NONE && initted) + g_cond_wait (&ptp_cond, &ptp_lock); + + ret = initted; + if (ret) { + GST_DEBUG ("Initialized and got clock id 0x%016" G_GINT64_MODIFIER "x %u", + ptp_clock_id.clock_identity, ptp_clock_id.port_number); + } else { + GST_ERROR ("Failed to initialize"); + supported = FALSE; + } + +done: + g_strfreev (argv); + + if (!ret) { + if (ptp_helper_pid) { + kill (ptp_helper_pid, SIGKILL); + waitpid (ptp_helper_pid, NULL, 0); + g_spawn_close_pid (ptp_helper_pid); + } + ptp_helper_pid = 0; + + if (stdin_channel) + g_io_channel_unref (stdin_channel); + stdin_channel = NULL; + if (stdout_channel) + g_io_channel_unref (stdout_channel); + stdout_channel = NULL; + + if (main_loop && ptp_helper_thread) { + g_main_loop_quit (main_loop); + g_thread_join (ptp_helper_thread); + } + ptp_helper_thread = NULL; + if (main_loop) + g_main_loop_unref (main_loop); + main_loop = NULL; + if (main_context) + g_main_context_unref (main_context); + main_context = NULL; + + if (delay_req_rand) + g_rand_free (delay_req_rand); + delay_req_rand = NULL; + } + + g_mutex_unlock (&ptp_lock); + + return ret; +} + +/** + * gst_ptp_deinit: + * + * Deinitialize the GStreamer PTP subsystem and stop the PTP clock. If there + * are any remaining GstPtpClock instances, they won't be further synchronized + * to the PTP network clock. + * + * Since: 1.6 + */ +void +gst_ptp_deinit (void) +{ + GList *l, *m; + + g_mutex_lock (&ptp_lock); + + if (ptp_helper_pid) { + kill (ptp_helper_pid, SIGKILL); + waitpid (ptp_helper_pid, NULL, 0); + g_spawn_close_pid (ptp_helper_pid); + } + ptp_helper_pid = 0; + + if (stdin_channel) + g_io_channel_unref (stdin_channel); + stdin_channel = NULL; + if (stdout_channel) + g_io_channel_unref (stdout_channel); + stdout_channel = NULL; + + if (main_loop && ptp_helper_thread) { + GThread *tmp = ptp_helper_thread; + ptp_helper_thread = NULL; + g_mutex_unlock (&ptp_lock); + g_main_loop_quit (main_loop); + g_thread_join (tmp); + g_mutex_lock (&ptp_lock); + } + if (main_loop) + g_main_loop_unref (main_loop); + main_loop = NULL; + if (main_context) + g_main_context_unref (main_context); + main_context = NULL; + + if (delay_req_rand) + g_rand_free (delay_req_rand); + delay_req_rand = NULL; + + for (l = domain_data; l; l = l->next) { + PtpDomainData *domain = l->data; + + for (m = domain->announce_senders; m; m = m->next) { + PtpAnnounceSender *sender = m->data; + + g_queue_foreach (&sender->announce_messages, (GFunc) g_free, NULL); + g_queue_clear (&sender->announce_messages); + g_free (sender); + } + g_list_free (domain->announce_senders); + + g_queue_foreach (&domain->pending_syncs, (GFunc) ptp_pending_sync_free, + NULL); + g_queue_clear (&domain->pending_syncs); + gst_object_unref (domain->domain_clock); + g_free (domain); + } + g_list_free (domain_data); + domain_data = NULL; + g_list_foreach (domain_clocks, (GFunc) g_free, NULL); + g_list_free (domain_clocks); + domain_clocks = NULL; + + ptp_clock_id.clock_identity = GST_PTP_CLOCK_ID_NONE; + ptp_clock_id.port_number = 0; + + initted = FALSE; + + g_mutex_unlock (&ptp_lock); +} + +#define DEFAULT_DOMAIN 0 + +enum +{ + PROP_0, + PROP_DOMAIN, + PROP_INTERNAL_CLOCK +}; + +#define GST_PTP_CLOCK_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE ((obj), GST_TYPE_PTP_CLOCK, GstPtpClockPrivate)) + +struct _GstPtpClockPrivate +{ + guint domain; + GstClock *domain_clock; + gulong domain_stats_id; +}; + +#define gst_ptp_clock_parent_class parent_class +G_DEFINE_TYPE (GstPtpClock, gst_ptp_clock, GST_TYPE_SYSTEM_CLOCK); + +static void gst_ptp_clock_set_property (GObject * object, guint prop_id, + const GValue * value, GParamSpec * pspec); +static void gst_ptp_clock_get_property (GObject * object, guint prop_id, + GValue * value, GParamSpec * pspec); +static void gst_ptp_clock_finalize (GObject * object); + +static GstClockTime gst_ptp_clock_get_internal_time (GstClock * clock); + +static void +gst_ptp_clock_class_init (GstPtpClockClass * klass) +{ + GObjectClass *gobject_class; + GstClockClass *clock_class; + + gobject_class = G_OBJECT_CLASS (klass); + clock_class = GST_CLOCK_CLASS (klass); + + g_type_class_add_private (klass, sizeof (GstPtpClockPrivate)); + + gobject_class->finalize = gst_ptp_clock_finalize; + gobject_class->get_property = gst_ptp_clock_get_property; + gobject_class->set_property = gst_ptp_clock_set_property; + + g_object_class_install_property (gobject_class, PROP_DOMAIN, + g_param_spec_uint ("domain", "Domain", + "The PTP domain", 0, G_MAXUINT8, + DEFAULT_DOMAIN, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property (gobject_class, PROP_INTERNAL_CLOCK, + g_param_spec_object ("internal-clock", "Internal Clock", + "Internal clock", GST_TYPE_CLOCK, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); + + clock_class->get_internal_time = gst_ptp_clock_get_internal_time; +} + +static void +gst_ptp_clock_init (GstPtpClock * self) +{ + GstPtpClockPrivate *priv; + + self->priv = priv = GST_PTP_CLOCK_GET_PRIVATE (self); + + GST_OBJECT_FLAG_SET (self, GST_CLOCK_FLAG_CAN_SET_MASTER); + GST_OBJECT_FLAG_SET (self, GST_CLOCK_FLAG_NEEDS_STARTUP_SYNC); + + priv->domain = DEFAULT_DOMAIN; +} + +static gboolean +gst_ptp_clock_ensure_domain_clock (GstPtpClock * self) +{ + gboolean got_clock = TRUE; + + if (G_UNLIKELY (!self->priv->domain_clock)) { + g_mutex_lock (&domain_clocks_lock); + if (!self->priv->domain_clock) { + GList *l; + + got_clock = FALSE; + + for (l = domain_clocks; l; l = l->next) { + PtpDomainData *clock_data = l->data; + + if (clock_data->domain == self->priv->domain + && clock_data->last_ptp_time != 0) { + self->priv->domain_clock = clock_data->domain_clock; + got_clock = TRUE; + break; + } + } + } + g_mutex_unlock (&domain_clocks_lock); + if (got_clock) { + g_object_notify (G_OBJECT (self), "internal-clock"); + gst_clock_set_synced (GST_CLOCK (self), TRUE); + } + } + + return got_clock; +} + +static gboolean +gst_ptp_clock_stats_callback (guint8 domain, const GstStructure * stats, + gpointer user_data) +{ + GstPtpClock *self = user_data; + + if (domain != self->priv->domain + || !gst_structure_has_name (stats, GST_PTP_STATISTICS_TIME_UPDATED)) + return TRUE; + + /* Let's set our internal clock */ + if (!gst_ptp_clock_ensure_domain_clock (self)) + return TRUE; + + self->priv->domain_stats_id = 0; + + return FALSE; +} + +static void +gst_ptp_clock_set_property (GObject * object, guint prop_id, + const GValue * value, GParamSpec * pspec) +{ + GstPtpClock *self = GST_PTP_CLOCK (object); + + switch (prop_id) { + case PROP_DOMAIN: + self->priv->domain = g_value_get_uint (value); + gst_ptp_clock_ensure_domain_clock (self); + if (!self->priv->domain_clock) + self->priv->domain_stats_id = + gst_ptp_statistics_callback_add (gst_ptp_clock_stats_callback, self, + NULL); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gst_ptp_clock_get_property (GObject * object, guint prop_id, + GValue * value, GParamSpec * pspec) +{ + GstPtpClock *self = GST_PTP_CLOCK (object); + + switch (prop_id) { + case PROP_DOMAIN: + g_value_set_uint (value, self->priv->domain); + break; + case PROP_INTERNAL_CLOCK: + gst_ptp_clock_ensure_domain_clock (self); + g_value_set_object (value, self->priv->domain_clock); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gst_ptp_clock_finalize (GObject * object) +{ + GstPtpClock *self = GST_PTP_CLOCK (object); + + if (self->priv->domain_stats_id) + gst_ptp_statistics_callback_remove (self->priv->domain_stats_id); + + G_OBJECT_CLASS (gst_ptp_clock_parent_class)->finalize (object); +} + +static GstClockTime +gst_ptp_clock_get_internal_time (GstClock * clock) +{ + GstPtpClock *self = GST_PTP_CLOCK (clock); + + gst_ptp_clock_ensure_domain_clock (self); + + if (!self->priv->domain_clock) { + GST_ERROR_OBJECT (self, "Domain %u has no clock yet and is not synced", + self->priv->domain); + return GST_CLOCK_TIME_NONE; + } + + return gst_clock_get_time (self->priv->domain_clock); +} + +/** + * gst_ptp_clock_new: + * @name: Name of the clock + * @domain: PTP domain + * + * Creates a new PTP clock instance that exports the PTP time of the master + * clock in @domain. This clock can be slaved to other clocks as needed. + * + * If gst_ptp_init() was not called before, this will call gst_ptp_init() with + * default parameters. + * + * + * This clock only returns valid timestamps after it received the first + * times from the PTP master clock on the network. Once this happens the + * GstPtpClock::internal-clock property will become non-NULL. You can connect + * to the notify::internal-clock signal to get notified about this, or + * alternatively use gst_ptp_clock_wait_ready() to wait for this to happen. + * + * Since: 1.6 + */ +GstClock * +gst_ptp_clock_new (const gchar * name, guint domain) +{ + g_return_val_if_fail (name != NULL, NULL); + g_return_val_if_fail (domain <= G_MAXUINT8, NULL); + + if (!initted && !gst_ptp_init (GST_PTP_CLOCK_ID_NONE, NULL)) { + GST_ERROR ("Failed to initialize PTP"); + return NULL; + } + + return g_object_new (GST_TYPE_PTP_CLOCK, "name", name, "domain", domain, + NULL); +} + +typedef struct +{ + guint8 domain; + const GstStructure *stats; +} DomainStatsMarshalData; + +static void +domain_stats_marshaller (GHook * hook, DomainStatsMarshalData * data) +{ + GstPtpStatisticsCallback callback = (GstPtpStatisticsCallback) hook->func; + + if (!callback (data->domain, data->stats, hook->data)) + g_hook_destroy (&domain_stats_hooks, hook->hook_id); +} + +static void +emit_ptp_statistics (guint8 domain, const GstStructure * stats) +{ + DomainStatsMarshalData data = { domain, stats }; + + g_mutex_lock (&ptp_lock); + g_hook_list_marshal (&domain_stats_hooks, TRUE, + (GHookMarshaller) domain_stats_marshaller, &data); + g_mutex_unlock (&ptp_lock); +} + +/** + * gst_ptp_statistics_callback_add: + * @callback: GstPtpStatisticsCallback to call + * @user_data: Data to pass to the callback + * @destroy_data: GDestroyNotify to destroy the data + * + * Installs a new statistics callback for gathering PTP statistics. See + * GstPtpStatisticsCallback for a list of statistics that are provided. + * + * Returns: Id for the callback that can be passed to + * gst_ptp_statistics_callback_remove() + * + * Since: 1.6 + */ +gulong +gst_ptp_statistics_callback_add (GstPtpStatisticsCallback callback, + gpointer user_data, GDestroyNotify destroy_data) +{ + GHook *hook; + + g_mutex_lock (&ptp_lock); + + if (!domain_stats_hooks_initted) { + g_hook_list_init (&domain_stats_hooks, sizeof (GHook)); + domain_stats_hooks_initted = TRUE; + } + + hook = g_hook_alloc (&domain_stats_hooks); + hook->func = callback; + hook->data = user_data; + hook->destroy = destroy_data; + g_hook_prepend (&domain_stats_hooks, hook); + g_atomic_int_add (&domain_stats_n_hooks, 1); + + g_mutex_unlock (&ptp_lock); + + return hook->hook_id; +} + +/** + * gst_ptp_statistics_callback_remove: + * @id: Callback id to remove + * + * Removes a PTP statistics callback that was previously added with + * gst_ptp_statistics_callback_add(). + * + * Since: 1.6 + */ +void +gst_ptp_statistics_callback_remove (gulong id) +{ + g_mutex_lock (&ptp_lock); + if (g_hook_destroy (&domain_stats_hooks, id)) + g_atomic_int_add (&domain_stats_n_hooks, -1); + g_mutex_unlock (&ptp_lock); +} + +#else /* HAVE_PTP */ + +GType +gst_ptp_clock_get_type (void) +{ + return G_TYPE_INVALID; +} + +gboolean +gst_ptp_is_supported (void) +{ + return FALSE; +} + +gboolean +gst_ptp_is_initialized (void) +{ + return FALSE; +} + +gboolean +gst_ptp_init (guint64 clock_id, gchar ** interfaces) +{ + return FALSE; +} + +void +gst_ptp_deinit (void) +{ +} + +GstClock * +gst_ptp_clock_new (const gchar * name, guint domain) +{ + return NULL; +} + +gboolean +gst_ptp_clock_wait_ready (GstPtpClock * self, GstClockTime timeout) +{ + return FALSE; +} + +gulong +gst_ptp_statistics_callback_add (GstPtpStatisticsCallback callback, + gpointer user_data, GDestroyNotify destroy_data) +{ + return 0; +} + +void +gst_ptp_statistics_callback_remove (gulong id) +{ + return; +} + +#endif diff --git a/libs/gst/net/gstptpclock.h b/libs/gst/net/gstptpclock.h new file mode 100644 index 000000000..f50b83edc --- /dev/null +++ b/libs/gst/net/gstptpclock.h @@ -0,0 +1,142 @@ +/* GStreamer + * Copyright (C) 2015 Sebastian Dröge <sebastian@centricular.com> + * + * + * 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., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __GST_PTP_CLOCK_H__ +#define __GST_PTP_CLOCK_H__ + +#include <gst/gst.h> +#include <gst/gstsystemclock.h> + +G_BEGIN_DECLS + +#define GST_TYPE_PTP_CLOCK \ + (gst_ptp_clock_get_type()) +#define GST_PTP_CLOCK(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_PTP_CLOCK,GstPtpClock)) +#define GST_PTP_CLOCK_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_PTP_CLOCK,GstPtpClockClass)) +#define GST_IS_PTP_CLOCK(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_PTP_CLOCK)) +#define GST_IS_PTP_CLOCK_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE((klass),GST_TYPE_PTP_CLOCK)) + +typedef struct _GstPtpClock GstPtpClock; +typedef struct _GstPtpClockClass GstPtpClockClass; +typedef struct _GstPtpClockPrivate GstPtpClockPrivate; + +/** + * GstPtpClock: + * + * Opaque #GstPtpClock structure. + */ +struct _GstPtpClock { + GstSystemClock clock; + + /*< private >*/ + GstPtpClockPrivate *priv; + + gpointer _gst_reserved[GST_PADDING]; +}; + +struct _GstPtpClockClass { + GstSystemClockClass parent_class; + + /*< private >*/ + gpointer _gst_reserved[GST_PADDING]; +}; + +/** + * GST_PTP_CLOCK_ID_NONE: + * PTP clock identification that can be passed to gst_ptp_init() to + * automatically select one based on the MAC address of interfaces + */ +#define GST_PTP_CLOCK_ID_NONE ((guint64) -1) + +GType gst_ptp_clock_get_type (void); + +gboolean gst_ptp_is_supported (void); +gboolean gst_ptp_is_initialized (void); +gboolean gst_ptp_init (guint64 clock_id, + gchar ** interfaces); +void gst_ptp_deinit (void); + +#define GST_PTP_STATISTICS_NEW_DOMAIN_FOUND "GstPtpStatisticsNewDomainFound" +#define GST_PTP_STATISTICS_BEST_MASTER_CLOCK_SELECTED "GstPtpStatisticsBestMasterClockSelected" +#define GST_PTP_STATISTICS_PATH_DELAY_MEASURED "GstPtpStatisticsPathDelayMeasured" +#define GST_PTP_STATISTICS_TIME_UPDATED "GstPtpStatisticsTimeUpdated" + +/** + * GstPtpStatisticsCallback: + * @domain: PTP domain identifier + * @stats: New statistics + * @user_data: Data passed to gst_ptp_statistics_callback_add() + * + * The statistics can be the following structures: + * + * GST_PTP_STATISTICS_NEW_DOMAIN_FOUND: + * "domain" G_TYPE_UINT The domain identifier of the domain + * "clock" GST_TYPE_CLOCK The internal clock that is slaved to the + * PTP domain + * + * GST_PTP_STATISTICS_BEST_MASTER_CLOCK_SELECTED: + * "domain" G_TYPE_UINT The domain identifier of the domain + * "master-clock-id" G_TYPE_UINT64 PTP clock identifier of the selected master + * clock + * "master-clock-port" G_TYPE_UINT PTP port number of the selected master clock + * "grandmaster-clock-id" G_TYPE_UINT64 PTP clock identifier of the grandmaster clock + * + * GST_PTP_STATISTICS_PATH_DELAY_MEASURED: + * "domain" G_TYPE_UINT The domain identifier of the domain + * "mean-path-delay-avg" GST_TYPE_CLOCK_TIME Average mean path delay + * "mean-path-delay" GST_TYPE_CLOCK_TIME Latest mean path delay + * "delay-request-delay" GST_TYPE_CLOCK_TIME Delay of DELAY_REQ / DELAY_RESP messages + * + * GST_PTP_STATISTICS_TIME_UPDATED: + * "domain" G_TYPE_UINT The domain identifier of the domain + * "mean-path-delay-avg" GST_TYPE_CLOCK_TIME Average mean path delay + * "local-time" GST_TYPE_CLOCK_TIME Local time that corresponds to ptp-time + * "ptp-time" GST_TYPE_CLOCK_TIME Newly measured PTP time at local-time + * "estimated-ptp-time" GST_TYPE_CLOCK_TIME Estimated PTP time based on previous measurements + * "discontinuity" G_TYPE_INT64 Difference between estimated and measured PTP time + * "synced" G_TYPE_BOOLEAN Currently synced to the remote clock + * "r-squared" G_TYPE_DOUBLE R² of clock estimation regression + * "internal-time" GST_TYPE_CLOCK_TIME Internal time clock parameter + * "external-time" GST_TYPE_CLOCK_TIME External time clock parameter + * "rate-num" G_TYPE_UINT64 Internal/external rate numerator + * "rate-den" G_TYPE_UINT64 Internal/external rate denominator + * "rate" G_TYPE_DOUBLE Internal/external rate + * + * If %FALSE is returned, the callback is removed and never called again. + * + */ +typedef gboolean (*GstPtpStatisticsCallback) (guint8 domain, + const GstStructure * stats, + gpointer user_data); +gulong gst_ptp_statistics_callback_add (GstPtpStatisticsCallback callback, + gpointer user_data, GDestroyNotify destroy_data); +void gst_ptp_statistics_callback_remove (gulong id); + +GstClock* gst_ptp_clock_new (const gchar *name, + guint domain); + +G_END_DECLS + +#endif /* __GST_PTP_CLOCK_H__ */ + diff --git a/libs/gst/net/net.h b/libs/gst/net/net.h index 1ef53d641..8af032e79 100644 --- a/libs/gst/net/net.h +++ b/libs/gst/net/net.h @@ -27,5 +27,6 @@ #include <gst/net/gstnetclientclock.h> #include <gst/net/gstnettimepacket.h> #include <gst/net/gstnettimeprovider.h> +#include <gst/net/gstptpclock.h> #endif /* __GST_NET__H__ */ diff --git a/tests/examples/Makefile.am b/tests/examples/Makefile.am index f3cfd8d71..6918f9023 100644 --- a/tests/examples/Makefile.am +++ b/tests/examples/Makefile.am @@ -11,6 +11,7 @@ always_dirs = \ manual \ memory \ netclock \ + ptp \ stepping \ streamiddemux \ streams diff --git a/tests/examples/ptp/.gitignore b/tests/examples/ptp/.gitignore new file mode 100644 index 000000000..b5b83be1a --- /dev/null +++ b/tests/examples/ptp/.gitignore @@ -0,0 +1 @@ +ptp-print-times diff --git a/tests/examples/ptp/Makefile.am b/tests/examples/ptp/Makefile.am new file mode 100644 index 000000000..d76b4ad4c --- /dev/null +++ b/tests/examples/ptp/Makefile.am @@ -0,0 +1,7 @@ +noinst_PROGRAMS = ptp-print-times + +ptp_print_times_LDADD = \ + $(top_builddir)/libs/gst/net/libgstnet-@GST_API_VERSION@.la \ + $(GST_OBJ_LIBS) +ptp_print_times_CFLAGS = $(GST_OBJ_CFLAGS) + diff --git a/tests/examples/ptp/ptp-print-times.c b/tests/examples/ptp/ptp-print-times.c new file mode 100644 index 000000000..e6c666fdc --- /dev/null +++ b/tests/examples/ptp/ptp-print-times.c @@ -0,0 +1,100 @@ +/* GStreamer + * Copyright (C) 2015 Sebastian Dröge <sebastian@centricular.com> + * + * + * 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., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +/* Create a PTP client clock and print times and statistics. + * + * When running this from a GStreamer build tree, you will have to set + * GST_PTP_HELPER to libs/gst/helpers/.libs/gst-ptp-helper and also + * make sure that it has the right permissions (setuid root or appropriate + * capabilities + * + * You can test this with any PTP compatible clock, e.g. ptpd from here: http://ptpd.sourceforge.net/ + * + * For testing the accuracy, you can use the PTP reflector available from + * http://code.centricular.com/ptp-clock-reflector/ or here + * https://github.com/sdroege/ptp-clock-reflector + */ + +#include <gst/gst.h> +#include <gst/net/net.h> + +static gint domain = 0; +static gboolean stats = FALSE; + +static GOptionEntry opt_entries[] = { + {"domain", 'd', 0, G_OPTION_ARG_INT, &domain, + "PTP domain", NULL}, + {"stats", 's', 0, G_OPTION_ARG_NONE, &stats, + "Print PTP statistics", NULL}, + {NULL} +}; + +static gboolean +stats_cb (guint8 d, const GstStructure * stats, gpointer user_data) +{ + if (d == domain) { + gchar *stats_str = gst_structure_to_string (stats); + g_print ("Got stats: %s\n", stats_str); + g_free (stats_str); + } + + return TRUE; +} + +gint +main (gint argc, gchar ** argv) +{ + GOptionContext *opt_ctx; + GstClock *clock; + GError *err = NULL; + + opt_ctx = g_option_context_new ("- GStreamer PTP clock test app"); + g_option_context_add_main_entries (opt_ctx, opt_entries, NULL); + g_option_context_add_group (opt_ctx, gst_init_get_option_group ()); + if (!g_option_context_parse (opt_ctx, &argc, &argv, &err)) + g_error ("Error parsing options: %s", err->message); + g_option_context_free (opt_ctx); + + if (!gst_ptp_init (GST_PTP_CLOCK_ID_NONE, NULL)) + g_error ("failed to init ptp"); + + if (stats) + gst_ptp_statistics_callback_add (stats_cb, NULL, NULL); + + clock = gst_ptp_clock_new ("test-clock", domain); + + gst_clock_wait_for_sync (GST_CLOCK (clock), GST_CLOCK_TIME_NONE); + + while (TRUE) { + GstClockTime local, remote; + GstClockTimeDiff diff; + + local = g_get_real_time () * 1000; + remote = gst_clock_get_time (clock); + diff = GST_CLOCK_DIFF (local, remote); + + g_print ("local: %" GST_TIME_FORMAT " ptp: %" GST_TIME_FORMAT " diff: %s%" + GST_TIME_FORMAT "\n", GST_TIME_ARGS (local), GST_TIME_ARGS (remote), + (diff < 0 ? "-" : " "), GST_TIME_ARGS (ABS (diff))); + g_usleep (100000); + } + + return 0; +} diff --git a/win32/common/libgstnet.def b/win32/common/libgstnet.def index 8e3f3270d..0434c32cd 100644 --- a/win32/common/libgstnet.def +++ b/win32/common/libgstnet.def @@ -16,3 +16,11 @@ EXPORTS gst_net_time_packet_serialize gst_net_time_provider_get_type gst_net_time_provider_new + gst_ptp_clock_get_type + gst_ptp_clock_new + gst_ptp_deinit + gst_ptp_init + gst_ptp_is_initialized + gst_ptp_is_supported + gst_ptp_statistics_callback_add + gst_ptp_statistics_callback_remove |