diff --git a/gst-env.py b/gst-env.py
index 2a714c28e95b98be3c9a9f4c3ef1b64d867c3c19..2d4ae32a4312e891e016804d86f2327a0f4109b5 100755
--- a/gst-env.py
+++ b/gst-env.py
@@ -289,7 +289,7 @@ def get_subprocess_env(options, gst_version):
     env["GST_PLUGIN_SCANNER"] = os.path.normpath(
         "%s/subprojects/gstreamer/libs/gst/helpers/gst-plugin-scanner" % options.builddir)
     env["GST_PTP_HELPER"] = os.path.normpath(
-        "%s/subprojects/gstreamer/libs/gst/helpers/gst-ptp-helper" % options.builddir)
+        "%s/subprojects/gstreamer/libs/gst/helpers/ptp/gst-ptp-helper" % options.builddir)
 
     if os.name == 'nt':
         lib_path_envvar = 'PATH'
diff --git a/subprojects/gstreamer/libs/gst/helpers/gst-ptp-helper.c b/subprojects/gstreamer/libs/gst/helpers/gst-ptp-helper.c
deleted file mode 100644
index 4e8ab3624f5cd74eb40dc430012d2c38a3e9421c..0000000000000000000000000000000000000000
--- a/subprojects/gstreamer/libs/gst/helpers/gst-ptp-helper.c
+++ /dev/null
@@ -1,687 +0,0 @@
-/* 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 <sys/socket.h>
-#include <net/if.h>
-#include <netinet/in.h>
-#include <string.h>
-
-#ifdef HAVE_GETIFADDRS_AF_LINK
-#include <ifaddrs.h>
-#include <net/if_dl.h>
-#endif
-
-#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);
-  g_clear_error (&err);
-
-  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);
-    g_clear_error (&err);
-  } 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);
-    g_clear_error (&err);
-  } 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);
-    g_clear_error (&err);
-  } 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);
-    g_clear_error (&err);
-  } 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);
-      g_clear_error (&err);
-      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);
-  g_clear_error (&err);
-
-  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);
-  g_clear_error (&err);
-
-  /* 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) {
-#if defined(HAVE_SIOCGIFCONF_SIOCGIFFLAGS_SIOCGIFHWADDR)
-    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) {
-      guint i, idx = 0;
-
-      probed_ifaces = g_new0 (gchar *, ifc.ifc_len + 1);
-
-      for (i = 0; i < ifc.ifc_len / sizeof (struct ifreq); i++) {
-        strncpy (ifr.ifr_name, ifc.ifc_req[i].ifr_name, IFNAMSIZ);
-        if (ioctl (g_socket_get_fd (socket_event), SIOCGIFFLAGS, &ifr) == 0) {
-          if ((ifr.ifr_flags & IFF_LOOPBACK))
-            continue;
-          probed_ifaces[idx] = g_strndup (ifc.ifc_req[i].ifr_name, IFNAMSIZ);
-          idx++;
-        } else {
-          g_warning ("can't get flags of interface '%s'",
-              ifc.ifc_req[i].ifr_name);
-          probed_ifaces[idx] = g_strndup (ifc.ifc_req[i].ifr_name, IFNAMSIZ);
-          idx++;
-        }
-        if (idx != 0)
-          ifaces = probed_ifaces;
-      }
-    }
-#elif defined(HAVE_GETIFADDRS_AF_LINK)
-    struct ifaddrs *ifaddr, *ifa;
-
-    if (getifaddrs (&ifaddr) != -1) {
-      GPtrArray *arr;
-
-      arr = g_ptr_array_new ();
-
-      for (ifa = ifaddr; ifa; ifa = ifa->ifa_next) {
-        if ((ifa->ifa_flags & IFF_LOOPBACK))
-          continue;
-
-        if (!ifa->ifa_addr || ifa->ifa_addr->sa_family != AF_LINK)
-          continue;
-
-        g_ptr_array_add (arr, g_strdup (ifa->ifa_name));
-      }
-      freeifaddrs (ifaddr);
-
-      g_ptr_array_add (arr, NULL);
-      ifaces = probed_ifaces = (gchar **) g_ptr_array_free (arr, FALSE);
-    }
-#else
-#warning "Implement something to list all network interfaces"
-#endif
-  }
-
-  /* Get a clock id from the MAC address if none was given */
-  if (clock_id == (guint64) - 1) {
-    gboolean success = FALSE;
-
-#if defined(HAVE_SIOCGIFCONF_SIOCGIFFLAGS_SIOCGIFHWADDR)
-    struct ifreq ifr;
-
-    if (ifaces) {
-      gchar **ptr = ifaces;
-
-      while (*ptr) {
-        memcpy (ifr.ifr_name, *ptr, IFNAMSIZ);
-        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) {
-        guint i;
-
-        for (i = 0; i < ifc.ifc_len / sizeof (struct ifreq); i++) {
-          strncpy (ifr.ifr_name, ifc.ifc_req[i].ifr_name, IFNAMSIZ);
-          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'",
-                ifc.ifc_req[i].ifr_name);
-          }
-        }
-      }
-    }
-#elif defined(HAVE_GETIFADDRS_AF_LINK)
-    struct ifaddrs *ifaddr, *ifa;
-
-    if (getifaddrs (&ifaddr) != -1) {
-      for (ifa = ifaddr; ifa; ifa = ifa->ifa_next) {
-        struct sockaddr_dl *sdl = (struct sockaddr_dl *) ifa->ifa_addr;
-        guint8 mac_addr[6];
-
-        if ((ifa->ifa_flags & IFF_LOOPBACK))
-          continue;
-
-        if (!ifa->ifa_addr || ifa->ifa_addr->sa_family != AF_LINK)
-          continue;
-
-        if (ifaces) {
-          gchar **p = ifaces;
-          gboolean found = FALSE;
-
-          while (*p) {
-            if (strcmp (*p, ifa->ifa_name) == 0) {
-              found = TRUE;
-              break;
-            }
-            p++;
-          }
-
-          if (!found)
-            continue;
-        }
-
-        if (sdl->sdl_alen != 6)
-          continue;
-
-        memcpy (mac_addr, LLADDR (sdl), sdl->sdl_alen);
-
-        clock_id_array[0] = mac_addr[0];
-        clock_id_array[1] = mac_addr[1];
-        clock_id_array[2] = mac_addr[2];
-        clock_id_array[3] = 0xff;
-        clock_id_array[4] = 0xfe;
-        clock_id_array[5] = mac_addr[3];
-        clock_id_array[6] = mac_addr[4];
-        clock_id_array[7] = mac_addr[5];
-        success = TRUE;
-        break;
-      }
-
-      freeifaddrs (ifaddr);
-    }
-#else
-#warning "Implement something to get MAC addresses of network interfaces"
-#endif
-
-    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_error_matches (err, G_IO_ERROR, G_IO_ERROR_ADDRESS_IN_USE))
-        g_warning ("Couldn't join multicast group on interface '%s': %s", *ptr,
-            err->message);
-      else
-        c++;
-      g_clear_error (&err);
-
-      if (!g_socket_join_multicast_group (socket_general, mcast_addr, FALSE,
-              *ptr, &err)
-          && !g_error_matches (err, G_IO_ERROR, G_IO_ERROR_ADDRESS_IN_USE))
-        g_warning ("Couldn't join multicast group on interface '%s': %s", *ptr,
-            err->message);
-      else
-        c++;
-      g_clear_error (&err);
-
-      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);
-      g_clear_error (&err);
-      if (!g_socket_join_multicast_group (socket_general, mcast_addr, FALSE,
-              NULL, &err))
-        g_error ("Couldn't join multicast group: %s", err->message);
-      g_clear_error (&err);
-    }
-  } 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);
-    g_clear_error (&err);
-    if (!g_socket_join_multicast_group (socket_general, mcast_addr, FALSE, NULL,
-            &err))
-      g_error ("Couldn't join multicast group: %s", err->message);
-    g_clear_error (&err);
-  }
-
-  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);
-    g_clear_error (&err);
-  } 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);
-    g_clear_error (&err);
-  } 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);
-  }
-}
-
-#ifdef __APPLE__
-static gint
-dummy_poll (GPollFD * fds, guint nfds, gint timeout)
-{
-  return g_poll (fds, nfds, timeout);
-}
-#endif
-
-gint
-main (gint argc, gchar ** argv)
-{
-  GOptionContext *opt_ctx;
-  GMainLoop *loop;
-  GError *err = NULL;
-
-  /* FIXME: Work around some side effects of the changes from
-   * https://bugzilla.gnome.org/show_bug.cgi?id=741054
-   *
-   * The modified poll function somehow calls setugid(), which
-   * then abort()s the application. Make sure that we use g_poll()
-   * here!
-   */
-#ifdef __APPLE__
-  {
-    GMainContext *context = g_main_context_default ();
-    g_main_context_set_poll_func (context, dummy_poll);
-  }
-#endif
-
-#ifdef HAVE_PTP_HELPER_SETUID
-  if (setuid (0) < 0)
-    g_error ("not running with superuser privileges");
-#endif
-
-  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_clear_error (&err);
-  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/subprojects/gstreamer/libs/gst/helpers/meson.build b/subprojects/gstreamer/libs/gst/helpers/meson.build
index 543064ca139fcdb9b37079fc209119d5480eb7d2..7346e4de0aa2b7a7b44e1482d8c842c008c0bb47 100644
--- a/subprojects/gstreamer/libs/gst/helpers/meson.build
+++ b/subprojects/gstreamer/libs/gst/helpers/meson.build
@@ -1,3 +1,5 @@
+subdir('ptp')
+
 exe = executable('gst-plugin-scanner',
   'gst-plugin-scanner.c',
   c_args : gst_c_args,
@@ -24,107 +26,6 @@ if bashcomp_found
   )
 endif
 
-# Check PTP support
-have_ptp = false
-if host_system == 'android'
-  message('PTP not supported on Android because of permissions.')
-elif host_system == 'windows'
-  message('PTP not supported on Windows, not ported yet.')
-elif host_system == 'ios'
-  message('PTP not supported on iOS because of permissions.')
-elif ['linux', 'darwin', 'netbsd', 'freebsd', 'openbsd', 'kfreebsd', 'dragonfly', 'sunos', 'gnu', 'gnu/kfreebsd'].contains(host_system)
-  message('PTP supported on ' + host_system + '.')
-  have_ptp = true
-else
-  message('PTP not supported on ' + host_system + ', not ported yet.')
-endif
-
-if have_ptp
-  cdata.set('HAVE_PTP', 1, description : 'PTP support available')
-
-  if cc.compiles('''#include <sys/ioctl.h>
-                    #include <net/if.h>
-                    int some_func (void) {
-                      struct ifreq ifr;
-                      struct ifconf ifc;
-                      ioctl(0, SIOCGIFCONF, &ifc);
-                      ioctl(0, SIOCGIFFLAGS, &ifr);
-                      ioctl(0, SIOCGIFHWADDR, &ifr);
-                      return ifr.ifr_hwaddr.sa_data[0];
-                    }''',
-                 name : 'SIOCGIFCONF, SIOCGIFFLAGS and SIOCGIFHWADDR available')
-    cdata.set('HAVE_SIOCGIFCONF_SIOCGIFFLAGS_SIOCGIFHWADDR', 1,
-      description : 'SIOCGIFCONF, SIOCGIFFLAGS and SIOCGIFHWADDR is available')
-  endif
-
-  if cc.compiles('''#include <ifaddrs.h>
-                    #include <net/if.h>
-                    #include <net/if_dl.h>
-                    int some_func (void) {
-                      struct ifaddrs *ifaddr;
-                      getifaddrs(&ifaddr);
-                      return (ifaddr->ifa_flags & IFF_LOOPBACK) && ifaddr->ifa_addr->sa_family != AF_LINK;
-                    }''', name : 'getifaddrs() and AF_LINK available')
-    cdata.set('HAVE_GETIFADDRS_AF_LINK', 1,
-      description : 'getifaddrs() and AF_LINK is available')
-  endif
-
-  setcap_prog = find_program('setcap', '/usr/sbin/setcap', '/sbin/setcap', required : false)
-  cap_dep = dependency('libcap', required: false)
-
-  # user/group to change to in gst-ptp-helper
-  ptp_helper_setuid_user = get_option('ptp-helper-setuid-user')
-  if ptp_helper_setuid_user != ''
-    cdata.set_quoted('HAVE_PTP_HELPER_SETUID_USER', ptp_helper_setuid_user,
-      description : 'PTP helper setuid user')
-  endif
-  ptp_helper_setuid_group = get_option('ptp-helper-setuid-group')
-  if ptp_helper_setuid_group != ''
-    cdata.set_quoted('HAVE_PTP_HELPER_SETUID_GROUP', ptp_helper_setuid_group,
-      description : 'PTP helper setuid group')
-  endif
-
-  # how to install gst-ptp-helper
-  with_ptp_helper_permissions = get_option('ptp-helper-permissions')
-  if with_ptp_helper_permissions == 'auto'
-    if setcap_prog.found() and cap_dep.found()
-      with_ptp_helper_permissions = 'capabilities'
-    else
-      with_ptp_helper_permissions = 'setuid-root'
-    endif
-  endif
-  message('How to install gst-ptp-helper: ' + with_ptp_helper_permissions)
-
-  if with_ptp_helper_permissions == 'none'
-    # nothing to do
-  elif with_ptp_helper_permissions == 'setuid-root'
-    cdata.set('HAVE_PTP_HELPER_SETUID', 1,
-        description : 'Use setuid-root for permissions in PTP helper')
-  elif with_ptp_helper_permissions == 'capabilities'
-    if not setcap_prog.found()
-      error('capabilities-based ptp-helper-permissions requested, but could not find setcap tool.')
-    elif not cap_dep.found()
-      error('capabilities-based ptp-helper-permissions requested, but could not find libcap.')
-    endif
-    cdata.set('HAVE_PTP_HELPER_CAPABILITIES', 1,
-        description : 'Use capabilities for permissions in PTP helper')
-  else
-    error('Unexpected ptp helper permissions value: ' + with_ptp_helper_permissions)
-  endif
-
-  exe = executable('gst-ptp-helper', 'gst-ptp-helper.c',
-    c_args : gst_c_args,
-    include_directories : [configinc, libsinc],
-    dependencies : [gst_dep, gio_dep, mathlib, cap_dep],
-    install_dir : helpers_install_dir,
-    install : true)
-
-  meson.add_install_script('ptp_helper_post_install.sh',
-      helpers_install_dir, with_ptp_helper_permissions,
-      setcap_prog.found() ? setcap_prog.full_path() : '')
-  meson.add_devenv({'GST_PTP_HELPER': exe.full_path()})
-endif
-
 install_data(['gst_gdb.py', 'glib_gobject_helper.py'],
   install_dir : join_paths(get_option('datadir'), 'gstreamer-@0@'.format(apiversion), 'gdb'),
   install_tag : 'devel')
diff --git a/subprojects/gstreamer/libs/gst/helpers/ptp/args.rs b/subprojects/gstreamer/libs/gst/helpers/ptp/args.rs
new file mode 100644
index 0000000000000000000000000000000000000000..3b58de00c6d2ea5b7c8d1dda1c59345480b7a432
--- /dev/null
+++ b/subprojects/gstreamer/libs/gst/helpers/ptp/args.rs
@@ -0,0 +1,56 @@
+use std::env;
+
+use crate::{
+    bail,
+    error::{Context, Error},
+};
+
+/// Parsed command-line arguments.
+#[derive(Debug)]
+pub struct Args {
+    pub interfaces: Vec<String>,
+    pub verbose: bool,
+    pub clock_id: u64,
+}
+
+/// Parse the command-line arguments.
+pub fn parse_args() -> Result<Args, Error> {
+    let mut interfaces = Vec::new();
+    let mut verbose = false;
+    let mut clock_id = 0;
+
+    let mut args = env::args();
+    // Skip executable name
+    let _ = args.next();
+
+    while let Some(arg) = args.next() {
+        match arg.as_str() {
+            "-v" | "--verbose" => {
+                verbose = true;
+            }
+            "-i" | "--interface" => {
+                let iface = args.next().context("No interface following -i")?;
+                interfaces.push(iface);
+            }
+            "-c" | "--clock-id" => {
+                let clock_id_arg = args.next().context("No clock-id following -c")?;
+                clock_id = clock_id_arg.parse::<u64>().context("Invalid clock ID")?;
+            }
+            arg => {
+                bail!("Unknown command-line argument {}", arg);
+            }
+        }
+    }
+
+    let args = Args {
+        interfaces,
+        verbose,
+        clock_id,
+    };
+
+    if verbose {
+        eprintln!("Running with arguments {:#?}", args);
+    }
+
+    Ok(args)
+}
diff --git a/subprojects/gstreamer/libs/gst/helpers/ptp/conf_lib.rs.in b/subprojects/gstreamer/libs/gst/helpers/ptp/conf_lib.rs.in
new file mode 100644
index 0000000000000000000000000000000000000000..039b153c4b1a1b299f6d3eb3286ab8455e8fb271
--- /dev/null
+++ b/subprojects/gstreamer/libs/gst/helpers/ptp/conf_lib.rs.in
@@ -0,0 +1,2 @@
+pub const PTP_HELPER_SETUID_USER: Option<&str> = @PTP_HELPER_SETUID_USER@;
+pub const PTP_HELPER_SETUID_GROUP: Option<&str> = @PTP_HELPER_SETUID_GROUP@;
diff --git a/subprojects/gstreamer/libs/gst/helpers/ptp/error.rs b/subprojects/gstreamer/libs/gst/helpers/ptp/error.rs
new file mode 100644
index 0000000000000000000000000000000000000000..458ac8355e73f852e3c7d1baf7d1bc3ec96ee6ce
--- /dev/null
+++ b/subprojects/gstreamer/libs/gst/helpers/ptp/error.rs
@@ -0,0 +1,209 @@
+use std::{
+    error::Error as StdError,
+    fmt::{Debug, Display},
+};
+
+/// Custom error type for error display reasons.
+pub struct Error(Box<ErrorInner>);
+
+impl Error {
+    #[doc(hidden)]
+    pub fn new(message: String, source: Option<Box<dyn StdError + 'static>>) -> Self {
+        Error(Box::new(ErrorInner { message, source }))
+    }
+}
+
+struct ErrorInner {
+    message: String,
+    source: Option<Box<dyn StdError + 'static>>,
+}
+
+impl Debug for Error {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let mut e = self;
+        let mut first = true;
+
+        // Print the actual error message of this error and then iterate over the whole
+        // error chain and print each cause on the chain indented.
+        'next_error: loop {
+            if first {
+                writeln!(f, "{}", e.0.message)?;
+                first = false;
+            } else {
+                for line in e.0.message.lines() {
+                    writeln!(f, "  {}", line)?;
+                }
+            }
+
+            let mut source = match e.0.source {
+                Some(ref source) => &**source,
+                None => break 'next_error,
+            };
+
+            if let Some(source) = source.downcast_ref::<Error>() {
+                e = source;
+                writeln!(f, "\nCaused by:\n")?;
+                continue 'next_error;
+            }
+
+            loop {
+                writeln!(f, "\nCaused by:\n")?;
+                let source_str = source.to_string();
+                for line in source_str.lines() {
+                    writeln!(f, "  {}", line)?;
+                }
+
+                source = match source.source() {
+                    None => break 'next_error,
+                    Some(source) => source,
+                };
+
+                if let Some(source) = source.downcast_ref::<Error>() {
+                    e = source;
+                    writeln!(f, "\nCaused by:\n")?;
+                    continue 'next_error;
+                }
+            }
+        }
+
+        Ok(())
+    }
+}
+
+impl Display for Error {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        <Self as Debug>::fmt(self, f)
+    }
+}
+
+impl StdError for Error {
+    fn source(&self) -> Option<&(dyn StdError + 'static)> {
+        match self.0.source {
+            None => None,
+            Some(ref source) => Some(&**source),
+        }
+    }
+}
+
+impl<'a> From<&'a str> for Error {
+    fn from(message: &'a str) -> Self {
+        Error(Box::new(ErrorInner {
+            message: String::from(message),
+            source: None,
+        }))
+    }
+}
+
+impl From<String> for Error {
+    fn from(message: String) -> Self {
+        Error(Box::new(ErrorInner {
+            message,
+            source: None,
+        }))
+    }
+}
+
+#[macro_export]
+/// Create a new `Error` from the given message and possibly source error.
+macro_rules! format_err {
+    (source: $source:expr, $msg:literal $(,)?) => {
+        $crate::error::Error::new(
+            String::from($msg),
+            Some($source.into()),
+        )
+    };
+    (source: $source:expr, $err:expr $(,)?) => {
+        $crate::error::Error::new(
+            format!($err),
+            Some($source.into()),
+        )
+    };
+    (source: $source:expr, $fmt:expr, $($arg:tt)*) => {
+        $crate::error::Error::new(
+            format!($fmt, $($arg)*),
+            Some($source.into()),
+        )
+    };
+
+    ($msg:literal $(,)?) => {
+        $crate::error::Error::new(
+            String::from($msg),
+            None,
+        )
+    };
+    ($err:expr $(,)?) => {
+        $crate::error::Error::new(
+            format!($err),
+            None,
+        )
+    };
+    ($fmt:expr, $($arg:tt)*) => {
+        $crate::error::Error::new(
+            format!($fmt, $($arg)*),
+            None,
+        )
+    };
+}
+
+#[macro_export]
+/// Return new `Error` from the given message and possibly source error.
+macro_rules! bail {
+    ($($arg:tt)+) => {
+        return Err($crate::format_err!($($arg)+));
+    };
+}
+
+/// Trait for adding a context message to any `Result<T, E>` or `Option<T>`
+/// and turning it into a `Result<T, Error>`.
+pub trait Context<T, E> {
+    /// Add a static context.
+    ///
+    /// This should only be called if `context` requires no allocations or otherwise
+    /// exists already.
+    fn context<C>(self, context: C) -> Result<T, Error>
+    where
+        C: Display;
+
+    /// Add a lazily created context.
+    fn with_context<C, F>(self, func: F) -> Result<T, Error>
+    where
+        C: Display,
+        F: FnOnce() -> C;
+}
+
+impl<T, E> Context<T, E> for Result<T, E>
+where
+    E: StdError + 'static,
+{
+    fn context<C>(self, context: C) -> Result<T, Error>
+    where
+        C: Display,
+    {
+        self.map_err(|err| Error::new(context.to_string(), Some(Box::new(err))))
+    }
+
+    fn with_context<C, F>(self, func: F) -> Result<T, Error>
+    where
+        C: Display,
+        F: FnOnce() -> C,
+    {
+        self.map_err(|err| Error::new(func().to_string(), Some(Box::new(err))))
+    }
+}
+
+impl<T> Context<T, Error> for Option<T> {
+    fn context<C>(self, context: C) -> Result<T, Error>
+    where
+        C: Display,
+    {
+        self.ok_or_else(|| Error::new(context.to_string(), None))
+    }
+
+    fn with_context<C, F>(self, func: F) -> Result<T, Error>
+    where
+        C: Display,
+        F: FnOnce() -> C,
+    {
+        self.ok_or_else(|| Error::new(func().to_string(), None))
+    }
+}
diff --git a/subprojects/gstreamer/libs/gst/helpers/ptp/ffi.rs b/subprojects/gstreamer/libs/gst/helpers/ptp/ffi.rs
new file mode 100644
index 0000000000000000000000000000000000000000..c963ec79d95699fc12a91fabd40cafbccb0bfb09
--- /dev/null
+++ b/subprojects/gstreamer/libs/gst/helpers/ptp/ffi.rs
@@ -0,0 +1,805 @@
+#[cfg(unix)]
+#[allow(non_camel_case_types, non_upper_case_globals, non_snake_case)]
+pub mod unix {
+    use std::os::{raw::*, unix::io::RawFd};
+
+    #[cfg(not(any(
+        target_os = "linux",
+        target_os = "android",
+        target_os = "freebsd",
+        target_os = "openbsd",
+        target_os = "netbsd",
+        target_os = "dragonfly",
+        target_os = "macos",
+        target_os = "solaris",
+        target_os = "illumos",
+    )))]
+    compile_error!("Unsupported Operating System");
+
+    // Taken from libc 0.2.139
+    // XXX: Once meson has cargo subproject support all of the below can be replaced with the libc crate
+    pub const STDIN_FILENO: RawFd = 0;
+    pub const STDOUT_FILENO: RawFd = 1;
+    pub const O_RDONLY: c_int = 0;
+
+    pub const POLLIN: c_short = 0x1;
+    pub const POLLERR: c_short = 0x8;
+    pub const POLLHUP: c_short = 0x10;
+    pub const POLLNVAL: c_short = 0x20;
+
+    pub const IPPROTO_IP: c_int = 0;
+    #[cfg(any(
+        target_os = "freebsd",
+        target_os = "openbsd",
+        target_os = "netbsd",
+        target_os = "dragonfly",
+        target_os = "macos",
+    ))]
+    pub const IP_ADD_MEMBERSHIP: c_int = 12;
+    #[cfg(any(target_os = "linux", target_os = "android"))]
+    pub const IP_ADD_MEMBERSHIP: c_int = 35;
+    #[cfg(any(target_os = "solaris", target_os = "illumos"))]
+    pub const IP_ADD_MEMBERSHIP: c_int = 19;
+
+    #[cfg(any(
+        target_os = "solaris",
+        target_os = "illumos",
+        target_os = "freebsd",
+        target_os = "openbsd",
+        target_os = "netbsd",
+        target_os = "dragonfly",
+        target_os = "macos",
+    ))]
+    pub const SOL_SOCKET: c_int = 0xffff;
+    #[cfg(all(
+        target_os = "linux",
+        any(
+            target_arch = "sparc",
+            target_arch = "sparc64",
+            target_arch = "mips",
+            target_arch = "mips64"
+        )
+    ))]
+    pub const SOL_SOCKET: c_int = 0xffff;
+    #[cfg(all(any(
+        all(
+            target_os = "linux",
+            not(any(
+                target_arch = "sparc",
+                target_arch = "sparc64",
+                target_arch = "mips",
+                target_arch = "mips64"
+            ))
+        ),
+        target_os = "android",
+    )))]
+    pub const SOL_SOCKET: c_int = 1;
+
+    #[cfg(any(
+        target_os = "solaris",
+        target_os = "illumos",
+        target_os = "freebsd",
+        target_os = "openbsd",
+        target_os = "netbsd",
+        target_os = "dragonfly",
+        target_os = "macos",
+    ))]
+    pub const SO_REUSEADDR: c_int = 0x4;
+    #[cfg(all(
+        target_os = "linux",
+        any(
+            target_arch = "sparc",
+            target_arch = "sparc64",
+            target_arch = "mips",
+            target_arch = "mips64"
+        )
+    ))]
+    pub const SO_REUSEADDR: c_int = 0x4;
+    #[cfg(all(any(
+        all(
+            target_os = "linux",
+            not(any(
+                target_arch = "sparc",
+                target_arch = "sparc64",
+                target_arch = "mips",
+                target_arch = "mips64"
+            ))
+        ),
+        target_os = "android",
+    )))]
+    pub const SO_REUSEADDR: c_int = 2;
+
+    #[cfg(any(
+        target_os = "freebsd",
+        target_os = "openbsd",
+        target_os = "netbsd",
+        target_os = "dragonfly",
+        target_os = "macos",
+    ))]
+    pub const SO_REUSEPORT: c_int = 0x200;
+    #[cfg(all(
+        target_os = "linux",
+        any(
+            target_arch = "sparc",
+            target_arch = "sparc64",
+            target_arch = "mips",
+            target_arch = "mips64"
+        )
+    ))]
+    pub const SO_REUSEPORT: c_int = 0x200;
+    #[cfg(all(any(
+        all(
+            target_os = "linux",
+            not(any(
+                target_arch = "sparc",
+                target_arch = "sparc64",
+                target_arch = "mips",
+                target_arch = "mips64"
+            ))
+        ),
+        target_os = "android",
+    )))]
+    pub const SO_REUSEPORT: c_int = 15;
+
+    pub const AF_INET: c_int = 2;
+    #[cfg(any(
+        target_os = "freebsd",
+        target_os = "openbsd",
+        target_os = "netbsd",
+        target_os = "dragonfly",
+        target_os = "macos",
+    ))]
+    pub const AF_LINK: c_int = 18;
+    #[cfg(any(target_os = "solaris", target_os = "illumos"))]
+    pub const AF_LINK: c_int = 25;
+    #[cfg(any(target_os = "linux", target_os = "android"))]
+    pub const AF_PACKET: c_int = 17;
+
+    pub const IFF_UP: c_int = 0x1;
+    pub const IFF_LOOPBACK: c_int = 0x8;
+
+    #[cfg(any(target_os = "linux", target_os = "android"))]
+    pub const IFF_MULTICAST: c_int = 0x1000;
+    #[cfg(any(target_os = "solaris", target_os = "illumos"))]
+    pub const IFF_MULTICAST: ::c_int = 0x0800;
+    #[cfg(any(
+        target_os = "freebsd",
+        target_os = "openbsd",
+        target_os = "netbsd",
+        target_os = "dragonfly",
+        target_os = "macos",
+    ))]
+    pub const IFF_MULTICAST: c_int = 0x08000;
+
+    #[cfg(any(target_os = "solaris", target_os = "illumos"))]
+    pub const IF_NAMESIZE: usize = 32;
+    #[cfg(not(any(
+        target_os = "linux",
+        target_os = "android",
+        target_os = "solaris",
+        target_os = "illumos"
+    )))]
+    pub const IF_NAMESIZE: usize = 16;
+
+    extern "C" {
+        #[cfg_attr(
+            all(target_os = "macos", target_arch = "x86"),
+            link_name = "open$UNIX2003"
+        )]
+        pub fn open(path: *const u8, oflag: c_int, ...) -> i32;
+        #[cfg_attr(
+            all(target_os = "macos", target_arch = "x86"),
+            link_name = "read$UNIX2003"
+        )]
+        pub fn read(fd: RawFd, buf: *mut u8, count: usize) -> isize;
+        #[cfg_attr(
+            all(target_os = "macos", target_arch = "x86"),
+            link_name = "write$UNIX2003"
+        )]
+        pub fn write(fd: RawFd, buf: *const u8, count: usize) -> isize;
+
+        #[cfg_attr(
+            all(target_os = "macos", target_arch = "x86"),
+            link_name = "close$UNIX2003"
+        )]
+        pub fn close(fd: c_int) -> c_int;
+
+        #[cfg_attr(
+            all(target_os = "macos", target_arch = "x86"),
+            link_name = "poll$UNIX2003"
+        )]
+        pub fn poll(fds: *mut pollfd, nfds: nfds_t, timeout: c_int) -> c_int;
+
+        #[cfg(not(any(target_os = "solaris", target_os = "illumos")))]
+        pub fn if_nametoindex(name: *const c_char) -> c_uint;
+        #[cfg(not(any(target_os = "solaris", target_os = "illumos")))]
+        pub fn setsockopt(
+            socket: c_int,
+            level: c_int,
+            name: c_int,
+            value: *const c_void,
+            option_len: u32,
+        ) -> c_int;
+
+        pub fn getifaddrs(ifap: *mut *mut ifaddrs) -> c_int;
+        pub fn freeifaddrs(ifa: *mut ifaddrs);
+    }
+
+    #[cfg(any(
+        target_os = "linux",
+        target_os = "android",
+        target_os = "solaris",
+        target_os = "illumos",
+    ))]
+    pub type nfds_t = c_ulong;
+    #[cfg(not(any(
+        target_os = "linux",
+        target_os = "android",
+        target_os = "solaris",
+        target_os = "illumos",
+    )))]
+    pub type nfds_t = c_uint;
+
+    #[repr(C)]
+    #[derive(Clone, Copy)]
+    pub struct pollfd {
+        pub fd: c_int,
+        pub events: c_short,
+        pub revents: c_short,
+    }
+
+    pub type in_addr_t = u32;
+
+    #[repr(C)]
+    #[derive(Copy, Clone)]
+    pub struct in_addr {
+        pub s_addr: in_addr_t,
+    }
+
+    // Solaris does not have support for this so we fall back to use the std API
+    #[cfg(not(any(target_os = "solaris", target_os = "illumos")))]
+    #[repr(C)]
+    #[derive(Copy, Clone)]
+    pub struct ip_mreqn {
+        pub imr_multiaddr: in_addr,
+        pub imr_address: in_addr,
+        pub imr_ifindex: c_int,
+    }
+
+    #[cfg(any(target_os = "solaris", target_os = "illumos"))]
+    #[repr(C)]
+    pub struct ifaddrs {
+        pub ifa_next: *mut ifaddrs,
+        pub ifa_name: *mut c_char,
+        pub ifa_flags: c_ulong,
+        pub ifa_addr: *mut sockaddr,
+        pub ifa_netmask: *mut sockaddr,
+        pub ifa_dstaddr: *mut sockaddr,
+        pub ifa_data: *mut c_void,
+    }
+
+    #[cfg(any(target_os = "linux", target_os = "android"))]
+    #[repr(C)]
+    pub struct ifaddrs {
+        pub ifa_next: *mut ifaddrs,
+        pub ifa_name: *mut c_char,
+        pub ifa_flags: c_uint,
+        pub ifa_addr: *mut sockaddr,
+        pub ifa_netmask: *mut sockaddr,
+        pub ifa_ifu: *mut sockaddr,
+        pub ifa_data: *mut c_void,
+    }
+
+    #[cfg(any(
+        target_os = "freebsd",
+        target_os = "openbsd",
+        target_os = "netbsd",
+        target_os = "dragonfly",
+        target_os = "macos",
+    ))]
+    #[repr(C)]
+    pub struct ifaddrs {
+        pub ifa_next: *mut ifaddrs,
+        pub ifa_name: *mut c_char,
+        pub ifa_flags: c_uint,
+        pub ifa_addr: *mut sockaddr,
+        pub ifa_netmask: *mut sockaddr,
+        pub ifa_dstaddr: *mut sockaddr,
+        pub ifa_data: *mut c_void,
+        #[cfg(target_os = "netbsd")]
+        pub ifa_addrflags: c_uint,
+    }
+
+    #[cfg(any(
+        target_os = "linux",
+        target_os = "android",
+        target_os = "solaris",
+        target_os = "illumos",
+    ))]
+    #[repr(C)]
+    pub struct sockaddr {
+        pub sa_family: u16,
+        pub sa_data: [u8; 14],
+    }
+
+    #[cfg(any(
+        target_os = "freebsd",
+        target_os = "openbsd",
+        target_os = "netbsd",
+        target_os = "dragonfly",
+        target_os = "macos",
+    ))]
+    #[repr(C)]
+    pub struct sockaddr {
+        pub sa_len: u8,
+        pub sa_family: u8,
+        pub sa_data: [u8; 14],
+    }
+
+    #[cfg(any(
+        target_os = "linux",
+        target_os = "android",
+        target_os = "solaris",
+        target_os = "illumos",
+    ))]
+    #[repr(C)]
+    pub struct sockaddr_in {
+        pub sin_family: u16,
+        pub sin_port: u16,
+        pub sin_addr: in_addr,
+        pub sin_zero: [u8; 8],
+    }
+    #[cfg(any(
+        target_os = "freebsd",
+        target_os = "openbsd",
+        target_os = "netbsd",
+        target_os = "dragonfly",
+        target_os = "macos",
+    ))]
+    #[repr(C)]
+    pub struct sockaddr_in {
+        pub sin_len: u8,
+        pub sin_family: u8,
+        pub sin_port: u16,
+        pub sin_addr: in_addr,
+        pub sin_zero: [u8; 8],
+    }
+
+    #[cfg(any(target_os = "solaris", target_os = "illumos"))]
+    #[repr(C)]
+    pub struct sockaddr_dl {
+        pub sdl_family: u16,
+        pub sdl_index: u16,
+        pub sdl_type: u8,
+        pub sdl_nlen: u8,
+        pub sdl_alen: u8,
+        pub sdl_slen: u8,
+        pub sdl_data: [u8; 244],
+    }
+
+    #[cfg(any(target_os = "netbsd", target_os = "macos"))]
+    #[repr(C)]
+    pub struct sockaddr_dl {
+        pub sdl_len: u8,
+        pub sdl_family: u8,
+        pub sdl_index: u16,
+        pub sdl_type: u8,
+        pub sdl_nlen: u8,
+        pub sdl_alen: u8,
+        pub sdl_slen: u8,
+        pub sdl_data: [u8; 12],
+    }
+
+    #[cfg(target_os = "openbsd")]
+    #[repr(C)]
+    pub struct sockaddr_dl {
+        pub sdl_len: u8,
+        pub sdl_family: u8,
+        pub sdl_index: u16,
+        pub sdl_type: u8,
+        pub sdl_nlen: u8,
+        pub sdl_alen: u8,
+        pub sdl_slen: u8,
+        pub sdl_data: [u8; 24],
+    }
+
+    #[cfg(target_os = "freebsd")]
+    #[repr(C)]
+    pub struct sockaddr_dl {
+        pub sdl_len: u8,
+        pub sdl_family: u8,
+        pub sdl_index: u16,
+        pub sdl_type: u8,
+        pub sdl_nlen: u8,
+        pub sdl_alen: u8,
+        pub sdl_slen: u8,
+        pub sdl_data: [u8; 46],
+    }
+
+    #[cfg(target_os = "dragonfly")]
+    #[repr(C)]
+    pub struct sockaddr_dl {
+        pub sdl_len: u8,
+        pub sdl_family: u8,
+        pub sdl_index: u16,
+        pub sdl_type: u8,
+        pub sdl_nlen: u8,
+        pub sdl_alen: u8,
+        pub sdl_slen: u8,
+        pub sdl_data: [u8; 12],
+        pub sdl_rcf: u16,
+        pub sdl_route: [u8; 16],
+    }
+
+    #[cfg(any(target_os = "linux", target_os = "android"))]
+    #[repr(C)]
+    pub struct sockaddr_ll {
+        pub sll_family: u16,
+        pub sll_protocol: u16,
+        pub sll_ifindex: u32,
+        pub sll_hatype: u16,
+        pub sll_pkttype: u8,
+        pub sll_halen: u8,
+        pub sll_addr: [u8; 8],
+    }
+
+    #[cfg(any(target_os = "android", target_os = "linux"))]
+    pub mod linux {
+        pub use super::*;
+
+        #[cfg(target_arch = "x86")]
+        pub const SYS_getrandom: c_ulong = 355;
+        #[cfg(all(target_arch = "x86_64", target_pointer_width = "32"))]
+        pub const SYS_getrandom: c_ulong = 0x40000000 + 318;
+        #[cfg(all(target_arch = "x86_64", target_pointer_width = "64"))]
+        pub const SYS_getrandom: c_ulong = 318;
+        #[cfg(target_arch = "arm")]
+        pub const SYS_getrandom: c_ulong = 384;
+        #[cfg(target_arch = "aarch64")]
+        pub const SYS_getrandom: c_ulong = 278;
+        #[cfg(target_arch = "mips")]
+        pub const SYS_getrandom: c_ulong = 4000 + 353;
+        #[cfg(target_arch = "mips64")]
+        pub const SYS_getrandom: c_ulong = 5000 + 313;
+        #[cfg(any(
+            target_arch = "riscv32",
+            target_arch = "riscv64",
+            target_arch = "loongarch64"
+        ))]
+        pub const SYS_getrandom: c_ulong = 278;
+        #[cfg(any(target_arch = "powerpc", target_arch = "powerpc64"))]
+        pub const SYS_getrandom: c_ulong = 359;
+        #[cfg(any(target_arch = "sparc", target_arch = "sparc64"))]
+        pub const SYS_getrandom: c_ulong = 347;
+        #[cfg(target_arch = "s390x")]
+        pub const SYS_getrandom: c_ulong = 349;
+        #[cfg(target_arch = "m68k")]
+        pub const SYS_getrandom: c_ulong = 352;
+        #[cfg(not(any(
+            target_arch = "x86",
+            target_arch = "x86_64",
+            target_arch = "arm",
+            target_arch = "aarch64",
+            target_arch = "mips",
+            target_arch = "mips64",
+            target_arch = "riscv32",
+            target_arch = "riscv64",
+            target_arch = "loongarch64",
+            target_arch = "powerpc",
+            target_arch = "powerpc64",
+            target_arch = "sparc",
+            target_arch = "sparc64",
+            target_arch = "s390x",
+            target_arch = "m68k",
+        )))]
+        pub const SYS_getrandom: c_ulong = 0;
+
+        extern "C" {
+            pub fn syscall(num: c_ulong, ...) -> c_long;
+        }
+    }
+
+    #[cfg(ptp_helper_permissions = "setcap")]
+    pub mod setcaps {
+        use super::*;
+
+        pub type cap_t = *mut c_void;
+
+        #[link(name = "cap")]
+        extern "C" {
+            pub fn cap_clear(c: cap_t) -> c_int;
+            pub fn cap_get_proc() -> cap_t;
+            pub fn cap_set_proc(c: cap_t) -> c_int;
+            pub fn cap_free(c: cap_t) -> c_int;
+        }
+    }
+
+    #[cfg(ptp_helper_permissions = "setuid-root")]
+    pub mod setuid_root {
+        use super::*;
+
+        pub type uid_t = u32;
+        pub type gid_t = u32;
+
+        #[repr(C)]
+        pub struct passwd {
+            pub pw_name: *mut c_char,
+            pub pw_passwd: *mut c_char,
+            pub pw_uid: uid_t,
+            pub pw_gid: gid_t,
+            // More fields following here
+            truncated: c_void,
+        }
+
+        #[repr(C)]
+        pub struct group {
+            pub gr_name: *mut c_char,
+            pub gr_passwd: *mut c_char,
+            pub gr_gid: gid_t,
+            // More fields following here
+            truncated: c_void,
+        }
+
+        extern "C" {
+            #[cfg_attr(target_os = "netbsd", link_name = "__getpwnam50")]
+            pub fn getpwnam(name: *const c_char) -> *mut passwd;
+            pub fn getgrnam(name: *const c_char) -> *mut group;
+            pub fn setgid(gid: gid_t) -> c_int;
+            pub fn getgid() -> gid_t;
+            pub fn setuid(gid: gid_t) -> c_int;
+        }
+    }
+}
+
+#[cfg(windows)]
+#[allow(non_camel_case_types, non_upper_case_globals, non_snake_case)]
+pub mod windows {
+    use std::os::{
+        raw::*,
+        windows::raw::{HANDLE, SOCKET},
+    };
+
+    pub const STD_INPUT_HANDLE: i32 = -10;
+    pub const STD_OUTPUT_HANDLE: i32 = -11;
+
+    pub const FILE_TYPE_CHAR: u32 = 0x0002;
+    pub const FILE_TYPE_PIPE: u32 = 0x0003;
+
+    #[link(name = "kernel32")]
+    extern "system" {
+        pub fn GetStdHandle(nstdhandle: i32) -> HANDLE;
+
+        pub fn ReadFile(
+            hfile: HANDLE,
+            lpbuffer: *mut u8,
+            nnumberofbytestoread: u32,
+            lpnumberofbytesread: *mut u32,
+            lpoverlapped: *mut c_void,
+        ) -> i32;
+
+        pub fn WriteFile(
+            hfile: HANDLE,
+            lpbuffer: *const u8,
+            nnumberofbytestowrite: u32,
+            lpnumberofbyteswritten: *mut u32,
+            lpoverlapped: *mut c_void,
+        ) -> i32;
+
+        pub fn WaitForMultipleObjects(
+            ncount: u32,
+            lphandles: *const HANDLE,
+            bwaitall: i32,
+            dwmilliseconds: u32,
+        ) -> u32;
+
+        pub fn SetConsoleMode(hconsolehandle: HANDLE, dwmode: u32) -> i32;
+        pub fn FlushConsoleInputBuffer(hconsolehandle: HANDLE) -> i32;
+
+        pub fn CreateEventA(
+            lpeventattributes: *const c_void,
+            bmanualreset: i32,
+            binitialstate: i32,
+            lpname: *const u8,
+        ) -> HANDLE;
+        pub fn SetEvent(hevent: HANDLE) -> i32;
+        pub fn ResetEvent(hevent: HANDLE) -> i32;
+        pub fn CloseHandle(hobject: HANDLE) -> i32;
+
+        pub fn GetFileType(hfile: HANDLE) -> u32;
+
+        pub fn GetProcessHeap() -> isize;
+        pub fn HeapAlloc(hheap: isize, dwflags: u32, dwbytes: usize) -> *mut c_void;
+        pub fn HeapFree(hheap: isize, dwflags: u32, lpmem: *mut c_void) -> i32;
+        pub fn HeapReAlloc(
+            hheap: isize,
+            dwflags: u32,
+            lpmem: *mut c_void,
+            dwbytes: usize,
+        ) -> *mut c_void;
+    }
+
+    pub const BCRYPT_USE_SYSTEM_PREFERRED_RNG: u32 = 0x00000002;
+
+    #[link(name = "bcrypt")]
+    extern "system" {
+        pub fn BCryptGenRandom(
+            hAlgorithm: *mut c_void,
+            pBuffer: *mut u8,
+            cbBuffer: u32,
+            dwFlags: u32,
+        ) -> u32;
+    }
+
+    pub const FD_READ: u32 = 1;
+    pub const FD_READ_BIT: usize = 0;
+
+    #[repr(C)]
+    #[derive(Copy, Clone)]
+    pub struct WSANETWORKEVENTS {
+        pub lnetworkevents: u32,
+        pub ierrorcode: [i32; 10],
+    }
+
+    pub const IPPROTO_IP: u32 = 0u32;
+    pub const IP_ADD_MEMBERSHIP: u32 = 12u32;
+
+    pub const SOL_SOCKET: u32 = 65535;
+    pub const SO_REUSEADDR: u32 = 4;
+
+    #[repr(C)]
+    #[derive(Copy, Clone)]
+    pub struct IN_ADDR_0_0 {
+        pub s_b1: u8,
+        pub s_b2: u8,
+        pub s_b3: u8,
+        pub s_b4: u8,
+    }
+
+    #[repr(C)]
+    #[derive(Copy, Clone)]
+    pub struct IN_ADDR_0_1 {
+        pub s_w1: u16,
+        pub s_w2: u16,
+    }
+
+    #[repr(C)]
+    #[derive(Copy, Clone)]
+    pub union IN_ADDR_0 {
+        pub S_un_b: IN_ADDR_0_0,
+        pub S_un_w: IN_ADDR_0_1,
+        pub S_addr: u32,
+    }
+
+    #[repr(C)]
+    #[derive(Copy, Clone)]
+    pub struct IN_ADDR {
+        pub S_un: IN_ADDR_0,
+    }
+
+    #[repr(C)]
+    #[derive(Copy, Clone)]
+    pub struct IP_MREQ {
+        pub imr_multiaddr: IN_ADDR,
+        pub imr_address: IN_ADDR,
+    }
+
+    #[link(name = "ws2_32")]
+    extern "system" {
+        pub fn WSAEventSelect(s: SOCKET, heventobject: HANDLE, lnetworkevents: u32) -> i32;
+        pub fn WSAEnumNetworkEvents(
+            s: SOCKET,
+            heventobject: HANDLE,
+            lpnetworkevents: *mut WSANETWORKEVENTS,
+        ) -> i32;
+        pub fn WSACreateEvent() -> HANDLE;
+        pub fn WSACloseEvent(hevent: HANDLE) -> i32;
+        pub fn WSAGetLastError() -> i32;
+
+        pub fn setsockopt(
+            socket: SOCKET,
+            level: i32,
+            name: i32,
+            value: *const c_void,
+            option_len: i32,
+        ) -> i32;
+    }
+
+    pub const AF_INET: u32 = 2;
+
+    pub const GAA_FLAG_SKIP_ANYCAST: u32 = 0x0002;
+    pub const GAA_FLAG_SKIP_MULTICAST: u32 = 0x0004;
+    pub const GAA_FLAG_SKIP_DNS_SERVER: u32 = 0x0008;
+
+    pub const ADAPTER_FLAG_RECEIVE_ONLY: u32 = 0x08;
+    pub const ADAPTER_FLAG_NO_MULTICAST: u32 = 0x10;
+    pub const ADAPTER_FLAG_IPV4_ENABLED: u32 = 0x80;
+
+    pub const IF_TYPE_SOFTWARE_LOOPBACK: u32 = 24;
+
+    pub const IF_OPER_STATUS_UP: u32 = 1;
+
+    pub const ERROR_NOT_ENOUGH_MEMORY: u32 = 8u32;
+
+    #[repr(C)]
+    #[derive(Copy, Clone)]
+    pub struct IP_ADAPTER_ADDRESSES_LH_0_0 {
+        pub length: u32,
+        pub ifindex: u32,
+    }
+
+    #[repr(C)]
+    #[derive(Copy, Clone)]
+    pub union IP_ADAPTER_ADDRESSES_LH_0 {
+        pub alignment: u64,
+        pub anonymous: IP_ADAPTER_ADDRESSES_LH_0_0,
+    }
+
+    #[repr(C)]
+    #[derive(Copy, Clone)]
+    pub struct IP_ADAPTER_UNICAST_ADDRESS_LH_0_0 {
+        pub length: u32,
+        pub ifindex: u32,
+    }
+
+    #[repr(C)]
+    #[derive(Copy, Clone)]
+    pub union IP_ADAPTER_UNICAST_ADDRESS_LH_0 {
+        pub alignment: u64,
+        pub anonymous: IP_ADAPTER_UNICAST_ADDRESS_LH_0_0,
+    }
+
+    #[repr(C)]
+    pub struct SOCKADDR {
+        pub sa_family: u16,
+        pub sin_port: u16,
+        pub in_addr: IN_ADDR,
+    }
+
+    #[repr(C)]
+    pub struct SOCKET_ADDRESS {
+        pub lpsocketaddr: *mut SOCKADDR,
+        pub isocketaddrlength: i32,
+    }
+
+    #[repr(C)]
+    pub struct IP_ADAPTER_UNICAST_ADDRESS_LH {
+        pub anonymous: IP_ADAPTER_UNICAST_ADDRESS_LH_0,
+        pub next: *mut IP_ADAPTER_UNICAST_ADDRESS_LH,
+        pub address: SOCKET_ADDRESS,
+        // More fields following here
+        truncated: c_void,
+    }
+
+    #[repr(C)]
+    pub struct IP_ADAPTER_ADDRESSES_LH {
+        pub anonymous: IP_ADAPTER_ADDRESSES_LH_0,
+        pub next: *mut IP_ADAPTER_ADDRESSES_LH,
+        pub adaptername: *const u8,
+        pub firstunicastaddress: *mut IP_ADAPTER_UNICAST_ADDRESS_LH,
+        pub firstanycastaddress: *mut c_void,
+        pub firstmulticastaddress: *mut c_void,
+        pub firstdnsserveraddress: *mut c_void,
+        pub dnssuffix: *const u16,
+        pub description: *const u16,
+        pub friendlyname: *const u16,
+        pub physicaladdress: [u8; 8],
+        pub physicaladdresslength: u32,
+        pub flags: u32,
+        pub mtu: u32,
+        pub iftype: u32,
+        pub operstatus: u32,
+        // More fields following here
+        truncated: c_void,
+    }
+
+    #[link(name = "iphlpapi")]
+    extern "system" {
+        pub fn GetAdaptersAddresses(
+            family: u32,
+            flags: u32,
+            reserved: *mut c_void,
+            adapteraddresses: *mut IP_ADAPTER_ADDRESSES_LH,
+            sizepointer: *mut u32,
+        ) -> u32;
+    }
+}
diff --git a/subprojects/gstreamer/libs/gst/helpers/ptp/io.rs b/subprojects/gstreamer/libs/gst/helpers/ptp/io.rs
new file mode 100644
index 0000000000000000000000000000000000000000..407bc9c659e1ac7b520e18d5124997659bec325b
--- /dev/null
+++ b/subprojects/gstreamer/libs/gst/helpers/ptp/io.rs
@@ -0,0 +1,759 @@
+#[cfg(unix)]
+mod imp {
+    use std::{
+        io::{self, Read, Write},
+        net::UdpSocket,
+        os::unix::io::{AsRawFd, RawFd},
+    };
+
+    use crate::{bail, error::Error, ffi::unix::*};
+
+    /// Inputs and outputs, and allowing to poll the inputs for available data.
+    ///
+    /// This carries the event/general UDP socket and stdin/stdout.
+    pub struct Poll {
+        event_socket: UdpSocket,
+        general_socket: UdpSocket,
+        stdin: Stdin,
+        stdout: Stdout,
+    }
+
+    /// Result of polling the inputs of the `Poll`.
+    ///
+    /// Any input that has data available for reading will be set to `true`, potentially multiple
+    /// at once.
+    ///
+    /// Note that reading from the sockets is non-blocking but reading from stdin is blocking so
+    /// special care has to be taken to only read as much as is available.
+    pub struct PollResult {
+        pub event_socket: bool,
+        pub general_socket: bool,
+        pub stdin: bool,
+    }
+
+    impl Poll {
+        /// Name of the input based on the `struct pollfd` index.
+        fn fd_name(idx: usize) -> &'static str {
+            match idx {
+                0 => "event socket",
+                1 => "general socket",
+                2 => "stdin",
+                _ => unreachable!(),
+            }
+        }
+
+        /// Create a new `Poll` instance from the two sockets.
+        pub fn new(event_socket: UdpSocket, general_socket: UdpSocket) -> Result<Self, Error> {
+            let stdin = Stdin::acquire();
+            let stdout = Stdout::acquire();
+
+            Ok(Self {
+                event_socket,
+                general_socket,
+                stdin,
+                stdout,
+            })
+        }
+
+        /// Mutable reference to the event socket.
+        pub fn event_socket(&mut self) -> &mut UdpSocket {
+            &mut self.event_socket
+        }
+
+        /// Mutable reference to the general socket.
+        pub fn general_socket(&mut self) -> &mut UdpSocket {
+            &mut self.general_socket
+        }
+
+        /// Mutable reference to stdin for reading.
+        pub fn stdin(&mut self) -> &mut Stdin {
+            &mut self.stdin
+        }
+
+        /// Mutable reference to stdout for writing.
+        pub fn stdout(&mut self) -> &mut Stdout {
+            &mut self.stdout
+        }
+
+        /// Poll the event socket, general socket and stdin for available data to read.
+        ///
+        /// This blocks until at least one input has data available.
+        pub fn poll(&mut self) -> Result<PollResult, Error> {
+            let mut pollfd = [
+                pollfd {
+                    fd: self.event_socket.as_raw_fd(),
+                    events: POLLIN,
+                    revents: 0,
+                },
+                pollfd {
+                    fd: self.general_socket.as_raw_fd(),
+                    events: POLLIN,
+                    revents: 0,
+                },
+                pollfd {
+                    fd: self.stdin.0,
+                    events: POLLIN,
+                    revents: 0,
+                },
+            ];
+
+            // SAFETY: Polls the given pollfds above and requires a valid number to be passed.
+            // A negative timeout means that it will wait until at least one of the pollfds is
+            // ready.
+            //
+            // Will return -1 on error, otherwise the number of ready pollfds. This can never be
+            // zero as a non-empty set of pollfds is passed.
+            //
+            // On EINTR polling should be retried.
+            unsafe {
+                loop {
+                    let res = poll(pollfd[..].as_mut_ptr(), pollfd.len() as _, -1);
+                    if res == -1 {
+                        let err = std::io::Error::last_os_error();
+                        if err.kind() == std::io::ErrorKind::Interrupted {
+                            continue;
+                        }
+                        bail!(source: err, "Failed polling");
+                    }
+                    assert_ne!(res, 0);
+                    break;
+                }
+            }
+
+            // Check for errors or hangup first
+            for (idx, pfd) in pollfd.iter().enumerate() {
+                if pfd.revents & (POLLERR | POLLNVAL) != 0 {
+                    bail!("Poll error on {}", Self::fd_name(idx));
+                }
+
+                if pfd.revents & POLLHUP != 0 {
+                    bail!("Hang up during polling on {}", Self::fd_name(idx));
+                }
+            }
+
+            Ok(PollResult {
+                event_socket: pollfd[0].revents & POLLIN != 0,
+                general_socket: pollfd[1].revents & POLLIN != 0,
+                stdin: pollfd[2].revents & POLLIN != 0,
+            })
+        }
+    }
+
+    /// Raw, unbuffered handle to `stdin`.
+    ///
+    /// This implements the `Read` trait for reading.
+    pub struct Stdin(RawFd);
+
+    impl Stdin {
+        fn acquire() -> Self {
+            Stdin(STDIN_FILENO)
+        }
+    }
+
+    impl Read for Stdin {
+        fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
+            // SAFETY: read() requires a valid fd and a mutable buffer with the given size.
+            // The fd is valid by construction as is the buffer.
+            //
+            // read() will return the number of bytes read or a negative value on errors.
+            let res = unsafe { read(self.0, buf.as_mut_ptr(), buf.len()) };
+
+            if res < 0 {
+                Err(std::io::Error::last_os_error())
+            } else {
+                Ok(res as usize)
+            }
+        }
+    }
+
+    /// Raw, unbuffered handle to `stdout`.
+    ///
+    /// This implements the `Write` trait for writing.
+    pub struct Stdout(RawFd);
+
+    impl Stdout {
+        fn acquire() -> Self {
+            Stdout(STDOUT_FILENO)
+        }
+    }
+
+    impl Write for Stdout {
+        fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+            // SAFETY: write() requires a valid fd and a mutable buffer with the given size.
+            // The fd is valid by construction as is the buffer.
+            //
+            // write() will return the number of bytes written or a negative value on errors.
+            let res = unsafe { write(self.0, buf.as_ptr(), buf.len()) };
+
+            if res == -1 {
+                Err(std::io::Error::last_os_error())
+            } else {
+                Ok(res as usize)
+            }
+        }
+
+        fn flush(&mut self) -> io::Result<()> {
+            Ok(())
+        }
+    }
+}
+
+#[cfg(windows)]
+mod imp {
+    use std::{
+        cmp,
+        io::{self, Read, Write},
+        mem,
+        net::UdpSocket,
+        os::windows::{io::AsRawSocket, raw::HANDLE},
+        ptr,
+        sync::{Arc, Condvar, Mutex},
+        thread,
+    };
+
+    use crate::{
+        bail,
+        error::{Context, Error},
+        ffi::windows::*,
+    };
+
+    /// Inputs and outputs, and allowing to poll the inputs for available data.
+    ///
+    /// This carries the event/general UDP socket and stdin/stdout.
+    pub struct Poll {
+        event_socket: UdpSocket,
+        event_socket_event: EventHandle,
+        general_socket: UdpSocket,
+        general_socket_event: EventHandle,
+        stdin: Stdin,
+        stdout: Stdout,
+    }
+
+    /// Helper struct for a WSA event.
+    struct EventHandle(HANDLE);
+
+    impl EventHandle {
+        fn new() -> io::Result<Self> {
+            // SAFETY: WSACreateEvent() returns 0 on error or otherwise a valid WSA event
+            // that has to be closed again later.
+            unsafe {
+                let event = WSACreateEvent();
+                if event.is_null() || event as isize == -1 {
+                    Err(io::Error::from_raw_os_error(WSAGetLastError()))
+                } else {
+                    Ok(EventHandle(event))
+                }
+            }
+        }
+    }
+
+    impl Drop for EventHandle {
+        fn drop(&mut self) {
+            // SAFETY: The event is valid by construction and dropped at most once, so can be
+            // safely closed here..
+            //
+            // The return value is intentionally ignored as nothing else can be done
+            // on errors anyway.
+            unsafe {
+                let _ = WSACloseEvent(self.0);
+            }
+        }
+    }
+
+    /// Result of polling the inputs of the `Poll`.
+    ///
+    /// Any input that has data available for reading will be set to `true`, potentially multiple
+    /// at once.
+    ///
+    /// Note that reading from the sockets is non-blocking but reading from stdin is blocking so
+    /// special care has to be taken to only read as much as is available.
+    pub struct PollResult {
+        pub event_socket: bool,
+        pub general_socket: bool,
+        pub stdin: bool,
+    }
+
+    impl Poll {
+        /// Create a new `Poll` instance from the two sockets.
+        pub fn new(event_socket: UdpSocket, general_socket: UdpSocket) -> Result<Self, Error> {
+            let stdin = Stdin::acquire().context("Failure acquiring stdin handle")?;
+            let stdout = Stdout::acquire().context("Failed acquiring stdout handle")?;
+
+            // Create event objects for the readability of the sockets.
+            let event_socket_event = EventHandle::new().context("Failed creating WSA event")?;
+            let general_socket_event = EventHandle::new().context("Failed creating WSA event")?;
+
+            // SAFETY: WSAEventSelect() requires a valid socket and WSA event, which are both
+            // passed here, and the bitflag of events that should be selected for.
+            //
+            // On error a non-zero value is returned.
+            unsafe {
+                if WSAEventSelect(event_socket.as_raw_socket(), event_socket_event.0, FD_READ) != 0
+                {
+                    bail!(
+                        source: io::Error::from_raw_os_error(WSAGetLastError()),
+                        "Failed selecting for read events on event socket"
+                    );
+                }
+
+                if WSAEventSelect(
+                    general_socket.as_raw_socket(),
+                    general_socket_event.0,
+                    FD_READ,
+                ) != 0
+                {
+                    bail!(
+                        source: io::Error::from_raw_os_error(WSAGetLastError()),
+                        "Failed selecting for read events on general socket"
+                    );
+                }
+            }
+
+            Ok(Self {
+                event_socket,
+                event_socket_event,
+                general_socket,
+                general_socket_event,
+                stdin,
+                stdout,
+            })
+        }
+
+        /// Mutable reference to the event socket.
+        pub fn event_socket(&mut self) -> &mut UdpSocket {
+            &mut self.event_socket
+        }
+
+        /// Mutable reference to the general socket.
+        pub fn general_socket(&mut self) -> &mut UdpSocket {
+            &mut self.general_socket
+        }
+
+        /// Mutable reference to stdin for reading.
+        pub fn stdin(&mut self) -> &mut Stdin {
+            &mut self.stdin
+        }
+
+        /// Mutable reference to stdout for writing.
+        pub fn stdout(&mut self) -> &mut Stdout {
+            &mut self.stdout
+        }
+
+        /// Poll the event socket, general socket and stdin for available data to read.
+        ///
+        /// This blocks until at least one input has data available.
+        pub fn poll(&mut self) -> Result<PollResult, Error> {
+            let handles = [
+                self.event_socket_event.0,
+                self.general_socket_event.0,
+                // If stdin is a pipe then we use the signalling event, otherwise stdin itself.
+                if let Some(ref thread_state) = self.stdin.thread_state {
+                    thread_state.event
+                } else {
+                    self.stdin.handle
+                },
+            ];
+
+            // If stdin is a pipe and currently no data is pending on it then signal
+            // the reading thread to try reading one byte and blocking for that long.
+            if let Some(ref mut thread_state) = self.stdin.thread_state {
+                let mut guard = thread_state.buffer.lock().unwrap();
+                if !guard.buffer_filled && !guard.fill_buffer {
+                    guard.fill_buffer = true;
+                    // SAFETY: The thread's event is valid by construction until the thread
+                    // is stopped, and can be reset at any time.
+                    unsafe {
+                        ResetEvent(thread_state.event);
+                    }
+                    thread_state.buffer_cond.notify_one();
+                }
+            }
+
+            // SAFETY: Wait for the socket/stdin objects to become ready. This requires a valid
+            // array of valid handles and the corresponding length, whether it should wait for all
+            // handles (no), and a timeout (infinity).
+            //
+            // On error u32::MAX is returned, otherwise an index into the array of handles is
+            // returned for the handle that became ready.
+            let res = unsafe {
+                let res =
+                    WaitForMultipleObjects(handles.len() as _, handles[..].as_ptr(), 0, u32::MAX);
+                if res == u32::MAX {
+                    bail!(
+                        source: io::Error::from_raw_os_error(WSAGetLastError()),
+                        "Failed waiting for events"
+                    );
+                }
+
+                assert!(
+                    (0..=2).contains(&res),
+                    "Unexpected WaitForMultipleObjects() return value {}",
+                    res,
+                );
+
+                res
+            };
+
+            // For the sockets, enumerate the events that woke up the waiting, collect any errors
+            // and reset the event objects.
+            if (0..=1).contains(&res) {
+                let (socket, event) = if res == 0 {
+                    (&self.event_socket, &self.event_socket_event)
+                } else {
+                    (&self.general_socket, &self.general_socket_event)
+                };
+
+                // SAFETY: Requires a valid socket and event, which is given by construction here.
+                // The passed in memory for the network events will be filled if no error happens,
+                // and the function returns a non-zero value if an error has happened.
+                let networkevents = unsafe {
+                    let mut networkevents = mem::MaybeUninit::uninit();
+                    if WSAEnumNetworkEvents(
+                        socket.as_raw_socket(),
+                        event.0,
+                        networkevents.as_mut_ptr(),
+                    ) != 0
+                    {
+                        bail!(
+                            source: io::Error::from_raw_os_error(WSAGetLastError()),
+                            "Failed enumerating network events on {} socket",
+                            if res == 0 { "event" } else { "general" },
+                        );
+                    }
+
+                    networkevents.assume_init()
+                };
+
+                if networkevents.ierrorcode[FD_READ_BIT] != 0 {
+                    bail!(
+                        source: io::Error::from_raw_os_error(networkevents.ierrorcode[FD_READ_BIT]),
+                        "Error on {} socket while waiting for events",
+                        if res == 0 { "event" } else { "general" },
+                    );
+                }
+                assert!(networkevents.lnetworkevents & FD_READ != 0);
+            }
+
+            Ok(PollResult {
+                event_socket: res == 0,
+                general_socket: res == 1,
+                stdin: res == 2,
+            })
+        }
+    }
+
+    /// Raw, unbuffered handle to `stdin`.
+    ///
+    /// This implements the `Read` trait for reading.
+    pub struct Stdin {
+        handle: HANDLE,
+        thread_state: Option<Arc<StdinThreadState>>,
+        join_handle: Option<thread::JoinHandle<()>>,
+    }
+
+    struct StdinThreadState {
+        buffer: Mutex<StdinBuffer>,
+        buffer_cond: Condvar,
+        event: HANDLE,
+        handle: HANDLE,
+    }
+
+    unsafe impl Send for StdinThreadState {}
+    unsafe impl Sync for StdinThreadState {}
+
+    struct StdinBuffer {
+        buffer: [u8; 1],
+        error: Option<io::Error>,
+        buffer_filled: bool,
+        fill_buffer: bool,
+        shutdown: bool,
+    }
+
+    impl Drop for Stdin {
+        fn drop(&mut self) {
+            // If stdin was a pipe and a thread was started to check for read-readiness
+            // then stop this thread now and release its resources.
+            if let Some(ref thread_state) = self.thread_state {
+                let mut guard = thread_state.buffer.lock().unwrap();
+                guard.shutdown = true;
+                thread_state.buffer_cond.notify_one();
+                drop(guard);
+                let _ = self.join_handle.take().unwrap().join();
+
+                // SAFETY: The thread is stopped now so the event is not used by anything else
+                // anymore and can safely be closed now.
+                //
+                // The return value is explicitly ignored because nothing can be done on error
+                // anyway.
+                unsafe {
+                    let _ = CloseHandle(thread_state.event);
+                }
+            }
+        }
+    }
+
+    impl Stdin {
+        fn acquire() -> Result<Self, Error> {
+            // SAFETY: GetStdHandle returns a borrowed handle, or 0 if none is set or -1 if an
+            // error has happened.
+            let handle = unsafe {
+                let handle = GetStdHandle(STD_INPUT_HANDLE);
+                if handle.is_null() {
+                    bail!("No stdin handle set");
+                } else if handle as isize == -1 {
+                    bail!(source: io::Error::last_os_error(), "Can't get stdin handle");
+                }
+
+                handle
+            };
+            // SAFETY: GetFileType() is safe to call on any valid handle.
+            let type_ = unsafe { GetFileType(handle) };
+
+            if type_ == FILE_TYPE_CHAR {
+                // Set the console to raw mode and flush any pending input.
+                //
+                // SAFETY: Calling this on non-console handles will cause an error but otherwise
+                // have no negative effects. We can safely change the console mode here as nothing
+                // else is accessing the console.
+                unsafe {
+                    let _ = SetConsoleMode(handle, 0);
+                    let _ = FlushConsoleInputBuffer(handle);
+                }
+
+                Ok(Stdin {
+                    handle,
+                    thread_state: None,
+                    join_handle: None,
+                })
+            } else if type_ == FILE_TYPE_PIPE {
+                // XXX: Because g_spawn() creates the overridden pipes with _pipe(), they're
+                //        1. Full duplex, so WaitForMultipleObjects() always considers them ready as you can write
+                //        2. Not overlapped so only synchronous IO can be used
+                //      To work around this we're creating a thread here that just reads synchronously
+                //      from stdin to signal ready-ness.
+
+                // SAFETY: Creating an event handle with all-zero parameters is valid and on error
+                // a NULL handle will be returned. Otherwise a valid event handle is returned that
+                // needs to be closed again later, which happens as part of the StdinThreadState
+                // Drop implementation.
+                let event = unsafe {
+                    let event = CreateEventA(ptr::null(), 0, 0, ptr::null());
+                    if event.is_null() {
+                        bail!(
+                            source: io::Error::last_os_error(),
+                            "Failed creating event handle"
+                        );
+                    }
+
+                    event
+                };
+                let thread_state = Arc::new(StdinThreadState {
+                    buffer: Mutex::new(StdinBuffer {
+                        buffer: [0],
+                        error: None,
+                        buffer_filled: false,
+                        fill_buffer: true,
+                        shutdown: false,
+                    }),
+                    buffer_cond: Condvar::new(),
+                    event,
+                    handle,
+                });
+
+                let join_handle = thread::spawn({
+                    let thread_state = thread_state.clone();
+                    move || Self::stdin_readiness_thread(&thread_state)
+                });
+
+                Ok(Stdin {
+                    handle,
+                    thread_state: Some(thread_state),
+                    join_handle: Some(join_handle),
+                })
+            } else {
+                bail!("unhandled stdin handle type {:x}", type_);
+            }
+        }
+
+        /// Thread function to signal readiness of stdin.
+        ///
+        /// This thread tries to read a single byte and buffers it, then signals an event
+        /// object and waits for reading to finish and a new call to `poll()` to
+        /// start trying to read a single byte again.
+        fn stdin_readiness_thread(thread_state: &StdinThreadState) {
+            loop {
+                let mut buffer = [0u8];
+                // SAFETY: Reads one byte from the handle synchronously. Nothing else is currently
+                // reading from the handle as this thread is waiting below on the condition
+                // variable as long as a single byte was read already, and only wakes up again if a
+                // full packet was read from stdin and the other thread is waiting on the event
+                // handle again.
+                let res = unsafe {
+                    let mut bytes_read = mem::MaybeUninit::uninit();
+                    let res = ReadFile(
+                        thread_state.handle,
+                        buffer[..].as_mut_ptr(),
+                        buffer.len() as u32,
+                        bytes_read.as_mut_ptr(),
+                        ptr::null_mut(),
+                    );
+                    if res == 0 {
+                        Err(io::Error::last_os_error())
+                    } else {
+                        Ok(bytes_read.assume_init())
+                    }
+                };
+
+                let mut guard = thread_state.buffer.lock().unwrap();
+                assert!(!guard.buffer_filled);
+                assert!(guard.fill_buffer);
+                if guard.shutdown {
+                    break;
+                }
+                guard.buffer_filled = true;
+                guard.fill_buffer = false;
+                match res {
+                    Err(err) => {
+                        guard.error = Some(err);
+                    }
+                    Ok(bytes_read) => {
+                        guard.buffer[0] = buffer[0];
+                        assert_eq!(bytes_read, 1);
+                    }
+                }
+
+                // SAFETY: Signalling an event is valid from any thread at any time and the event
+                // handle is valid by construction.
+                unsafe {
+                    SetEvent(thread_state.event);
+                }
+                while !guard.shutdown && !guard.fill_buffer {
+                    guard = thread_state.buffer_cond.wait(guard).unwrap();
+                }
+                if guard.shutdown {
+                    break;
+                }
+            }
+        }
+    }
+
+    impl Read for Stdin {
+        fn read(&mut self, mut buf: &mut [u8]) -> io::Result<usize> {
+            if buf.is_empty() {
+                return Ok(0);
+            }
+
+            // If a read byte is pending from the readiness signalling thread then
+            // read that first here before reading any remaining data.
+            let mut already_read = 0;
+            if let Some(ref mut thread_state) = self.thread_state {
+                let mut guard = thread_state.buffer.lock().unwrap();
+                assert!(!guard.fill_buffer);
+                if guard.buffer_filled {
+                    guard.buffer_filled = false;
+                    if let Some(err) = guard.error.take() {
+                        return Err(err);
+                    }
+                    buf[0] = guard.buffer[0];
+                    if buf.len() == 1 {
+                        return Ok(1);
+                    }
+                    buf = &mut buf[1..];
+                    already_read = 1;
+                }
+            }
+
+            // SAFETY: Reads the given number of bytes into the buffer from the stdin handle.
+            // The other thread is not currently reading as checked above, and would only be
+            // triggered to read again by this thread once poll() is called.
+            unsafe {
+                let mut lpnumberofbytesread = mem::MaybeUninit::uninit();
+                let res = ReadFile(
+                    self.handle,
+                    buf.as_mut_ptr(),
+                    cmp::min(buf.len() as u32, u32::MAX) as u32,
+                    lpnumberofbytesread.as_mut_ptr(),
+                    ptr::null_mut(),
+                );
+
+                if res == 0 {
+                    Err(io::Error::last_os_error())
+                } else {
+                    Ok(lpnumberofbytesread.assume_init() as usize + already_read)
+                }
+            }
+        }
+    }
+
+    /// Raw, unbuffered handle to `stdout`.
+    ///
+    /// This implements the `Write` trait for writing.
+    pub struct Stdout(HANDLE);
+
+    impl Stdout {
+        fn acquire() -> Result<Self, Error> {
+            // SAFETY: GetStdHandle returns a borrowed handle, or 0 if none is set or -1 if an
+            // error has happened.
+            let handle = unsafe {
+                let handle = GetStdHandle(STD_OUTPUT_HANDLE);
+                if handle.is_null() {
+                    bail!("No stdout handle set");
+                } else if handle as isize == -1 {
+                    bail!(
+                        source: io::Error::last_os_error(),
+                        "Can't get stdout handle"
+                    );
+                }
+
+                handle
+            };
+            // SAFETY: GetFileType() is safe to call on any valid handle.
+            let type_ = unsafe { GetFileType(handle) };
+
+            if type_ == FILE_TYPE_CHAR {
+                // Set the console to raw mode.
+                //
+                // SAFETY: Calling this on non-console handles will cause an error but otherwise
+                // have no negative effects. We can safely change the console mode here as nothing
+                // else is accessing the console.
+                unsafe {
+                    let _ = SetConsoleMode(handle, 0);
+                }
+            } else if type_ != FILE_TYPE_PIPE {
+                bail!("Unsupported stdout handle type {:x}", type_);
+            }
+
+            Ok(Stdout(handle))
+        }
+    }
+
+    impl Write for Stdout {
+        fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+            // SAFETY: Writes the given number of bytes to stdout or at most u32::MAX. On error
+            // zero is returned, otherwise the number of bytes written is set accordingly and
+            // returned.
+            unsafe {
+                let mut lpnumberofbyteswritten = mem::MaybeUninit::uninit();
+                let res = WriteFile(
+                    self.0,
+                    buf.as_ptr(),
+                    cmp::min(buf.len() as u32, u32::MAX) as u32,
+                    lpnumberofbyteswritten.as_mut_ptr(),
+                    ptr::null_mut(),
+                );
+
+                if res == 0 {
+                    Err(io::Error::last_os_error())
+                } else {
+                    Ok(lpnumberofbyteswritten.assume_init() as usize)
+                }
+            }
+        }
+
+        fn flush(&mut self) -> io::Result<()> {
+            Ok(())
+        }
+    }
+}
+
+pub use self::imp::{Poll, PollResult, Stdin, Stdout};
diff --git a/subprojects/gstreamer/libs/gst/helpers/ptp/main.rs b/subprojects/gstreamer/libs/gst/helpers/ptp/main.rs
new file mode 100644
index 0000000000000000000000000000000000000000..dc3173085e71dc1e1cc1727444965608e50ec3ef
--- /dev/null
+++ b/subprojects/gstreamer/libs/gst/helpers/ptp/main.rs
@@ -0,0 +1,232 @@
+// GStreamer
+//
+// Copyright (C) 2015-2023 Sebastian Dröge <sebastian@centricular.com>
+//
+// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
+// If a copy of the MPL was not distributed with this file, You can obtain one at
+// <https://mozilla.org/MPL/2.0/>.
+//
+// SPDX-License-Identifier: MPL-2.0
+
+//! 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 stdin to those sockets.
+//! Additionally it provides the MAC address of a network interface via stdout
+
+use std::{
+    io::{Read, Write},
+    net::{Ipv4Addr, SocketAddr, UdpSocket},
+};
+
+mod args;
+mod error;
+mod ffi;
+mod io;
+mod net;
+mod privileges;
+mod rand;
+
+use error::{Context, Error};
+use rand::rand;
+
+/// PTP Multicast group.
+const PTP_MULTICAST_ADDR: Ipv4Addr = Ipv4Addr::new(224, 0, 1, 129);
+/// PTP Event message port.
+const PTP_EVENT_PORT: u16 = 319;
+/// PTP General message port.
+const PTP_GENERAL_PORT: u16 = 320;
+
+/// Create a new `UdpSocket` for the given port and configure it for PTP.
+fn create_socket(port: u16) -> Result<UdpSocket, Error> {
+    let socket = UdpSocket::bind(SocketAddr::from((Ipv4Addr::UNSPECIFIED, port)))
+        .with_context(|| format!("Failed to bind socket to port {}", port))?;
+
+    socket.set_ttl(1).context("Failed setting TTL on socket")?;
+    socket
+        .set_multicast_ttl_v4(1)
+        .context("Failed to set multicast TTL on socket")?;
+
+    net::set_reuse(&socket);
+
+    Ok(socket)
+}
+
+/// Join the multicast groups for PTP on the configured interfaces.
+fn join_multicast(
+    args: &args::Args,
+    event_socket: &UdpSocket,
+    general_socket: &UdpSocket,
+) -> Result<[u8; 8], Error> {
+    let mut ifaces = net::query_interfaces().context("Failed to query network interfaces")?;
+    if ifaces.is_empty() {
+        bail!("No suitable network interfaces for PTP found");
+    }
+
+    if !args.interfaces.is_empty() {
+        ifaces.retain(|iface| {
+            for filter_iface in &args.interfaces {
+                if &iface.name == filter_iface {
+                    return true;
+                }
+                if let Some(ref other_name) = iface.other_name {
+                    if other_name == filter_iface {
+                        return true;
+                    }
+                }
+                if let Ok(addr) = filter_iface.parse::<Ipv4Addr>() {
+                    if addr == iface.ip_addr {
+                        return true;
+                    }
+                }
+            }
+
+            false
+        });
+
+        if ifaces.is_empty() {
+            bail!("None of the selected network interfaces found");
+        }
+        if ifaces.len() != args.interfaces.len() {
+            bail!("Not all selected network interfaces found");
+        }
+    }
+
+    for socket in [&event_socket, &general_socket].iter() {
+        for iface in &ifaces {
+            net::join_multicast_v4(socket, &PTP_MULTICAST_ADDR, iface)
+                .context("Failed to join multicast group")?;
+        }
+    }
+
+    let clock_id = if args.clock_id == 0 {
+        ifaces
+            .iter()
+            .find_map(|iface| iface.hw_addr)
+            .map(|hw_addr| {
+                [
+                    hw_addr[0], hw_addr[1], hw_addr[2], 0xff, 0xfe, hw_addr[3], hw_addr[4],
+                    hw_addr[5],
+                ]
+            })
+            .unwrap_or_else(rand)
+    } else {
+        args.clock_id.to_be_bytes()
+    };
+
+    Ok(clock_id)
+}
+
+fn main() -> Result<(), Error> {
+    let args = args::parse_args().context("Failed parsing commandline parameters")?;
+
+    let event_socket = create_socket(PTP_EVENT_PORT).context("Failed creating event socket")?;
+    let general_socket =
+        create_socket(PTP_GENERAL_PORT).context("Failed creating general socket")?;
+
+    privileges::drop().context("Failed dropping privileges")?;
+
+    let clock_id = join_multicast(&args, &event_socket, &general_socket)
+        .context("Failed joining multicast groups")?;
+
+    let mut poll = io::Poll::new(event_socket, general_socket).context("Failed creating poller")?;
+
+    // Write clock ID first
+    {
+        let mut clock_id_data = [0u8; 4 + 8];
+        clock_id_data[0..2].copy_from_slice(&8u16.to_le_bytes());
+        clock_id_data[2] = 2;
+        clock_id_data[3] = 0;
+        clock_id_data[4..].copy_from_slice(&clock_id);
+
+        poll.stdout()
+            .write_all(&clock_id_data)
+            .context("Failed writing to stdout")?;
+    }
+
+    // Now read-write from stdin/stdout and the sockets
+    //
+    // We assume that stdout never blocks and stdin receives a complete valid packet whenever it is
+    // ready and never blocks in the middle of a packet.
+    let mut socket_buffer = [0u8; 1500];
+    let mut stdinout_buffer = [0u8; 1504];
+
+    loop {
+        let poll_res = poll.poll().context("Failed polling")?;
+
+        // If any of the sockets are ready, continue reading packets from them until no more
+        // packets are left and directly forward them to stdout.
+        'next_socket: for idx in [poll_res.event_socket, poll_res.general_socket]
+            .iter()
+            .enumerate()
+            .filter_map(|(idx, r)| if *r { Some(idx) } else { None })
+        {
+            let res = match idx {
+                0 => poll.event_socket().recv(&mut socket_buffer),
+                1 => poll.general_socket().recv(&mut socket_buffer),
+                _ => unreachable!(),
+            };
+
+            match res {
+                Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => {
+                    continue 'next_socket;
+                }
+                Err(err) => {
+                    bail!(
+                        source: err,
+                        "Failed reading from {} socket",
+                        if idx == 0 { "event" } else { "general" }
+                    );
+                }
+                Ok(read) => {
+                    stdinout_buffer[0..2].copy_from_slice(&(read as u16).to_ne_bytes());
+                    stdinout_buffer[2] = idx as u8;
+                    stdinout_buffer[3] = 0;
+                    stdinout_buffer[4..][..read].copy_from_slice(&socket_buffer[..read]);
+
+                    poll.stdout()
+                        .write_all(&stdinout_buffer[..(read + 4)])
+                        .context("Failed writing to stdout")?;
+                }
+            }
+        }
+
+        // After handling the sockets check if a packet is available on stdin, read it and forward
+        // it to the corresponding socket.
+        if poll_res.stdin {
+            poll.stdin()
+                .read_exact(&mut stdinout_buffer[0..4])
+                .context("Failed reading packet header from stdin")?;
+
+            let size = u16::from_ne_bytes([stdinout_buffer[0], stdinout_buffer[1]]);
+            if size as usize > stdinout_buffer.len() {
+                bail!("Invalid packet size on stdin {}", size);
+            }
+            let type_ = stdinout_buffer[2];
+
+            poll.stdin()
+                .read_exact(&mut stdinout_buffer[0..size as usize])
+                .context("Failed reading packet body from stdin")?;
+
+            let buf = &stdinout_buffer[0..size as usize];
+            match type_ {
+                0 => poll
+                    .event_socket()
+                    .send_to(buf, (PTP_MULTICAST_ADDR, PTP_EVENT_PORT)),
+                1 => poll
+                    .general_socket()
+                    .send_to(buf, (PTP_MULTICAST_ADDR, PTP_GENERAL_PORT)),
+                _ => unreachable!(),
+            }
+            .with_context(|| {
+                format!(
+                    "Failed sending to {} socket",
+                    if type_ == 0 { "event" } else { "general" }
+                )
+            })?;
+        }
+    }
+}
diff --git a/subprojects/gstreamer/libs/gst/helpers/ptp/meson.build b/subprojects/gstreamer/libs/gst/helpers/ptp/meson.build
new file mode 100644
index 0000000000000000000000000000000000000000..9953a6a37a2001f6df5c7ba51fecf51c7e277d43
--- /dev/null
+++ b/subprojects/gstreamer/libs/gst/helpers/ptp/meson.build
@@ -0,0 +1,91 @@
+# Check PTP support
+have_rust = add_languages('rust', native : false, required : false)
+have_ptp = true
+if not have_rust
+  have_ptp = false
+  message('PTP not supported without Rust compiler')
+endif
+
+if not ['linux', 'android', 'freebsd', 'openbsd', 'netbsd', 'dragonfly', 'darwin', 'sunos', 'solaris', 'illumos', 'windows'].contains(host_system)
+  have_ptp = false
+  message('PTP not supported on this OS')
+endif
+
+if have_ptp
+  rustc = meson.get_compiler('rust')
+  cdata.set('HAVE_PTP', 1, description : 'PTP support available')
+
+  ptp_helper_conf_data = configuration_data()
+  rust_args = []
+
+  setcap_prog = find_program('setcap', '/usr/sbin/setcap', '/sbin/setcap', required : false)
+  cap_dep = dependency('libcap', required: false)
+
+  # user/group to change to in gst-ptp-helper
+  ptp_helper_setuid_user = get_option('ptp-helper-setuid-user')
+  if ptp_helper_setuid_user != ''
+    ptp_helper_conf_data.set('PTP_HELPER_SETUID_USER', 'Some("@0@")'.format(ptp_helper_setuid_user))
+  else
+    ptp_helper_conf_data.set('PTP_HELPER_SETUID_USER', 'None')
+  endif
+  ptp_helper_setuid_group = get_option('ptp-helper-setuid-group')
+  if ptp_helper_setuid_group != ''
+    ptp_helper_conf_data.set('PTP_HELPER_SETUID_GROUP', 'Some("@0@")'.format(ptp_helper_setuid_group))
+  else
+    ptp_helper_conf_data.set('PTP_HELPER_SETUID_GROUP', 'None')
+  endif
+
+  # how to install gst-ptp-helper
+  with_ptp_helper_permissions = get_option('ptp-helper-permissions')
+  if with_ptp_helper_permissions == 'auto'
+    if setcap_prog.found() and cap_dep.found()
+      with_ptp_helper_permissions = 'capabilities'
+    elif host_system == 'windows'
+      with_ptp_helper_permissions = 'none'
+    else
+      with_ptp_helper_permissions = 'setuid-root'
+    endif
+  endif
+  message('How to install gst-ptp-helper: ' + with_ptp_helper_permissions)
+
+  if with_ptp_helper_permissions == 'none'
+    rust_args += ['--cfg', 'ptp_helper_permissions="none"']
+    # nothing to do
+  elif with_ptp_helper_permissions == 'setuid-root'
+    rust_args += ['--cfg', 'ptp_helper_permissions="setuid-root"']
+  elif with_ptp_helper_permissions == 'capabilities'
+    if not setcap_prog.found()
+      error('capabilities-based ptp-helper-permissions requested, but could not find setcap tool.')
+    elif not cap_dep.found()
+      error('capabilities-based ptp-helper-permissions requested, but could not find libcap.')
+    endif
+    rust_args += ['--cfg', 'ptp_helper_permissions="setcap"']
+  else
+    error('Unexpected ptp helper permissions value: ' + with_ptp_helper_permissions)
+  endif
+
+  conf_lib_rs = configure_file(input : 'conf_lib.rs.in',
+                               output : 'conf_lib.rs',
+                               configuration: ptp_helper_conf_data)
+
+  conf = static_library('gst_ptp_helper_conf', conf_lib_rs,
+    override_options : ['rust_std=2018'],
+    rust_args : ['-Cpanic=abort'],
+    rust_crate_type : 'rlib')
+
+  exe = executable('gst-ptp-helper', 'main.rs',
+    override_options : ['rust_std=2018'],
+    rust_args : ['-Cpanic=abort', rust_args],
+    dependencies : [cap_dep],
+    link_with : conf,
+    install_dir : helpers_install_dir,
+    install : true)
+
+  if host_system != 'windows'
+    meson.add_install_script('ptp_helper_post_install.sh',
+        helpers_install_dir, with_ptp_helper_permissions,
+        setcap_prog.found() ? setcap_prog.full_path() : '')
+  endif
+
+  meson.add_devenv({'GST_PTP_HELPER': exe.full_path()})
+endif
diff --git a/subprojects/gstreamer/libs/gst/helpers/ptp/net.rs b/subprojects/gstreamer/libs/gst/helpers/ptp/net.rs
new file mode 100644
index 0000000000000000000000000000000000000000..936d36cddc30630c321067fd0b80711aeb87f12e
--- /dev/null
+++ b/subprojects/gstreamer/libs/gst/helpers/ptp/net.rs
@@ -0,0 +1,657 @@
+use std::net::Ipv4Addr;
+
+use crate::{bail, error::Error};
+
+#[derive(Debug)]
+/// Network interface information.
+pub struct InterfaceInfo {
+    /// Name of the interface
+    pub name: String,
+    /// Other name of the interface, if any
+    pub other_name: Option<String>,
+    /// Interface index
+    pub index: usize,
+    /// Unicast IPv4 address of the interface
+    pub ip_addr: Ipv4Addr,
+    /// Physical MAC address of the interface, if any
+    pub hw_addr: Option<[u8; 6]>,
+}
+
+#[cfg(unix)]
+mod imp {
+    use super::*;
+
+    use std::{ffi::CStr, io, marker, mem, net::UdpSocket, os::unix::io::AsRawFd, ptr};
+
+    use crate::{error::Context, ffi::unix::*};
+
+    /// Returns information for all non-loopback, multicast-capable network interfaces.
+    pub fn query_interfaces() -> Result<Vec<InterfaceInfo>, Error> {
+        struct Ifaddrs(*mut ifaddrs);
+
+        impl Ifaddrs {
+            fn new() -> io::Result<Self> {
+                loop {
+                    // SAFETY: Requires passing valid storage for the returned ifaddrs pointer and
+                    // returns -1 on errors. It might return NULL if there are no network interfaces.
+                    //
+                    // On error it might return EINTR in which case we should simply try again.
+                    unsafe {
+                        let mut ifaddrs = ptr::null_mut();
+                        if getifaddrs(&mut ifaddrs) == -1 {
+                            let err = io::Error::last_os_error();
+                            if err.kind() == std::io::ErrorKind::Interrupted {
+                                continue;
+                            }
+
+                            return Err(err);
+                        } else {
+                            return Ok(Self(ifaddrs));
+                        }
+                    }
+                }
+            }
+
+            fn iter(&self) -> IfaddrsIter {
+                IfaddrsIter {
+                    ptr: ptr::NonNull::new(self.0),
+                    phantom: marker::PhantomData,
+                }
+            }
+        }
+
+        impl Drop for Ifaddrs {
+            fn drop(&mut self) {
+                // SAFETY: The pointer is a valid ifaddrs pointer by construction and dropped only
+                // once, so freeing it here is OK. It might be NULL so check for that first.
+                unsafe {
+                    if !self.0.is_null() {
+                        freeifaddrs(self.0);
+                    }
+                }
+            }
+        }
+
+        struct IfaddrsIter<'a> {
+            ptr: Option<ptr::NonNull<ifaddrs>>,
+            phantom: marker::PhantomData<&'a Ifaddrs>,
+        }
+
+        impl<'a> Iterator for IfaddrsIter<'a> {
+            type Item = &'a ifaddrs;
+
+            fn next(&mut self) -> Option<Self::Item> {
+                match self.ptr {
+                    None => None,
+                    Some(ptr) => {
+                        // SAFETY: The pointer is a valid ifaddrs pointer by construction so
+                        // creating a reference to it is OK.
+                        let addr = unsafe { &*ptr.as_ptr() };
+                        self.ptr = ptr::NonNull::new(addr.ifa_next);
+                        Some(addr)
+                    }
+                }
+            }
+        }
+
+        let ifaddrs = Ifaddrs::new().context("Failed getting interface addresses")?;
+
+        let mut if_infos = Vec::<InterfaceInfo>::new();
+
+        for ifaddr in ifaddrs.iter() {
+            // SAFETY: ifa_name points to a NUL-terminated interface name string that is valid as
+            // long as its struct is
+            let name = unsafe { CStr::from_ptr(ifaddr.ifa_name) }.to_str().unwrap();
+
+            // Skip loopback interfaces, interfaces that are not up and interfaces that can't do
+            // multicast. These are all unusable for PTP purposes.
+            let flags = ifaddr.ifa_flags;
+            if (flags & IFF_LOOPBACK as u32 != 0)
+                || (flags & IFF_UP as u32 == 0)
+                || (flags & IFF_MULTICAST as u32 == 0)
+            {
+                continue;
+            }
+
+            // If the interface has no address then skip it. Only interfaces with IPv4 addresses
+            // are usable for PTP purposes.
+            if ifaddr.ifa_addr.is_null() {
+                continue;
+            }
+
+            // Get the interface index from its name. If it has none then we can't use it to join
+            // the PTP multicast group reliable for this interface.
+            //
+            // SAFETY: Must be called with a valid, NUL-terminated string which is provided by
+            // ifa_name and will return the interface index or zero on error.
+            //
+            // On error it can return EINTR in which case we should try again.
+            let index = loop {
+                let index = unsafe { if_nametoindex(ifaddr.ifa_name) } as usize;
+                if index == 0 {
+                    let err = io::Error::last_os_error();
+                    if err.kind() == io::ErrorKind::Interrupted {
+                        continue;
+                    }
+                }
+                break index;
+            };
+            if index == 0 {
+                continue;
+            }
+
+            // Interfaces are listed multiple times here, once per address. We collect all IPv4 and
+            // MAC addresses for interfaces below.
+
+            // SAFETY: ifa_addr is a valid sockaddr pointer and was checked to be not NULL further
+            // above.
+            let sa_family = unsafe { (*ifaddr.ifa_addr).sa_family };
+
+            // If this interface has an IPv4 address then retrieve and store it here.
+            if sa_family == AF_INET as _ {
+                // SAFETY: If the address family is AF_INET then it is actually a valid sockaddr_in
+                // pointer and can be used as such.
+                let addr = unsafe { &*(ifaddr.ifa_addr as *const sockaddr_in) };
+                let ip_addr = Ipv4Addr::from(addr.sin_addr.s_addr.to_ne_bytes());
+
+                if let Some(if_info) = if_infos.iter_mut().find(|info| info.name == name) {
+                    if if_info.ip_addr.is_broadcast() {
+                        if_info.ip_addr = ip_addr;
+                    }
+                } else {
+                    if_infos.push(InterfaceInfo {
+                        name: String::from(name),
+                        other_name: None,
+                        index,
+                        ip_addr,
+                        hw_addr: None,
+                    });
+                }
+            }
+
+            #[cfg(any(target_os = "linux", target_os = "android"))]
+            {
+                if sa_family == AF_PACKET as _ {
+                    // SAFETY: If the address family is AF_PACKET then it is actually a valid sockaddr_ll
+                    // pointer and can be used as such.
+                    let addr = unsafe { &*(ifaddr.ifa_addr as *const sockaddr_ll) };
+                    if addr.sll_halen == 6 {
+                        let mut hw_addr = [0u8; 6];
+                        hw_addr.copy_from_slice(&addr.sll_addr[0..6]);
+                        if let Some(if_info) = if_infos.iter_mut().find(|info| info.name == name) {
+                            if if_info.hw_addr.is_none() {
+                                if_info.hw_addr = Some(hw_addr);
+                            }
+                        } else {
+                            if_infos.push(InterfaceInfo {
+                                name: String::from(name),
+                                other_name: None,
+                                index,
+                                ip_addr: Ipv4Addr::BROADCAST,
+                                hw_addr: Some(hw_addr),
+                            });
+                        }
+                    }
+                }
+            }
+            #[cfg(not(any(target_os = "linux", target_os = "android")))]
+            {
+                use std::slice;
+
+                if sa_family == AF_LINK as _ {
+                    // SAFETY: If the address family is AF_LINK then it is actually a valid sockaddr_dl
+                    // pointer and can be used as such.
+                    let addr = unsafe { &*(ifaddr.ifa_addr as *const sockaddr_dl) };
+                    if addr.sdl_nlen <= IF_NAMESIZE as u8 && addr.sdl_alen == 6 {
+                        let mut hw_addr = [0u8; 6];
+                        // SAFETY: There can be more than the given number of bytes stored and
+                        // this happens regularly on macOS at least. It is required that the
+                        // interface name is at most IF_NAMESIZE (checked just above).
+                        unsafe {
+                            let sdl_addr_ptr = addr.sdl_data.as_ptr() as *const u8;
+                            let sdl_addr =
+                                slice::from_raw_parts(sdl_addr_ptr.add(addr.sdl_nlen as usize), 6);
+                            hw_addr.copy_from_slice(sdl_addr);
+                        }
+
+                        if let Some(if_info) = if_infos.iter_mut().find(|info| info.name == name) {
+                            if if_info.hw_addr.is_none() {
+                                if_info.hw_addr = Some(hw_addr);
+                            }
+                        } else {
+                            if_infos.push(InterfaceInfo {
+                                name: String::from(name),
+                                other_name: None,
+                                index,
+                                ip_addr: Ipv4Addr::BROADCAST,
+                                hw_addr: Some(hw_addr),
+                            });
+                        }
+                    }
+                }
+            }
+        }
+
+        if_infos.retain(|iface| !iface.ip_addr.is_broadcast());
+
+        Ok(if_infos)
+    }
+
+    // Join multicast address for a given interface.
+    pub fn join_multicast_v4(
+        socket: &UdpSocket,
+        addr: &Ipv4Addr,
+        iface: &InterfaceInfo,
+    ) -> Result<(), Error> {
+        #[cfg(not(any(target_os = "solaris", target_os = "illumos")))]
+        {
+            let mreqn = ip_mreqn {
+                imr_multiaddr: in_addr {
+                    s_addr: u32::from_ne_bytes(addr.octets()),
+                },
+                imr_address: in_addr {
+                    s_addr: u32::from_ne_bytes(Ipv4Addr::UNSPECIFIED.octets()),
+                },
+                imr_ifindex: iface.index as _,
+            };
+
+            // SAFETY: Requires a valid ip_mreq or ip_mreqn struct to be passed together
+            // with its size for checking which of the two it is. On errors a negative
+            // integer is returned.
+            unsafe {
+                if setsockopt(
+                    socket.as_raw_fd(),
+                    IPPROTO_IP,
+                    IP_ADD_MEMBERSHIP,
+                    &mreqn as *const _ as *const _,
+                    mem::size_of_val(&mreqn) as _,
+                ) < 0
+                {
+                    bail!(
+                        source: io::Error::last_os_error(),
+                        "Failed joining multicast group for interface {}",
+                        iface.name,
+                    );
+                }
+            }
+
+            Ok(())
+        }
+        #[cfg(any(target_os = "solaris", target_os = "illumos"))]
+        {
+            use crate::error::Context;
+
+            socket
+                .join_multicast_v4(addr, &iface.ip_addr)
+                .with_context(|| {
+                    format!(
+                        "Failed joining multicast group for interface {} at address {}",
+                        iface.name, iface.ip_addr
+                    )
+                })?;
+
+            Ok(())
+        }
+    }
+
+    /// Allow multiple sockets to bind to the same address / port.
+    ///
+    /// This is best-effort and might not actually do anything.
+    pub fn set_reuse(socket: &UdpSocket) {
+        // SAFETY: SO_REUSEADDR takes an i32 value that can be 0/false or 1/true and
+        // enables the given feature on the socket.
+        //
+        // We explicitly ignore errors here. If it works, good, if it doesn't then not much
+        // lost other than the ability to run multiple processes at once.
+        unsafe {
+            let v = 1i32;
+            let _ = setsockopt(
+                socket.as_raw_fd(),
+                SOL_SOCKET,
+                SO_REUSEADDR,
+                &v as *const _ as *const _,
+                mem::size_of_val(&v) as u32,
+            );
+        }
+
+        #[cfg(not(any(target_os = "solaris", target_os = "illumos")))]
+        {
+            // SAFETY: SO_REUSEPORT takes an i32 value that can be 0/false or 1/true and
+            // enables the given feature on the socket.
+            //
+            // We explicitly ignore errors here. If it works, good, if it doesn't then not much
+            // lost other than the ability to run multiple processes at once.
+            unsafe {
+                let v = 1i32;
+                let _ = setsockopt(
+                    socket.as_raw_fd(),
+                    SOL_SOCKET,
+                    SO_REUSEPORT,
+                    &v as *const _ as *const _,
+                    mem::size_of_val(&v) as u32,
+                );
+            }
+        }
+    }
+}
+
+#[cfg(windows)]
+mod imp {
+    use super::*;
+
+    use std::{
+        ffi::{CStr, OsString},
+        io, marker, mem,
+        net::UdpSocket,
+        os::{
+            raw::*,
+            windows::{ffi::OsStringExt, io::AsRawSocket},
+        },
+        ptr, slice,
+    };
+
+    use crate::{error::Context, ffi::windows::*};
+
+    /// Returns information for all non-loopback, multicast-capable network interfaces.
+    pub fn query_interfaces() -> Result<Vec<InterfaceInfo>, Error> {
+        struct AdapterAddresses {
+            addresses: ptr::NonNull<IP_ADAPTER_ADDRESSES_LH>,
+            heap: isize,
+        }
+
+        impl AdapterAddresses {
+            fn new() -> io::Result<Self> {
+                // SAFETY: Gets the process's default heap and is safe to be called at any time.
+                let heap = unsafe { GetProcessHeap() };
+
+                // SAFETY: GetAdaptersAddresses() requires allocated memory to be passed in.
+                // In the beginning 16kB are allocated via HeapAlloc() from the default process's
+                // heap (see above), then passed to GetAdaptersAddresses().
+                //
+                // If this returns ERROR_NOT_ENOUGH_MEMORY then this was not enough memory and the
+                // required amount is returned as out parameter. In that case we loop up to 10
+                // times, reallocate memory via HeapReAlloc() and try again.
+                //
+                // On other errors the memory is freed before returning via HeapFree(), or when 10
+                // iterations were reached.
+                unsafe {
+                    let mut alloc_len = 16_384;
+                    let mut tries = 0;
+                    let mut addresses: *mut IP_ADAPTER_ADDRESSES_LH = ptr::null_mut();
+
+                    loop {
+                        if tries > 10 {
+                            HeapFree(heap, 0, addresses as *mut _);
+                            return Err(io::Error::from(io::ErrorKind::OutOfMemory));
+                        }
+
+                        if addresses.is_null() {
+                            addresses = HeapAlloc(heap, 0, alloc_len as usize) as *mut _;
+                        } else {
+                            addresses =
+                                HeapReAlloc(heap, 0, addresses as *mut _, alloc_len as usize)
+                                    as *mut _;
+                        }
+                        if addresses.is_null() {
+                            return Err(io::Error::from(io::ErrorKind::OutOfMemory));
+                        }
+
+                        let res = GetAdaptersAddresses(
+                            AF_INET,
+                            GAA_FLAG_SKIP_ANYCAST
+                                | GAA_FLAG_SKIP_MULTICAST
+                                | GAA_FLAG_SKIP_DNS_SERVER,
+                            ptr::null_mut(),
+                            addresses,
+                            &mut alloc_len,
+                        );
+
+                        if res == 0 {
+                            return Ok(AdapterAddresses {
+                                heap,
+                                addresses: ptr::NonNull::new_unchecked(addresses),
+                            });
+                        } else if res == ERROR_NOT_ENOUGH_MEMORY {
+                            tries += 1;
+                            continue;
+                        } else {
+                            HeapFree(heap, 0, addresses as *mut _);
+                            return Err(io::Error::from_raw_os_error(res as i32));
+                        }
+                    }
+                }
+            }
+
+            fn iter(&self) -> AdapterAddressesIter {
+                AdapterAddressesIter {
+                    ptr: Some(self.addresses),
+                    phantom: marker::PhantomData,
+                }
+            }
+        }
+
+        impl Drop for AdapterAddresses {
+            fn drop(&mut self) {
+                // SAFETY: The pointer is a valid IP_ADAPTER_ADDRESSES_LH pointer by construction
+                // and dropped only once, so freeing it here is OK. It might be NULL so check for
+                // that first.
+                //
+                // Heap is the process's heap as set in the constructor above.
+                unsafe {
+                    HeapFree(self.heap, 0, self.addresses.as_ptr() as *mut _);
+                }
+            }
+        }
+
+        struct AdapterAddressesIter<'a> {
+            ptr: Option<ptr::NonNull<IP_ADAPTER_ADDRESSES_LH>>,
+            phantom: marker::PhantomData<&'a AdapterAddresses>,
+        }
+
+        impl<'a> Iterator for AdapterAddressesIter<'a> {
+            type Item = &'a IP_ADAPTER_ADDRESSES_LH;
+
+            fn next(&mut self) -> Option<Self::Item> {
+                match self.ptr {
+                    None => None,
+                    Some(ptr) => {
+                        // SAFETY: The pointer is a valid IP_ADAPTER_ADDRESSES_LH pointer by
+                        // construction so creating a reference to it is OK.
+                        let addr = unsafe { &*ptr.as_ptr() };
+                        self.ptr = ptr::NonNull::new(addr.next);
+                        Some(addr)
+                    }
+                }
+            }
+        }
+
+        struct UnicastAddressesIter<'a> {
+            ptr: Option<ptr::NonNull<IP_ADAPTER_UNICAST_ADDRESS_LH>>,
+            phantom: marker::PhantomData<&'a IP_ADAPTER_ADDRESSES_LH>,
+        }
+
+        impl<'a> UnicastAddressesIter<'a> {
+            fn new(addresses: &'a IP_ADAPTER_ADDRESSES_LH) -> Self {
+                Self {
+                    ptr: ptr::NonNull::new(addresses.firstunicastaddress),
+                    phantom: marker::PhantomData,
+                }
+            }
+        }
+
+        impl<'a> Iterator for UnicastAddressesIter<'a> {
+            type Item = &'a IP_ADAPTER_UNICAST_ADDRESS_LH;
+
+            fn next(&mut self) -> Option<Self::Item> {
+                match self.ptr {
+                    None => None,
+                    Some(ptr) => {
+                        let addr = unsafe { &*ptr.as_ptr() };
+                        self.ptr = ptr::NonNull::new(addr.next);
+                        Some(addr)
+                    }
+                }
+            }
+        }
+
+        let addresses = AdapterAddresses::new().context("Failed getting adapter addresses")?;
+        let mut if_infos = Vec::<InterfaceInfo>::new();
+        for address in addresses.iter() {
+            // SAFETY: adaptername points to a NUL-terminated ASCII name string that is valid
+            // as long as its struct is
+            let adaptername = unsafe { CStr::from_ptr(address.adaptername as *const c_char) }
+                .to_str()
+                .unwrap();
+
+            // Skip adapters that are receive-only, can't do multicast or don't have IPv4 support
+            // as they're not usable in a PTP context.
+            if address.flags & ADAPTER_FLAG_RECEIVE_ONLY != 0
+                || address.flags & ADAPTER_FLAG_NO_MULTICAST != 0
+                || address.flags & ADAPTER_FLAG_IPV4_ENABLED == 0
+            {
+                continue;
+            }
+
+            // Skip adapters that are loopback or not up.
+            if address.iftype == IF_TYPE_SOFTWARE_LOOPBACK
+                || address.operstatus != IF_OPER_STATUS_UP
+            {
+                continue;
+            }
+
+            // SAFETY: Both fields of the union are always valid
+            let index = unsafe { address.anonymous.anonymous.ifindex } as usize;
+            // Skip adapters that have no valid interface index as they can't be used to join the
+            // PTP multicast group reliably for this interface only.
+            if index == 0 {
+                continue;
+            }
+
+            // SAFETY: friendlyname is a NUL-terminated UCS2/wide string or NULL.
+            let friendlyname = unsafe {
+                if !address.friendlyname.is_null() {
+                    let len = {
+                        let mut len = 0;
+                        while *address.friendlyname.add(len) != 0 {
+                            len += 1;
+                        }
+                        len
+                    };
+
+                    let f = slice::from_raw_parts(address.friendlyname, len);
+                    let f = OsString::from_wide(f);
+                    Some(String::from(f.to_str().unwrap()))
+                } else {
+                    None
+                }
+            };
+
+            let mut hw_addr = None;
+            if address.physicaladdresslength == 6 {
+                let mut addr = [0u8; 6];
+                addr.copy_from_slice(&address.physicaladdress[..6]);
+                hw_addr = Some(addr);
+            }
+
+            let ip_addr = UnicastAddressesIter::new(address).find_map(|addr| {
+                if addr.address.lpsocketaddr.is_null() {
+                    return None;
+                }
+
+                // SAFETY: lpsocketaddr is a valid, non-NULL socket address and its family
+                // field can be read to distinguish IPv4 and other socket addresses
+                if unsafe { (*addr.address.lpsocketaddr).sa_family } != AF_INET as u16 {
+                    return None;
+                }
+
+                Some(Ipv4Addr::from(
+                    // SAFETY: lpsocketaddr is a valid, non-NULL IPv4 socket address as checked
+                    // above and can be dereferenced as such.
+                    unsafe {
+                        (*addr.address.lpsocketaddr)
+                            .in_addr
+                            .S_un
+                            .S_addr
+                            .to_ne_bytes()
+                    },
+                ))
+            });
+
+            if let Some(ip_addr) = ip_addr {
+                if_infos.push(InterfaceInfo {
+                    name: String::from(adaptername),
+                    other_name: friendlyname,
+                    index,
+                    ip_addr,
+                    hw_addr,
+                });
+            }
+        }
+
+        Ok(if_infos)
+    }
+
+    // Join multicast address for a given interface.
+    pub fn join_multicast_v4(
+        socket: &UdpSocket,
+        addr: &Ipv4Addr,
+        iface: &InterfaceInfo,
+    ) -> Result<(), Error> {
+        let mreq = IP_MREQ {
+            imr_multiaddr: IN_ADDR {
+                S_un: IN_ADDR_0 {
+                    S_addr: u32::from_ne_bytes(addr.octets()),
+                },
+            },
+            imr_address: IN_ADDR {
+                S_un: IN_ADDR_0 {
+                    S_addr: u32::from_ne_bytes(Ipv4Addr::new(0, 0, 0, iface.index as u8).octets()),
+                },
+            },
+        };
+
+        // SAFETY: Requires a valid ip_mreq struct to be passed together with its size for checking
+        // validity. On errors a negative integer is returned.
+        unsafe {
+            if setsockopt(
+                socket.as_raw_socket(),
+                IPPROTO_IP as i32,
+                IP_ADD_MEMBERSHIP as i32,
+                &mreq as *const _ as *const _,
+                mem::size_of_val(&mreq) as _,
+            ) < 0
+            {
+                bail!(
+                    source: io::Error::from_raw_os_error(WSAGetLastError()),
+                    "Failed joining multicast group for interface {}",
+                    iface.name,
+                );
+            }
+        }
+
+        Ok(())
+    }
+
+    /// Allow multiple sockets to bind to the same address / port.
+    ///
+    /// This is best-effort and might not actually do anything.
+    pub fn set_reuse(socket: &UdpSocket) {
+        // SAFETY: SO_REUSEADDR takes an i32 value that can be 0/false or 1/true and
+        // enables the given feature on the socket.
+        //
+        // We explicitly ignore errors here. If it works, good, if it doesn't then not much
+        // lost other than the ability to run multiple processes at once.
+        unsafe {
+            let v = 1i32;
+            let _ = setsockopt(
+                socket.as_raw_socket(),
+                SOL_SOCKET as i32,
+                SO_REUSEADDR as i32,
+                &v as *const _ as *const _,
+                mem::size_of_val(&v) as _,
+            );
+        }
+    }
+}
+
+pub use imp::*;
diff --git a/subprojects/gstreamer/libs/gst/helpers/ptp/privileges.rs b/subprojects/gstreamer/libs/gst/helpers/ptp/privileges.rs
new file mode 100644
index 0000000000000000000000000000000000000000..f0366db054102c2958d2be3f54ece20b1eb96264
--- /dev/null
+++ b/subprojects/gstreamer/libs/gst/helpers/ptp/privileges.rs
@@ -0,0 +1,159 @@
+use crate::error::Error;
+
+/// Drop all additional permissions / capabilities the current process might have as they're not
+/// needed anymore.
+///
+/// This does nothing if no such mechanism is implemented / selected for the target platform.
+pub fn drop() -> Result<(), Error> {
+    #[cfg(ptp_helper_permissions = "setcap")]
+    {
+        // Drop all current capabilities of the process.
+
+        use std::io;
+
+        use crate::{bail, ffi::unix::setcaps::*};
+
+        struct Cap(cap_t);
+        impl Drop for Cap {
+            fn drop(&mut self) {
+                // SAFETY: The capabilities are valid by construction and are only dropped
+                // once here.
+                unsafe {
+                    let _ = cap_free(self.0);
+                }
+            }
+        }
+
+        // SAFETY: There are 3 steps here
+        //  1. Get the current capabilities of the process. This
+        //     returns NULL on error or otherwise newly allocated capabilities that have to be
+        //     freed again in the end. For that purpose we wrap them in the Cap struct.
+        //
+        //  2. Clearing all current capabilities. This requires a valid capabilities pointer,
+        //     which we have at this point by construction.
+        //
+        //  3. Setting the current process's capabilities, which is only affecting the current
+        //     thread unfortunately. At this point, no other threads were started yet so this is
+        //     not a problem. Also the capabilities pointer is still valid by construction.
+        //
+        //  On every return path, the capabilities are going to be freed.
+        unsafe {
+            let c = cap_get_proc();
+            if c.is_null() {
+                bail!(
+                    source: io::Error::last_os_error(),
+                    "Failed to get current process capabilities"
+                );
+            }
+
+            let c = Cap(c);
+            if cap_clear(c.0) != 0 {
+                bail!(
+                    source: io::Error::last_os_error(),
+                    "Failed to clear capabilities"
+                );
+            }
+            if cap_set_proc(c.0) != 0 {
+                bail!(
+                    source: io::Error::last_os_error(),
+                    "Failed to set current process capabilities"
+                );
+            }
+        }
+    }
+    #[cfg(ptp_helper_permissions = "setuid-root")]
+    {
+        // Drop the process's UID/GID from root to the configured user/group or the user "nobody".
+
+        use std::{ffi::CString, io};
+
+        use crate::{bail, error::Context, ffi::unix::setuid_root::*};
+
+        fn get_uid_gid_for_user(name: &str) -> io::Result<(uid_t, gid_t)> {
+            let name_cstr = CString::new(name).unwrap();
+
+            loop {
+                // SAFETY: getpwnam() requires a NUL-terminated user name string and
+                // returns either the user information in static storage, or NULL on error.
+                // In case of EINTR, getting the user information can be retried.
+                //
+                // The user information is stored in static storage so might change if something
+                // else calls related functions. As this is the only thread up to this point and we
+                // just extract two integers from it there is no such possibility.
+                unsafe {
+                    let pw = getpwnam(name_cstr.as_ptr());
+                    if pw.is_null() {
+                        let err = io::Error::last_os_error();
+                        if err.kind() == io::ErrorKind::Interrupted {
+                            continue;
+                        }
+                        return Err(err);
+                    }
+                    return Ok(((*pw).pw_uid, (*pw).pw_gid));
+                }
+            }
+        }
+
+        fn get_gid_for_group(name: &str) -> io::Result<gid_t> {
+            let name_cstr = CString::new(name).unwrap();
+            loop {
+                // SAFETY: getgrnam() requires a NUL-terminated group name string and
+                // returns either the group information in static storage, or NULL on error.
+                // In case of EINTR, getting the group information can be retried.
+                //
+                // The user information is stored in static storage so might change if something
+                // else calls related functions. As this is the only thread up to this point and we
+                // just extract two integers from it there is no such possibility.
+                unsafe {
+                    let grp = getgrnam(name_cstr.as_ptr());
+                    if grp.is_null() {
+                        let err = io::Error::last_os_error();
+                        if err.kind() == io::ErrorKind::Interrupted {
+                            continue;
+                        }
+                        return Err(err);
+                    }
+
+                    return Ok((*grp).gr_gid);
+                }
+            }
+        }
+
+        let username = gst_ptp_helper_conf::PTP_HELPER_SETUID_USER.unwrap_or("nobody");
+
+        let (uid, gid) = get_uid_gid_for_user(username)
+            .with_context(|| format!("Failed to get user information for {}", username))?;
+        let gid = if let Some(group) = gst_ptp_helper_conf::PTP_HELPER_SETUID_GROUP {
+            get_gid_for_group(group)
+                .with_context(|| format!("Failed to get group information for {}", group))?
+        } else {
+            gid
+        };
+
+        // SAFETY: This function can be called at any time and never fails.
+        let old_gid = unsafe { getgid() };
+
+        // SAFETY: Changes the effective group id of the process and return zero on success.
+        unsafe {
+            if setgid(gid) != 0 {
+                bail!(
+                    source: io::Error::last_os_error(),
+                    "Failed to set group id {} for process",
+                    gid,
+                );
+            }
+        }
+
+        // SAFETY: Changes the effective user id of the process and return zero on success.
+        // On errors, try resetting to the old gid just to be sure.
+        unsafe {
+            if setuid(uid) != 0 {
+                let err = io::Error::last_os_error();
+                let _ = setgid(old_gid);
+                bail!(source: err, "Failed to set user id {} for process", uid);
+            }
+        }
+    }
+
+    Ok(())
+}
diff --git a/subprojects/gstreamer/libs/gst/helpers/ptp_helper_post_install.sh b/subprojects/gstreamer/libs/gst/helpers/ptp/ptp_helper_post_install.sh
similarity index 100%
rename from subprojects/gstreamer/libs/gst/helpers/ptp_helper_post_install.sh
rename to subprojects/gstreamer/libs/gst/helpers/ptp/ptp_helper_post_install.sh
diff --git a/subprojects/gstreamer/libs/gst/helpers/ptp/rand.rs b/subprojects/gstreamer/libs/gst/helpers/ptp/rand.rs
new file mode 100644
index 0000000000000000000000000000000000000000..d53bc030c248e053ed444f87ffdd72d808fb05a9
--- /dev/null
+++ b/subprojects/gstreamer/libs/gst/helpers/ptp/rand.rs
@@ -0,0 +1,140 @@
+/// Returns a random'ish 64 bit value.
+pub fn rand() -> [u8; 8] {
+    #[cfg(unix)]
+    {
+        // Try getrandom syscall or otherwise first on Linux
+        #[cfg(any(target_os = "android", target_os = "linux"))]
+        {
+            use std::io::Read;
+
+            use crate::ffi::unix::linux::*;
+
+            // Depends on us knowing the syscall number
+            if SYS_getrandom != 0 {
+                struct GetRandom;
+
+                impl Read for GetRandom {
+                    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
+                        // SAFETY: `getrandom` syscall fills up to the requested amount of bytes of
+                        // the provided memory and returns the number of bytes or a negative value
+                        // on errors.
+                        unsafe {
+                            let res = syscall(SYS_getrandom, buf.as_mut_ptr(), buf.len(), 0u32);
+                            if res < 0 {
+                                Err(std::io::Error::last_os_error())
+                            } else {
+                                Ok(res as usize)
+                            }
+                        }
+                    }
+                }
+
+                let mut r = [0u8; 8];
+                if GetRandom.read_exact(&mut r).is_ok() {
+                    return r;
+                }
+            }
+        }
+
+        // Otherwise try /dev/urandom
+        {
+            use crate::ffi::unix::*;
+            use std::{io::Read, os::raw::c_int};
+
+            struct Fd(c_int);
+
+            impl Drop for Fd {
+                fn drop(&mut self) {
+                    // SAFETY: The fd is valid by construction below and closed by this at
+                    // most once.
+                    unsafe {
+                        // Return value is intentionally ignored as there's nothing that
+                        // can be done on errors anyway.
+                        let _ = close(self.0);
+                    }
+                }
+            }
+
+            impl Read for Fd {
+                fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
+                    // SAFETY: read() requires a valid fd and a mutable buffer with the given size.
+                    // The fd is valid by construction as is the buffer.
+                    //
+                    // read() will return the number of bytes read or a negative value on errors.
+                    let res = unsafe { read(self.0, buf.as_mut_ptr(), buf.len()) };
+                    if res < 0 {
+                        Err(std::io::Error::last_os_error())
+                    } else {
+                        Ok(res as usize)
+                    }
+                }
+            }
+
+            let fd = loop {
+                // SAFETY: open() requires a NUL-terminated file path and will
+                // return an integer in any case. A negative value is an invalid fd
+                // and signals an error. On EINTR, opening can be retried.
+                let fd = unsafe { open(b"/dev/urandom\0".as_ptr(), O_RDONLY) };
+                if fd < 0 {
+                    let err = std::io::Error::last_os_error();
+                    if err.kind() == std::io::ErrorKind::Interrupted {
+                        continue;
+                    }
+
+                    break None;
+                }
+
+                break Some(Fd(fd));
+            };
+
+            if let Some(mut fd) = fd {
+                let mut r = [0u8; 8];
+
+                if fd.read_exact(&mut r).is_ok() {
+                    return r;
+                }
+            }
+        }
+    }
+    #[cfg(windows)]
+    {
+        // Try BCryptGenRandom(), which is available since Windows Vista
+        //
+        // SAFETY: BCryptGenRandom() fills the provided memory with the requested number of bytes
+        // and returns 0 on success. In that case, all memory was written and is initialized now.
+        unsafe {
+            use std::{mem, ptr};
+
+            use crate::ffi::windows::*;
+
+            let mut r = mem::MaybeUninit::<[u8; 8]>::uninit();
+            let res = BCryptGenRandom(
+                ptr::null_mut(),
+                r.as_mut_ptr() as *mut u8,
+                8,
+                BCRYPT_USE_SYSTEM_PREFERRED_RNG,
+            );
+            if res == 0 {
+                return r.assume_init();
+            }
+        }
+    }
+
+    // As fallback use a combination of the process ID and the current system time
+    let now = std::time::SystemTime::now()
+        .duration_since(std::time::SystemTime::UNIX_EPOCH)
+        .unwrap()
+        .as_nanos()
+        .to_be_bytes();
+    let pid = std::process::id().to_be_bytes();
+    [
+        now[0] ^ now[15] ^ pid[0],
+        now[1] ^ now[14] ^ pid[1],
+        now[2] ^ now[13] ^ pid[2],
+        now[3] ^ now[12] ^ pid[3],
+        now[4] ^ now[11] ^ pid[0],
+        now[5] ^ now[10] ^ pid[1],
+        now[6] ^ now[9] ^ pid[2],
+        now[7] ^ now[8] ^ pid[3],
+    ]
+}