Commit 8d777598 authored by Sebastian Dröge's avatar Sebastian Dröge 🍵
Browse files

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...
parent fe3249b0
......@@ -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}
......
......@@ -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">
......
......@@ -941,6 +941,32 @@ GST_IS_NET_TIME_PROVIDER_CLASS
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>
......
......@@ -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
......
/* 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 =