[gnome-network-displays/cc-tmp: 1/80] cc: Add Chromecast support




commit 3f242d359569683506a8ab166ffdab320d52204a
Author: Anupam Kumar <kyteinsky gmail com>
Date:   Fri Jul 1 19:13:14 2022 +0530

    cc: Add Chromecast support
    
    Initial Commit

 src/cc/cc-client.c   | 645 +++++++++++++++++++++++++++++++++++++++++++++++++++
 src/cc/cc-client.h   |  21 ++
 src/meson.build      |   2 +
 src/nd-cc-provider.c | 412 ++++++++++++++++++++++++++++++++
 src/nd-cc-provider.h |  39 ++++
 src/nd-cc-sink.c     | 575 +++++++++++++++++++++++++++++++++++++++++++++
 src/nd-cc-sink.h     |  36 +++
 src/nd-window.c      |   6 +-
 8 files changed, 1735 insertions(+), 1 deletion(-)
---
diff --git a/src/cc/cc-client.c b/src/cc/cc-client.c
new file mode 100644
index 0000000..e990cdd
--- /dev/null
+++ b/src/cc/cc-client.c
@@ -0,0 +1,645 @@
+#include <glib-object.h>
+#include <gst/rtsp/gstrtspmessage.h>
+#include <gst/video/video.h>
+#include "cc-client.h"
+#include "wfd-media-factory.h"
+#include "wfd-media.h"
+#include "wfd-params.h"
+
+typedef enum {
+  INIT_STATE_M0_INVALID = 0,
+  INIT_STATE_M1_SOURCE_QUERY_OPTIONS = 1,
+  INIT_STATE_M2_SINK_QUERY_OPTIONS = 2,
+  INIT_STATE_M3_SOURCE_GET_PARAMS = 3,
+  INIT_STATE_M4_SOURCE_SET_PARAMS = 4,
+  INIT_STATE_M5_SOURCE_TRIGGER_SETUP = 5,
+
+  INIT_STATE_DONE = 9999,
+} CCClientInitState;
+
+typedef enum {
+  CONNECTION_TYPE_WEBRTC = 0,
+  CONNECTION_TYPE_RTSP = 1
+} CCConnectionType;
+
+struct _CCClient
+{
+  GstRTSPClient      parent_instance;
+
+  CCConnectionType  connection_type;
+  guint              keep_alive_source_id;
+
+  CCClientInitState init_state;
+  WfdMedia          *media;
+  WfdParams         *params;
+
+  WfdMediaQuirks     media_quirks;
+};
+
+G_DEFINE_TYPE (CCClient, cc_client, GST_TYPE_RTSP_CLIENT)
+
+// XXX
+static const gchar * supported_rtsp_features[] = {
+  "org.wfa.wfd1.0",
+  "OPTIONS",
+  "DESCRIBE",
+  "GET_PARAMETER",
+  "PAUSE",
+  "PLAY",
+  "SETUP",
+  "SET_PARAMETER",
+  "TEARDOWN",
+  NULL
+};
+
+CCClient *
+cc_client_new (void)
+{
+  return g_object_new (CC_TYPE_CLIENT, NULL);
+}
+
+static void
+cc_client_finalize (GObject *object)
+{
+  CCClient *self = (CCClient *) object;
+
+  g_debug ("CCClient: Finalize");
+
+  g_clear_pointer (&self->params, wfd_params_free);
+
+  if (self->keep_alive_source_id)
+    g_source_remove (self->keep_alive_source_id);
+  self->keep_alive_source_id = 0;
+
+  G_OBJECT_CLASS (cc_client_parent_class)->finalize (object);
+}
+
+gchar *
+cc_client_check_requirements (GstRTSPClient *client, GstRTSPContext *ctx, gchar ** arr)
+{
+  g_autoptr(GPtrArray) unsupported = NULL;
+  gchar **req;
+  char *res = NULL;
+
+  for (req = arr; *req; req++)
+    {
+      if (!g_strv_contains (supported_rtsp_features, *req))
+        {
+          if (unsupported == NULL)
+            unsupported = g_ptr_array_new ();
+          g_ptr_array_add (unsupported, *req);
+        }
+    }
+
+  if (!unsupported)
+    return g_strdup ("");
+
+  res = g_strjoinv (", ", (GStrv) unsupported->pdata);
+  g_warning ("CCClient: Cannot support the following requested features: %s", res);
+
+  return res;
+}
+
+gint
+compare_resolutions (gconstpointer a, gconstpointer b)
+{
+  WfdResolution *res_a = (WfdResolution *) a;
+  WfdResolution *res_b = (WfdResolution *) b;
+  gint a_weight;
+  gint b_weight;
+
+  a_weight = res_a->width * res_a->height * 100 + res_a->refresh_rate / (res_a->interlaced ? 2 : 1) - 
res_a->interlaced;
+  b_weight = res_b->width * res_b->height * 100 + res_b->refresh_rate / (res_b->interlaced ? 2 : 1) - 
res_b->interlaced;
+  return a_weight - b_weight;
+}
+
+void
+cc_client_select_codec_and_resolution (CCClient *self, WfdH264ProfileFlags profile)
+{
+  gint i;
+  WfdVideoCodec *codec = NULL;
+
+  for (i = 0; i < self->params->video_codecs->len; i++)
+    {
+      WfdVideoCodec *item = g_ptr_array_index (self->params->video_codecs, i);
+
+      /* Use the first codec we can find. */
+      if (!codec)
+        codec = item;
+
+      if (codec->profile != item->profile && item->profile == profile)
+        codec = item;
+
+      if (codec->profile == item->profile && item->level > codec->level)
+        codec = item;
+    }
+
+  if (codec)
+    self->params->selected_codec = wfd_video_codec_ref (codec);
+  else
+    g_warning ("No codec/resolution could be found, falling back to defaults!");
+
+#if 0
+  /* The native resolution reported by some devices is just useless */
+  if (codec->native)
+    {
+      self->params->selected_resolution = wfd_resolution_copy (codec->native);
+    }
+  else
+    {
+      /* Find a good resolution. */
+      g_autoptr(GList) resolutions = NULL;
+      GList *last;
+
+      resolutions = wfd_video_codec_get_resolutions (codec);
+      resolutions = g_list_sort (resolutions, compare_resolutions);
+      last = g_list_last (resolutions);
+      if (last)
+        {
+          self->params->selected_resolution = wfd_resolution_copy ((WfdResolution *) last->data);
+        }
+      else
+        {
+#endif
+  /* Create a standard full HD resolution if everything fails. */
+  g_warning ("CCClient: No resolution found, falling back to standard FullHD resolution.");
+  self->params->selected_resolution = wfd_resolution_new ();
+  self->params->selected_resolution->width = 1920;
+  self->params->selected_resolution->height = 1080;
+  self->params->selected_resolution->refresh_rate = 30;
+  self->params->selected_resolution->interlaced = FALSE;
+#if 0
+}
+}
+#endif
+  g_debug ("selected resolution %i, %i @%i", self->params->selected_resolution->width, 
self->params->selected_resolution->height, self->params->selected_resolution->refresh_rate);
+
+  /* We currently only support AAC with two channels  */
+  for (i = 0; i < self->params->audio_codecs->len; i++)
+    {
+      WfdAudioCodec *codec = g_ptr_array_index (self->params->audio_codecs, i);
+
+      /* Accept AAC with 48KHz and 2 channels; this is currently hardcoded in the media factory */
+      if (codec->type == WFD_AUDIO_AAC && codec->modes & 0x1)
+        {
+          self->params->selected_audio_codec = wfd_audio_codec_new ();
+          self->params->selected_audio_codec->type = WFD_AUDIO_AAC;
+          self->params->selected_audio_codec->modes = G_GUINT64_CONSTANT (0x00000001);
+        }
+    }
+}
+
+gboolean
+cc_client_configure_client_media (GstRTSPClient * client,
+                                   GstRTSPMedia * media, GstRTSPStream * stream,
+                                   GstRTSPContext * ctx)
+{
+  CCClient *self = CC_CLIENT (client);
+
+  g_autoptr(GstElement) element = NULL;
+  gboolean res;
+
+  g_return_val_if_fail (self->params->selected_codec, FALSE);
+  g_return_val_if_fail (self->params->selected_resolution, FALSE);
+
+  self->media = WFD_MEDIA (media);
+
+  element = gst_rtsp_media_get_element (media);
+  self->media_quirks = wfd_configure_media_element (GST_BIN (element), self->params);
+
+  res = GST_RTSP_CLIENT_CLASS (cc_client_parent_class)->configure_client_media (client, media, stream, ctx);
+
+  return res;
+}
+
+static gboolean
+cc_client_idle_trigger_setup (gpointer user_data)
+{
+  cc_client_trigger_method (CC_CLIENT (user_data), "SETUP");
+  g_object_unref (user_data);
+
+  return G_SOURCE_REMOVE;
+}
+
+static gchar *
+cc_client_get_presentation_uri (CCClient *self)
+{
+  g_autoptr(GSocketAddress) sock_addr = NULL;
+  GstRTSPClient *client = GST_RTSP_CLIENT (self);
+  GstRTSPConnection *connection;
+  GSocket *socket;
+  GInetAddress *inet_addr;
+  g_autofree gchar *addr = NULL;
+  gint port;
+
+  connection = gst_rtsp_client_get_connection (client);
+  socket = gst_rtsp_connection_get_read_socket (connection);
+  sock_addr = g_socket_get_local_address (socket, NULL);
+  inet_addr = g_inet_socket_address_get_address (G_INET_SOCKET_ADDRESS (sock_addr));
+  addr = g_inet_address_to_string (inet_addr);
+  port = g_inet_socket_address_get_port (G_INET_SOCKET_ADDRESS (sock_addr));
+
+  // XXX
+  return g_strdup_printf ("rtsp://%s:%d/wfd1.0/streamid=0", addr, port);
+}
+
+static void
+cc_client_set_params (CCClient *self)
+{
+  GstRTSPMessage msg = { 0 };
+  g_autofree gchar * body = NULL;
+  g_autofree gchar * presentation_uri = NULL;
+  g_autofree gchar * resolution_descr = NULL;
+  g_autofree gchar * audio_descr = NULL;
+
+  self->init_state = INIT_STATE_M4_SOURCE_SET_PARAMS;
+
+  gst_rtsp_message_init_request (&msg, GST_RTSP_SET_PARAMETER, "rtsp://localhost/wfd1.0");
+
+  presentation_uri = cc_client_get_presentation_uri (self);
+  resolution_descr = wfd_video_codec_get_descriptor_for_resolution (self->params->selected_codec, 
self->params->selected_resolution);
+  audio_descr = wfd_audio_get_descriptor (self->params->selected_audio_codec);
+
+  body = g_strdup_printf (
+    "wfd_video_formats: %s\r\n"
+    "wfd_audio_codecs: %s\r\n"
+    "wfd_presentation_URL: %s none\r\n"
+    "wfd_client_rtp_ports: RTP/AVP/UDP;unicast %u %u mode=play\r\n",
+    resolution_descr,
+    audio_descr,
+    presentation_uri,
+    self->params->primary_rtp_port, self->params->secondary_rtp_port);
+
+  gst_rtsp_message_add_header_by_name (&msg, "Content-Type", "text/parameters");
+  gst_rtsp_message_set_body (&msg, (guint8 *) body, strlen (body));
+
+  gst_rtsp_client_send_message (GST_RTSP_CLIENT (self), NULL, &msg);
+
+  gst_rtsp_message_unset (&msg);
+}
+
+static gboolean
+cc_client_idle_set_params (gpointer user_data)
+{
+  cc_client_set_params (CC_CLIENT (user_data));
+  g_object_unref (user_data);
+
+  return G_SOURCE_REMOVE;
+}
+
+GstRTSPFilterResult
+cc_client_touch_session_filter_func (GstRTSPClient  *client,
+                                      GstRTSPSession *sess,
+                                      gpointer        user_data)
+{
+  gst_rtsp_session_touch (sess);
+
+  return GST_RTSP_FILTER_KEEP;
+}
+
+void
+cc_client_handle_response (GstRTSPClient * client, GstRTSPContext *ctx)
+{
+  CCClient *self = CC_CLIENT (client);
+
+  /* Some sinks do not reply with the correct session-id. Which causes
+   * gst-rtsp-server to not touch the session, triggering a timeout
+   * even though the sink actually replied.
+   *
+   * Work around this by explicitly touching the session (again). And
+   * to do that, just touch all of them, which is acceptable as we will
+   * only have one.
+   */
+  gst_rtsp_client_session_filter (client, cc_client_touch_session_filter_func, NULL);
+
+  /* Track the initialization process and possibly trigger the
+   * next state of the connection establishment. */
+  switch (self->init_state)
+    {
+    case INIT_STATE_M1_SOURCE_QUERY_OPTIONS:
+      g_debug ("CCClient: OPTIONS querying done");
+      self->init_state = INIT_STATE_M2_SINK_QUERY_OPTIONS;
+      break;
+
+    case INIT_STATE_M3_SOURCE_GET_PARAMS:
+      g_debug ("CCClient: GET_PARAMS done");
+      wfd_params_from_sink (self->params, ctx->response->body, ctx->response->body_size);
+
+      /* XXX: Pick the better profile if we have an encoder that supports it! */
+      cc_client_select_codec_and_resolution (self, WFD_H264_PROFILE_BASE);
+
+      g_idle_add (cc_client_idle_set_params, g_object_ref (self));
+      break;
+
+    case INIT_STATE_M4_SOURCE_SET_PARAMS:
+      g_debug ("CCClient: SET_PARAMS done");
+      g_idle_add (cc_client_idle_trigger_setup, g_object_ref (self));
+      break;
+
+    case INIT_STATE_M5_SOURCE_TRIGGER_SETUP:
+      self->init_state = INIT_STATE_DONE;
+      g_debug ("CCClient: Initialization done!");
+      break;
+
+    default:
+      /* Nothing to be done in the other states. */
+      break;
+    }
+}
+
+static gchar *
+cc_client_make_path_from_uri (GstRTSPClient * client, const GstRTSPUrl * uri)
+{
+  GstRTSPContext *ctx = gst_rtsp_context_get_current ();
+
+  /* Strip /streamid=0.
+   * This is a bad hack, because gstreamer does not support playing/pausing
+   * a specific stream. We can do so safely because we only have one stream.
+   */
+  if (ctx->request &&
+      (ctx->request->type_data.request.method == GST_RTSP_PLAY ||
+       ctx->request->type_data.request.method == GST_RTSP_PAUSE))
+    {
+      if (g_str_has_suffix (uri->abspath, "/streamid=0"))
+        return g_strndup (uri->abspath, strlen (uri->abspath) - 11);
+      else
+        return g_strdup (uri->abspath);
+    }
+  else
+    {
+      return GST_RTSP_CLIENT_CLASS (cc_client_parent_class)->make_path_from_uri (client, uri);
+    }
+}
+
+GstRTSPFilterResult
+cc_client_timeout_session_filter_func (GstRTSPClient  *client,
+                                        GstRTSPSession *sess,
+                                        gpointer        user_data)
+{
+  GstRTSPMessage msg = { 0 };
+
+  g_debug ("CCClient: Doing keep-alive");
+
+  gst_rtsp_message_init_request (&msg, GST_RTSP_GET_PARAMETER, "rtsp://localhost/wfd1.0/streamid=0");
+
+  gst_rtsp_client_send_message (client, sess, &msg);
+
+  gst_rtsp_message_unset (&msg);
+
+  return GST_RTSP_FILTER_KEEP;
+}
+
+static gboolean
+cc_client_keep_alive_timeout (gpointer user_data)
+{
+  GstRTSPClient *client = GST_RTSP_CLIENT (user_data);
+
+  gst_rtsp_client_session_filter (client, cc_client_timeout_session_filter_func, NULL);
+
+  return G_SOURCE_CONTINUE;
+}
+
+static void
+cc_client_new_session (GstRTSPClient *client, GstRTSPSession *session)
+{
+  CCClient *self = CC_CLIENT (client);
+
+  // XXX: look into this
+  /* The WFD standard suggests a timeout of 30 seconds */
+  gst_rtsp_session_set_timeout (session, 30);
+  g_object_set (session, "timeout-always-visible", FALSE, NULL);
+
+  // XXX: this too
+  if (self->connection_type == CONNECTION_TYPE_WEBRTC && self->keep_alive_source_id == 0)
+    self->keep_alive_source_id = g_timeout_add_seconds (25, cc_client_keep_alive_timeout, client);
+}
+
+static GstRTSPResult
+cc_client_params_set (GstRTSPClient *client, GstRTSPContext *ctx)
+{
+  CCClient *self = CC_CLIENT (client);
+  g_autofree gchar *body_str = NULL;
+
+  g_auto(GStrv) lines = NULL;
+  gchar **line = NULL;
+
+  gst_rtsp_message_init_response (ctx->response, GST_RTSP_STS_OK,
+                                  gst_rtsp_status_as_text (GST_RTSP_STS_OK), ctx->request);
+
+  if (ctx->request->body == NULL || ctx->request->body_size == 0)
+    return GST_RTSP_OK;
+
+  body_str = g_strndup ((gchar *) ctx->request->body, ctx->request->body_size);
+  lines = g_strsplit (body_str, "\n", 0);
+
+  for (line = lines; *line; line++)
+    {
+      g_auto(GStrv) split_line = NULL;
+      gchar *option;
+      G_GNUC_UNUSED gchar *value;
+
+      g_strstrip (*line);
+
+      /* Ignore empty lines */
+      if (**line == '\0')
+        continue;
+
+      split_line = g_strsplit (*line, ":", 2);
+
+      option = g_strstrip (split_line[0]);
+      if (split_line[1])
+        value = g_strstrip (split_line[1]);
+      else
+        value = NULL;
+
+      if (g_str_equal (option, "wfd_idr_request"))
+        {
+          /* Force a key unit event. */
+          if (self->media_quirks & WFD_QUIRK_NO_IDR)
+            {
+              g_debug ("Cannot force key frame as the pipeline doesn't support it!");
+            }
+          else if (self->media)
+            {
+              GstRTSPStream *stream;
+              g_autoptr(GstPad) srcpad = NULL;
+              g_autoptr(GstEvent) event = NULL;
+
+              stream = gst_rtsp_media_get_stream (GST_RTSP_MEDIA (self->media), 0);
+              if (!stream)
+                return GST_RTSP_OK;
+
+              srcpad = gst_rtsp_stream_get_srcpad (stream);
+
+              g_debug ("Forcing a keyframe!");
+              event = gst_video_event_new_upstream_force_key_unit (GST_CLOCK_TIME_NONE, TRUE, 0);
+              gst_pad_send_event (srcpad, g_steal_pointer (&event));
+            }
+          else
+            {
+              g_debug ("Cannot force key frame currently, no media!");
+            }
+        }
+      else
+        {
+          g_debug ("Ignoring unknown parameter %s", option);
+        }
+    }
+
+  return GST_RTSP_OK;
+}
+
+static gboolean
+cc_client_idle_cc_query_params (gpointer user_data)
+{
+  CCClient *self = CC_CLIENT (user_data);
+  GstRTSPMessage msg = { 0 };
+  g_autofree gchar * query_params = NULL;
+
+  g_debug ("CC query params");
+
+  self->init_state = INIT_STATE_M3_SOURCE_GET_PARAMS;
+
+  gst_rtsp_message_init_request (&msg, GST_RTSP_GET_PARAMETER, "rtsp://localhost/wfd1.0");
+  gst_rtsp_message_add_header_by_name (&msg, "Content-Type", "text/parameters");
+  query_params = wfd_params_m3_query_params (self->params);
+  gst_rtsp_message_set_body (&msg, (guint8 *) query_params, strlen (query_params));
+
+  gst_rtsp_client_send_message (GST_RTSP_CLIENT (self), NULL, &msg);
+
+  gst_rtsp_message_unset (&msg);
+  g_object_unref (user_data);
+
+  return G_SOURCE_REMOVE;
+}
+
+GstRTSPStatusCode
+cc_client_pre_options_request (GstRTSPClient *client, GstRTSPContext *ctx)
+{
+  CCClient *self = CC_CLIENT (client);
+
+  if (self->init_state <= INIT_STATE_M2_SINK_QUERY_OPTIONS)
+    {
+      if (self->init_state != INIT_STATE_M2_SINK_QUERY_OPTIONS)
+        {
+          g_message ("CCClient: Got OPTIONS before getting reply querying WFD support; assuming normal RTSP 
connection.");
+          /* The standard says to disconnect. However, if we do this,
+           * then it is possible to connect a normal RTSP client for testing.
+           * e.g. VLC will play back the stream correctly.
+           *
+           * Also flag the connection as "normal" RTSP and set a selected audio codec.
+           */
+          self->connection_type = CONNECTION_TYPE_RTSP;
+
+          /* Enable audio with AAC and 2 channels (48kHz), currently hardcoded in the media factory*/
+          self->params->selected_audio_codec = wfd_audio_codec_new ();
+          self->params->selected_audio_codec->type = WFD_AUDIO_AAC;
+          self->params->selected_audio_codec->modes = G_GUINT64_CONSTANT (0x00000001);
+
+          self->init_state = INIT_STATE_DONE;
+        }
+      else
+        {
+          g_idle_add (cc_client_idle_cc_query_params, g_object_ref (client));
+        }
+    }
+
+  return GST_RTSP_STS_OK;
+}
+
+
+static void
+cc_client_send_message (GstRTSPClient *client, GstRTSPContext *ctx, GstRTSPMessage *msg)
+{
+  gchar *hdr = NULL;
+
+  /* Hook for sending a message. */
+
+  /* Modify the "Public" header to advertise support for WFD 1.0 */
+  gst_rtsp_message_get_header (msg, GST_RTSP_HDR_PUBLIC, &hdr, 0);
+  if (hdr)
+    {
+      g_autofree gchar * new_hdr = NULL;
+
+      new_hdr = g_strconcat ("org.wfa.wfd1.0, ", hdr, NULL);
+
+      gst_rtsp_message_remove_header (msg, GST_RTSP_HDR_PUBLIC, -1);
+      gst_rtsp_message_add_header (msg, GST_RTSP_HDR_PUBLIC, new_hdr);
+    }
+
+  /* Strip away any ;timeout=30 from outgoing messages. This seems to be
+   *  confuse some clients. */
+  gst_rtsp_message_get_header (msg, GST_RTSP_HDR_SESSION, &hdr, 0);
+  if (msg->type == GST_RTSP_MESSAGE_REQUEST && hdr)
+    {
+      if (g_str_has_suffix (hdr, ";timeout=30"))
+        {
+          g_autofree gchar * new_hdr = NULL;
+
+          new_hdr = g_strndup (hdr, strlen (hdr) - 11);
+          gst_rtsp_message_remove_header (msg, GST_RTSP_HDR_SESSION, -1);
+          gst_rtsp_message_add_header (msg, GST_RTSP_HDR_SESSION, new_hdr);
+        }
+    }
+}
+
+static void
+cc_client_class_init (CCClientClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GstRTSPClientClass *client_class = GST_RTSP_CLIENT_CLASS (klass);
+
+  object_class->finalize = cc_client_finalize;
+
+  client_class->check_requirements = cc_client_check_requirements;
+  client_class->configure_client_media = cc_client_configure_client_media;
+  client_class->handle_response = cc_client_handle_response;
+  client_class->make_path_from_uri = cc_client_make_path_from_uri;
+  client_class->new_session = cc_client_new_session;
+  client_class->params_set = cc_client_params_set;
+  client_class->pre_options_request = cc_client_pre_options_request;
+  client_class->send_message = cc_client_send_message;
+}
+
+static void
+cc_client_init (CCClient *self)
+{
+  self->init_state = INIT_STATE_M0_INVALID;
+  self->params = wfd_params_new ();
+}
+
+void
+cc_client_query_support (CCClient *self)
+{
+  GstRTSPMessage msg = { 0 };
+
+  if (self->init_state != INIT_STATE_M0_INVALID)
+    return;
+
+  self->init_state = INIT_STATE_M1_SOURCE_QUERY_OPTIONS;
+  gst_rtsp_message_init_request (&msg, GST_RTSP_OPTIONS, "*");
+  gst_rtsp_message_add_header_by_name (&msg, "Require", "org.wfa.wfd1.0");
+
+  gst_rtsp_client_send_message (GST_RTSP_CLIENT (self), NULL, &msg);
+
+  gst_rtsp_message_unset (&msg);
+}
+
+void
+cc_client_trigger_method (CCClient *self, const gchar *method)
+{
+  GstRTSPMessage msg = { 0 };
+  g_autofree gchar *body = NULL;
+
+  if (g_str_equal (method, "SETUP") && self->init_state == INIT_STATE_M4_SOURCE_SET_PARAMS)
+    self->init_state = INIT_STATE_M5_SOURCE_TRIGGER_SETUP;
+
+  gst_rtsp_message_init_request (&msg, GST_RTSP_SET_PARAMETER, "rtsp://localhost/wfd1.0");
+  body = g_strdup_printf ("wfd_trigger_method: %s\r\n", method);
+  gst_rtsp_message_set_body (&msg, (guint8 *) body, strlen (body));
+  gst_rtsp_message_add_header_by_name (&msg, "Content-Type", "text/parameters");
+
+  gst_rtsp_client_send_message (GST_RTSP_CLIENT (self), NULL, &msg);
+
+  gst_rtsp_message_unset (&msg);
+}
diff --git a/src/cc/cc-client.h b/src/cc/cc-client.h
new file mode 100644
index 0000000..5a4c291
--- /dev/null
+++ b/src/cc/cc-client.h
@@ -0,0 +1,21 @@
+#pragma once
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
+#include <gst/rtsp-server/rtsp-client.h>
+#pragma GCC diagnostic pop
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_CLIENT (cc_client_get_type ())
+#define CC_CLIENT_GET_CLASS(obj)    (G_TYPE_INSTANCE_GET_CLASS ((obj), CC_TYPE_CLIENT, CCClientClass))
+
+G_DECLARE_FINAL_TYPE (CCClient, cc_client, CC, CLIENT, GstRTSPClient)
+
+CCClient * cc_client_new (void);
+void cc_client_query_support (CCClient *self);
+void cc_client_trigger_method (CCClient   *self,
+                                const gchar *method);
+
+
+G_END_DECLS
diff --git a/src/meson.build b/src/meson.build
index c50da56..13ddba5 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -14,6 +14,8 @@ gnome_nd_sources = [
   'nd-meta-provider.c',
   'nd-wfd-mice-sink.c',
   'nd-wfd-mice-provider.c',
+  'nd-cc-sink.c',
+  'nd-cc-provider.c',
   'nd-wfd-p2p-sink.c',
   'nd-wfd-p2p-provider.c',
   'nd-nm-device-registry.c',
diff --git a/src/nd-cc-provider.c b/src/nd-cc-provider.c
new file mode 100644
index 0000000..3e4de08
--- /dev/null
+++ b/src/nd-cc-provider.c
@@ -0,0 +1,412 @@
+/* nd-cc-provider.c
+ *
+ * Copyright 2022 Christian Glombek <lorbus fedoraproject org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <avahi-gobject/ga-service-browser.h>
+#include <avahi-gobject/ga-service-resolver.h>
+#include <avahi-common/address.h>
+#include "gnome-network-displays-config.h"
+#include "nd-cc-provider.h"
+#include "nd-sink.h"
+#include "nd-cc-sink.h"
+
+struct _NdCCProvider
+{
+  GObject parent_instance;
+
+  GPtrArray *sinks;
+  GaClient *avahi_client;
+
+  GSocketClient *signalling_client;
+  // GSocketService *signalling_server;
+
+  gboolean discover;
+};
+
+enum
+{
+  PROP_CLIENT = 1,
+
+  PROP_DISCOVER,
+
+  PROP_LAST = PROP_DISCOVER,
+};
+
+static void nd_cc_provider_provider_iface_init(NdProviderIface *iface);
+static GList *nd_cc_provider_provider_get_sinks(NdProvider *provider);
+
+G_DEFINE_TYPE_EXTENDED(NdCCProvider, nd_cc_provider, G_TYPE_OBJECT, 0,
+                       G_IMPLEMENT_INTERFACE(ND_TYPE_PROVIDER,
+                                             nd_cc_provider_provider_iface_init);)
+
+static GParamSpec *props[PROP_LAST] = {
+    NULL,
+};
+
+static void
+nd_cc_provider_get_property(GObject *object,
+                            guint prop_id,
+                            GValue *value,
+                            GParamSpec *pspec)
+{
+  NdCCProvider *provider = ND_CC_PROVIDER(object);
+
+  switch (prop_id)
+  {
+  case PROP_CLIENT:
+    g_assert(provider->avahi_client == NULL);
+    g_value_set_object(value, provider->avahi_client);
+    break;
+
+  case PROP_DISCOVER:
+    g_value_set_boolean(value, provider->discover);
+    break;
+
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+    break;
+  }
+}
+
+static void
+nd_cc_provider_set_property(GObject *object,
+                            guint prop_id,
+                            const GValue *value,
+                            GParamSpec *pspec)
+{
+  NdCCProvider *provider = ND_CC_PROVIDER(object);
+
+  switch (prop_id)
+  {
+  case PROP_CLIENT:
+    /* Construct only */
+    provider->avahi_client = g_value_dup_object(value);
+    break;
+
+  case PROP_DISCOVER:
+    provider->discover = g_value_get_boolean(value);
+    break;
+
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+    break;
+  }
+}
+
+static void
+nd_cc_provider_finalize(GObject *object)
+{
+  NdCCProvider *provider = ND_CC_PROVIDER(object);
+  GError *error;
+
+  g_clear_pointer(&provider->sinks, g_ptr_array_unref);
+
+  if (provider->signalling_client)
+  {
+    if (!g_socket_close((GSocket *)provider->signalling_client, &error))
+      g_warning("NdCCProvider: Error closing signalling client socket: %s", error->message);
+    g_object_unref(provider->signalling_client);
+  }
+
+  // if (provider->signalling_server)
+  // {
+  //   if (!g_socket_close((GSocket *)provider->signalling_server, &error))
+  //     g_warning("NdCCProvider: Error closing signalling server socket: %s", error->message);
+  //   g_object_unref(provider->signalling_server);
+  // }
+
+  G_OBJECT_CLASS(nd_cc_provider_parent_class)->finalize(object);
+}
+
+static void
+nd_cc_provider_class_init(NdCCProviderClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS(klass);
+
+  object_class->get_property = nd_cc_provider_get_property;
+  object_class->set_property = nd_cc_provider_set_property;
+  object_class->finalize = nd_cc_provider_finalize;
+
+  props[PROP_CLIENT] =
+      g_param_spec_object("client", "Client",
+                          "The AvahiClient used to find sinks.",
+                          GA_TYPE_CLIENT,
+                          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties(object_class, PROP_LAST, props);
+
+  g_object_class_override_property(object_class, PROP_DISCOVER, "discover");
+}
+
+static void
+resolver_found_cb(GaServiceResolver *resolver,
+                  AvahiIfIndex iface,
+                  GaProtocol proto,
+                  gchar *name,
+                  gchar *type,
+                  gchar *domain,
+                  gchar *hostname,
+                  AvahiAddress *addr,
+                  gint port,
+                  AvahiStringList *txt,
+                  GaLookupResultFlags flags,
+                  NdCCProvider *provider)
+{
+  NdCCSink *sink = NULL;
+  gchar address[AVAHI_ADDRESS_STR_MAX];
+
+  g_debug("NdCCProvider: Found sink %s at %s:%d on interface %i", name, hostname, port, iface);
+
+  if (avahi_address_snprint(address, sizeof(address), addr) == NULL)
+    g_warning("NdCCProvider: Failed to convert AvahiAddress to string");
+
+  g_debug("NdCCProvider: Resolved %s to %s", hostname, address);
+
+  sink = nd_cc_sink_new(provider->signalling_client, name, address);
+
+  g_object_unref(resolver);
+
+  g_ptr_array_add(provider->sinks, sink);
+  g_signal_emit_by_name(provider, "sink-added", sink);
+}
+
+static void
+resolver_failure_cb(GaServiceResolver *resolver,
+                    GError *error,
+                    NdCCProvider *provider)
+{
+  g_warning("NdCCProvider: Failed to resolve Avahi service: %s", error->message);
+  g_object_unref(resolver);
+}
+
+static void
+service_added_cb(GaServiceBrowser *browser,
+                 AvahiIfIndex iface,
+                 GaProtocol proto,
+                 gchar *name,
+                 gchar *type,
+                 gchar *domain,
+                 GaLookupResultFlags flags,
+                 NdCCProvider *provider)
+{
+  GaServiceResolver *resolver;
+  GError *error = NULL;
+
+  resolver = ga_service_resolver_new(iface,
+                                     proto,
+                                     name,
+                                     type,
+                                     domain,
+                                     GA_PROTOCOL_INET,
+                                     GA_LOOKUP_NO_FLAGS);
+
+  g_signal_connect(resolver,
+                   "found",
+                   (GCallback)resolver_found_cb,
+                   provider);
+
+  g_signal_connect(resolver,
+                   "failure",
+                   (GCallback)resolver_failure_cb,
+                   provider);
+
+  if (!ga_service_resolver_attach(resolver,
+                                  provider->avahi_client,
+                                  &error))
+  {
+    g_warning("NdCCProvider: Failed to attach Avahi resolver: %s", error->message);
+    g_error_free(error);
+  }
+}
+
+static void
+service_removed_cb(GaServiceBrowser *browser,
+                   AvahiIfIndex iface,
+                   GaProtocol proto,
+                   gchar *name,
+                   gchar *type,
+                   gchar *domain,
+                   GaLookupResultFlags flags,
+                   NdCCProvider *provider)
+{
+  g_debug("NdCCProvider: mDNS service \"%s\" removed from interface %i", name, iface);
+
+  for (gint i = 0; i < provider->sinks->len; i++)
+  {
+    g_autoptr(NdCCSink) sink = g_object_ref(g_ptr_array_index(provider->sinks, i));
+
+    NdSinkState state = nd_cc_sink_get_state(sink);
+    if (state == ND_SINK_STATE_WAIT_STREAMING ||
+        state == ND_SINK_STATE_STREAMING)
+      continue;
+
+    gchar *remote_name = NULL;
+    g_object_get(sink, "name", &remote_name, NULL);
+    if (remote_name == name)
+    {
+      g_debug("NdCCProvider: Removing sink");
+      g_ptr_array_remove_index(provider->sinks, i);
+      g_signal_emit_by_name(provider, "sink-removed", sink);
+      break;
+    }
+  }
+}
+
+// // TODO: msg receiving callback
+// static void
+// signalling_incoming_cb(GSocketService *service,
+//                        GSocketConnection *connection,
+//                        GObject *source,
+//                        gpointer user_data)
+// {
+//   /* TODO */
+//   /* read full message, find sink, and respond with client */
+
+//   /* NdCCProvider * self = ND_CC_PROVIDER (user_data); */
+
+//   gchar buffer[1024];
+//   GInputStream *istream;
+//   GError *error;
+
+//   istream = g_io_stream_get_input_stream(G_IO_STREAM(connection));
+
+//   g_input_stream_read(istream, buffer, sizeof(buffer), NULL, &error);
+//   if (error != NULL)
+//     g_warning("NdCCProvider: Failed to connect to signalling host: %s", error->message);
+
+//   g_debug("NdCCProvider: Received Message: %s", buffer);
+
+//   return;
+// }
+
+// TODO: is GThreadedSocketService required?
+static void
+nd_cc_provider_init(NdCCProvider *provider)
+{
+  // g_autoptr(GError) error = NULL;
+  // GSocketService *server;
+
+  provider->discover = TRUE;
+  provider->sinks = g_ptr_array_new_with_free_func(g_object_unref);
+  provider->signalling_client = g_socket_client_new();
+  // server = g_socket_service_new();
+
+  // g_socket_listener_add_inet_port((GSocketListener *)server,
+  //                                 8009,
+  //                                 NULL,
+  //                                 &error);
+  // if (error != NULL)
+  // {
+  //   g_warning("NdCCProvider: Error starting signal listener: %s", error->message);
+  //   return;
+  // }
+
+  // g_signal_connect(server,
+  //                  "incoming",
+  //                  G_CALLBACK(signalling_incoming_cb),
+  //                  provider);
+
+  // g_socket_service_start(server);
+
+  // provider->signalling_server = server;
+}
+
+/******************************************************************
+ * NdProvider interface implementation
+ ******************************************************************/
+
+static void
+nd_cc_provider_provider_iface_init(NdProviderIface *iface)
+{
+  iface->get_sinks = nd_cc_provider_provider_get_sinks;
+}
+
+static GList *
+nd_cc_provider_provider_get_sinks(NdProvider *provider)
+{
+  NdCCProvider *cc_provider = ND_CC_PROVIDER(provider);
+  GList *res = NULL;
+
+  for (gint i = 0; i < cc_provider->sinks->len; i++)
+    res = g_list_prepend(res, g_ptr_array_index(cc_provider->sinks, i));
+
+  return res;
+}
+
+/******************************************************************
+ * NdCCProvider public functions
+ ******************************************************************/
+
+GaClient *
+nd_cc_provider_get_client(NdCCProvider *provider)
+{
+  return provider->avahi_client;
+}
+
+GSocketClient *
+nd_cc_provider_get_signalling_client(NdCCProvider *provider)
+{
+  return provider->signalling_client;
+}
+
+// GSocketService *
+// nd_cc_provider_get_signalling_server(NdCCProvider *provider)
+// {
+//   return provider->signalling_server;
+// }
+
+NdCCProvider *
+nd_cc_provider_new(GaClient *client)
+{
+  return g_object_new(ND_TYPE_CC_PROVIDER,
+                      "client", client,
+                      NULL);
+}
+
+gboolean
+nd_cc_provider_browse(NdCCProvider *provider, GError *error)
+{
+  GaServiceBrowser *avahi_browser;
+
+  avahi_browser = ga_service_browser_new("_googlecast._tcp");
+
+  if (provider->avahi_client == NULL)
+  {
+    g_warning("NdCCProvider: No Avahi client found");
+    return FALSE;
+  }
+
+  g_signal_connect(avahi_browser,
+                   "new-service",
+                   (GCallback)service_added_cb,
+                   provider);
+
+  g_signal_connect(avahi_browser,
+                   "removed-service",
+                   (GCallback)service_removed_cb,
+                   provider);
+
+  if (!ga_service_browser_attach(avahi_browser,
+                                 provider->avahi_client,
+                                 &error))
+  {
+    g_warning("NdCCProvider: Failed to attach Avahi Service Browser: %s", error->message);
+    return FALSE;
+  }
+
+  return TRUE;
+}
diff --git a/src/nd-cc-provider.h b/src/nd-cc-provider.h
new file mode 100644
index 0000000..924f5d6
--- /dev/null
+++ b/src/nd-cc-provider.h
@@ -0,0 +1,39 @@
+/* nd-cc-provider.h
+ *
+ * Copyright 2022 Christian Glombek <lorbus fedoraproject org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <avahi-gobject/ga-client.h>
+#include <gio/gio.h>
+#include "nd-provider.h"
+
+G_BEGIN_DECLS
+
+#define ND_TYPE_CC_PROVIDER (nd_cc_provider_get_type ())
+G_DECLARE_FINAL_TYPE (NdCCProvider, nd_cc_provider, ND, CC_PROVIDER, GObject)
+
+NdCCProvider * nd_cc_provider_new (GaClient * client);
+
+GaClient * nd_cc_provider_get_client (NdCCProvider *provider);
+
+GSocketClient * nd_cc_provider_get_signalling_client (NdCCProvider *provider);
+
+gboolean nd_cc_provider_browse (NdCCProvider * provider,
+                                GError       * error);
+
+G_END_DECLS
diff --git a/src/nd-cc-sink.c b/src/nd-cc-sink.c
new file mode 100644
index 0000000..f768ca1
--- /dev/null
+++ b/src/nd-cc-sink.c
@@ -0,0 +1,575 @@
+/* nd-cc-sink.c
+ *
+ * Copyright 2022 Christian Glombek <lorbus fedoraproject org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "gnome-network-displays-config.h"
+#include "nd-cc-sink.h"
+#include "cc/cc-client.h"
+#include "wfd/wfd-media-factory.h"
+#include "wfd/wfd-server.h"
+
+struct _NdCCSink
+{
+  GObject            parent_instance;
+
+  NdSinkState        state;
+
+  GCancellable      *cancellable;
+
+  GStrv              missing_video_codec;
+  GStrv              missing_audio_codec;
+  char              *missing_firewall_zone;
+
+  gchar             *remote_address;
+  gchar             *remote_name;
+
+  GSocketClient     *comm_client;
+  GSocketConnection *comm_client_conn; 
+
+  WfdServer         *server;
+  guint              server_source_id;
+};
+
+enum {
+  PROP_CLIENT = 1,
+  PROP_NAME,
+  PROP_ADDRESS,
+
+  PROP_DISPLAY_NAME,
+  PROP_MATCHES,
+  PROP_PRIORITY,
+  PROP_STATE,
+  PROP_MISSING_VIDEO_CODEC,
+  PROP_MISSING_AUDIO_CODEC,
+  PROP_MISSING_FIREWALL_ZONE,
+
+  PROP_LAST = PROP_DISPLAY_NAME,
+};
+
+// interface related functions
+static void nd_cc_sink_sink_iface_init (NdSinkIface *iface);
+static NdSink * nd_cc_sink_sink_start_stream (NdSink *sink);
+static void nd_cc_sink_sink_stop_stream (NdSink *sink);
+
+static void nd_cc_sink_sink_stop_stream_int (NdCCSink *self);
+
+G_DEFINE_TYPE_EXTENDED (NdCCSink, nd_cc_sink, G_TYPE_OBJECT, 0,
+                        G_IMPLEMENT_INTERFACE (ND_TYPE_SINK,
+                                               nd_cc_sink_sink_iface_init);
+                       )
+
+static GParamSpec * props[PROP_LAST] = { NULL, };
+
+// TODO: this should be the protobuf msg
+// msg sent after connection
+static gchar msg_source_ready[] = {
+  0x00, 0x29, /* Length (41 bytes) */
+  0x01, /* MICE Protocol Version */
+  0x01, /* Command SOURCE_READY */
+
+  0x00, /* Friendly Name TLV */
+  0x00, 0x0A, /* Length (10 bytes) */
+  /* GNOME (UTF-16-encoded) */
+  0x47, 0x00, 0x4E, 0x00, 0x4F, 0x00, 0x4D, 0x00, 0x45, 0x00,
+
+  0x02, /* RTSP Port TLV */
+  0x00, 0x02, /* Length (2 bytes) */
+  0x1C, 0x44, /* Port 7236 */
+
+  0x03, /* Source ID TLV */
+  0x00, 0x10, /* Length (16 bits) */
+  /* Source ID GnomeMICEDisplay (ascii) */
+  0x47, 0x6E, 0x6F, 0x6D, 0x65, 0x4D, 0x49, 0x43, 0x45, 0x44, 0x69, 0x73, 0x70, 0x6C, 0x61, 0x79
+};
+
+static gchar msg_stop_projection[] = {
+  0x00, 0x24, /* Length (36 bytes) */
+  0x01, /* MICE Protocol Version */
+  0x02, /* Command STOP_PROJECTION */
+
+  0x00, /* Friendly Name TLV */
+  0x00, 0x0A, /* Length (10 bytes) */
+  /* GNOME (UTF-16-encoded) */
+  0x47, 0x00, 0x4E, 0x00, 0x4F, 0x00, 0x4D, 0x00, 0x45, 0x00,
+
+  0x03, /* Source ID TLV */
+  0x00, 0x10, /* Length (16 bytes) */
+  /* Source ID GnomeMICEDisplay (ascii) */
+  0x47, 0x6E, 0x6F, 0x6D, 0x65, 0x4D, 0x49, 0x43, 0x45, 0x44, 0x69, 0x73, 0x70, 0x6C, 0x61, 0x79
+};
+
+static void
+nd_cc_sink_get_property (GObject *    object,
+                         guint        prop_id,
+                         GValue *     value,
+                         GParamSpec * pspec)
+{
+  NdCCSink *sink = ND_CC_SINK (object);
+
+  switch (prop_id)
+    {
+    case PROP_CLIENT:
+      g_value_set_object (value, sink->comm_client);
+      break;
+
+    case PROP_NAME:
+      g_value_set_string (value, sink->remote_name);
+      break;
+
+    case PROP_ADDRESS:
+      g_value_set_string (value, sink->remote_address);
+      break;
+
+    case PROP_DISPLAY_NAME:
+      g_object_get_property (G_OBJECT (sink), "name", value);
+      break;
+
+    case PROP_MATCHES:
+      {
+        g_autoptr(GPtrArray) res = NULL;
+        res = g_ptr_array_new_with_free_func (g_free);
+
+        if (sink->remote_name)
+          g_ptr_array_add (res, g_strdup (sink->remote_name));
+
+        g_value_take_boxed (value, g_steal_pointer (&res));
+        break;
+      }
+
+    case PROP_PRIORITY:
+      g_value_set_int (value, 100);
+      break;
+
+    case PROP_STATE:
+      g_value_set_enum (value, sink->state);
+      break;
+
+    case PROP_MISSING_VIDEO_CODEC:
+      g_value_set_boxed (value, sink->missing_video_codec);
+      break;
+
+    case PROP_MISSING_AUDIO_CODEC:
+      g_value_set_boxed (value, sink->missing_audio_codec);
+      break;
+
+    case PROP_MISSING_FIREWALL_ZONE:
+      g_value_set_string (value, sink->missing_firewall_zone);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+      break;
+    }
+}
+
+static void
+nd_cc_sink_set_property (GObject      *object,
+                         guint         prop_id,
+                         const GValue *value,
+                         GParamSpec   *pspec)
+{
+  NdCCSink *sink = ND_CC_SINK (object);
+
+  switch (prop_id)
+    {
+    case PROP_CLIENT:
+      /* Construct only */
+      sink->comm_client = g_value_dup_object (value);
+      break;
+
+    case PROP_NAME:
+      sink->remote_name = g_value_dup_string (value);
+      g_object_notify (G_OBJECT (sink), "display-name");
+      break;
+
+    case PROP_ADDRESS:
+      g_assert (sink->remote_address == NULL);
+      sink->remote_address = g_value_dup_string (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+      break;
+    }
+}
+
+void
+nd_cc_sink_finalize (GObject *object)
+{
+  NdCCSink *sink = ND_CC_SINK (object);
+
+  g_debug ("NdCCSink: Finalizing");
+
+  nd_cc_sink_sink_stop_stream_int (sink);
+
+  g_cancellable_cancel (sink->cancellable);
+  g_clear_object (&sink->cancellable);
+
+  g_clear_pointer (&sink->missing_video_codec, g_strfreev);
+  g_clear_pointer (&sink->missing_audio_codec, g_strfreev);
+  g_clear_pointer (&sink->missing_firewall_zone, g_free);
+
+  G_OBJECT_CLASS (nd_cc_sink_parent_class)->finalize (object);
+}
+
+static void
+nd_cc_sink_class_init (NdCCSinkClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->get_property = nd_cc_sink_get_property;
+  object_class->set_property = nd_cc_sink_set_property;
+  object_class->finalize = nd_cc_sink_finalize;
+
+  props[PROP_CLIENT] =
+    g_param_spec_object ("client", "Communication Client",
+                         "The GSocketClient used for Chromecast communication.",
+                         G_TYPE_SOCKET_CLIENT,
+                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+  props[PROP_NAME] =
+    g_param_spec_string ("name", "Sink Name",
+                         "The sink name found by the Avahi Client.",
+                         NULL,
+                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+  props[PROP_ADDRESS] =
+    g_param_spec_string ("address", "Sink Address",
+                         "The address the sink was found on.",
+                         NULL,
+                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (object_class, PROP_LAST, props);
+
+  g_object_class_override_property (object_class, PROP_DISPLAY_NAME, "display-name");
+  g_object_class_override_property (object_class, PROP_MATCHES, "matches");
+  g_object_class_override_property (object_class, PROP_PRIORITY, "priority");
+  g_object_class_override_property (object_class, PROP_STATE, "state");
+  g_object_class_override_property (object_class, PROP_MISSING_VIDEO_CODEC, "missing-video-codec");
+  g_object_class_override_property (object_class, PROP_MISSING_AUDIO_CODEC, "missing-audio-codec");
+  g_object_class_override_property (object_class, PROP_MISSING_FIREWALL_ZONE, "missing-firewall-zone");
+}
+
+static void
+nd_cc_sink_init (NdCCSink *sink)
+{
+  sink->state = ND_SINK_STATE_DISCONNECTED;
+  sink->cancellable = g_cancellable_new ();
+}
+
+/******************************************************************
+* NdSink interface implementation
+******************************************************************/
+
+static void
+nd_cc_sink_sink_iface_init (NdSinkIface *iface)
+{
+  iface->start_stream = nd_cc_sink_sink_start_stream;
+  iface->stop_stream = nd_cc_sink_sink_stop_stream;
+}
+
+static void
+play_request_cb (NdCCSink *sink, GstRTSPContext *ctx, CCClient *client)
+{
+  g_debug ("NdCCSink: Got play request from client");
+
+  sink->state = ND_SINK_STATE_STREAMING;
+  g_object_notify (G_OBJECT (sink), "state");
+}
+
+gboolean
+comm_client_send (NdCCSink      * self,
+                  GSocketClient * client,
+                  gchar         * remote_address,
+                  const void    * message,
+                  gssize          size,
+                  GCancellable  * cancellable,
+                  GError        * error)
+{
+  GOutputStream * ostream;
+
+  if (self->comm_client_conn == NULL)
+    self->comm_client_conn = g_socket_client_connect_to_host (client,
+                                                              (gchar *) remote_address,
+                                                              8009,
+                                                              // 8010,
+                                                              NULL,
+                                                              &error);
+
+  if (!self->comm_client_conn || error != NULL)
+    {
+      if (error != NULL)
+        g_warning ("NdCCSink: Failed to write to communication stream: %s", error->message);
+
+      return FALSE;
+
+    }
+
+  g_assert (G_IO_STREAM (self->comm_client_conn));
+
+  g_debug ("NdCCSink: Client connection established");
+
+  ostream = g_io_stream_get_output_stream (G_IO_STREAM (self->comm_client_conn));
+  if (!ostream)
+    {
+      g_warning ("NdCCSink: Could not signal to sink");
+
+      return FALSE;
+    }
+
+  size = g_output_stream_write (ostream, message, size, cancellable, &error);
+  if (error != NULL)
+    {
+      if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK))
+        {
+          g_warning ("NdCCSink: Communication client socket send would block");
+          return FALSE;
+        }
+      else
+        {
+          g_warning ("NdCCSink: Error writing to client socket output: %s", error->message);
+          return FALSE;
+        }
+    }
+
+  g_debug ("NdCCSink: Sent %" G_GSSIZE_FORMAT " bytes of data", size);
+
+  return TRUE;
+}
+
+static void
+closed_cb (NdCCSink *sink, CCClient *client)
+{
+  g_autoptr(GError) error = NULL;
+
+  /* Connection was closed, do a clean shutdown*/
+  gboolean comm_client_ok = comm_client_send (sink,
+                                              sink->comm_client,
+                                              sink->remote_address,
+                                              msg_stop_projection,
+                                              sizeof (msg_stop_projection),
+                                              NULL,
+                                              error);
+
+  if (!comm_client_ok || error != NULL)
+    {
+      if (error != NULL)
+        g_warning ("NdCCSink: Failed to send stop projection cmd to client: %s", error->message);
+      else
+        g_warning ("NdCCSink: Failed to send stop projection cmd to client");
+
+      sink->state = ND_SINK_STATE_ERROR;
+      g_object_notify (G_OBJECT (sink), "state");
+      g_clear_object (&sink->server);
+    }
+  nd_cc_sink_sink_stop_stream (ND_SINK (sink));
+}
+
+static void
+client_connected_cb (NdCCSink *sink, CCClient *client, WfdServer *server)
+{
+  g_debug ("NdCCSink: Got client connection");
+
+  g_signal_handlers_disconnect_by_func (sink->server, client_connected_cb, sink);
+  sink->state = ND_SINK_STATE_WAIT_STREAMING;
+  g_object_notify (G_OBJECT (sink), "state");
+
+  /* XXX: connect to further events. */
+  g_signal_connect_object (client,
+                           "play-request",
+                           (GCallback) play_request_cb,
+                           sink,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (client,
+                           "closed",
+                           (GCallback) closed_cb,
+                           sink,
+                           G_CONNECT_SWAPPED);
+}
+
+static GstElement *
+server_create_source_cb (NdCCSink *sink, WfdServer *server)
+{
+  GstElement *res;
+
+  g_signal_emit_by_name (sink, "create-source", &res);
+  g_debug ("NdCCSink: Create source signal emitted");
+  return res;
+}
+
+static GstElement *
+server_create_audio_source_cb (NdCCSink *sink, WfdServer *server)
+{
+  GstElement *res;
+
+  g_signal_emit_by_name (sink, "create-audio-source", &res);
+  g_debug ("NdCCSink: Create audio source signal emitted");
+
+  return res;
+}
+
+static NdSink *
+nd_cc_sink_sink_start_stream (NdSink *sink)
+{
+  NdCCSink *self = ND_CC_SINK (sink);
+  g_autoptr(GError) error = NULL;
+  gboolean send_ok;
+
+  g_return_val_if_fail (self->state == ND_SINK_STATE_DISCONNECTED, NULL);
+
+  g_assert (self->server == NULL);
+
+  // self->state = ND_SINK_STATE_ENSURE_FIREWALL;
+  // g_object_notify (G_OBJECT (self), "state");
+
+  g_debug ("NdCCSink: Attempting connection to Chromecast: %s", self->remote_name);
+
+  // not using server for now
+  self->server = wfd_server_new ();
+  self->server_source_id = gst_rtsp_server_attach (GST_RTSP_SERVER (self->server), NULL);
+
+  if (self->server_source_id == 0 || self->remote_address == NULL)
+    {
+      self->state = ND_SINK_STATE_ERROR;
+      g_object_notify (G_OBJECT (self), "state");
+      g_clear_object (&self->server);
+
+      return;
+    }
+
+  g_signal_connect_object (self->server,
+                           "client-connected",
+                           (GCallback) client_connected_cb,
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->server,
+                           "create-source",
+                           (GCallback) server_create_source_cb,
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->server,
+                           "create-audio-source",
+                           (GCallback) server_create_audio_source_cb,
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  self->state = ND_SINK_STATE_WAIT_SOCKET;
+  g_object_notify (G_OBJECT (self), "state");
+
+  send_ok = comm_client_send (self,
+                              self->comm_client,
+                              self->remote_address,
+                              msg_source_ready,
+                              sizeof (msg_source_ready),
+                              NULL,
+                              error);
+  if (!send_ok || error != NULL)
+    {
+      if (error != NULL)
+        g_warning ("NdCCSink: Failed to connect to Chromecast: %s", error->message);
+      else
+        g_warning ("NdCCSink: Failed to connect to Chromecast");
+
+      self->state = ND_SINK_STATE_ERROR;
+      g_object_notify (G_OBJECT (self), "state");
+      g_clear_object (&self->server);
+    }
+
+  return g_object_ref (sink);
+}
+
+static void
+nd_cc_sink_sink_stop_stream_int (NdCCSink *self)
+{
+  g_autoptr(GError) error;
+  gboolean close_ok;
+
+  g_cancellable_cancel (self->cancellable);
+  g_clear_object (&self->cancellable);
+
+  self->cancellable = g_cancellable_new ();
+
+  /* Close the client connection */
+  if (self->comm_client_conn != NULL)
+    {
+      close_ok = g_io_stream_close (G_IO_STREAM (self->comm_client_conn), NULL, &error);
+      if (error != NULL)
+        {
+          g_warning ("NdCCSink: Error closing communication client connection: %s", error->message);
+        }
+      if (!close_ok)
+        {
+          g_warning ("NdCCSink: Communication client connection not closed");
+        }
+
+      g_clear_object (&self->comm_client_conn);
+      g_debug ("NdCCSink: Client connection removed");
+    }
+
+  /* Destroy the server that is streaming. */
+  if (self->server_source_id)
+    {
+      g_source_remove (self->server_source_id);
+      self->server_source_id = 0;
+    }
+
+  /* Needs to protect against recursion. */
+  if (self->server)
+    {
+      g_autoptr(WfdServer) server = NULL;
+
+      server = g_steal_pointer (&self->server);
+      g_signal_handlers_disconnect_by_data (server, self);
+      wfd_server_purge (server);
+    }
+}
+
+static void
+nd_cc_sink_sink_stop_stream (NdSink *sink)
+{
+  NdCCSink *self = ND_CC_SINK (sink);
+
+  nd_cc_sink_sink_stop_stream_int (self);
+
+  self->state = ND_SINK_STATE_DISCONNECTED;
+  g_object_notify (G_OBJECT (self), "state");
+}
+
+/******************************************************************
+* NdCCSink public functions
+******************************************************************/
+
+NdCCSink *
+nd_cc_sink_new (GSocketClient *client,
+                gchar         *name,
+                gchar         *remote_address)
+{
+  return g_object_new (ND_TYPE_CC_SINK,
+                       "client", client,
+                       "name", name,
+                       "address", remote_address,
+                       NULL);
+}
+
+NdSinkState
+nd_cc_sink_get_state (NdCCSink *sink)
+{
+  return sink->state;
+}
diff --git a/src/nd-cc-sink.h b/src/nd-cc-sink.h
new file mode 100644
index 0000000..5f3a0fd
--- /dev/null
+++ b/src/nd-cc-sink.h
@@ -0,0 +1,36 @@
+/* nd-cc-sink.h
+ *
+ * Copyright 2022 Christian Glombek <lorbus fedoraproject org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <gio/gio.h>
+#include <gtk/gtk.h>
+#include "nd-sink.h"
+
+G_BEGIN_DECLS
+
+#define ND_TYPE_CC_SINK (nd_cc_sink_get_type ())
+G_DECLARE_FINAL_TYPE (NdCCSink, nd_cc_sink, ND, CC_SINK, GObject)
+
+NdCCSink * nd_cc_sink_new (GSocketClient *client,
+                           gchar         *name,
+                           gchar         *remote_address);
+
+NdSinkState nd_cc_sink_get_state (NdCCSink *sink);
+
+G_END_DECLS
diff --git a/src/nd-window.c b/src/nd-window.c
index 7a155f5..37adf0e 100644
--- a/src/nd-window.c
+++ b/src/nd-window.c
@@ -28,6 +28,7 @@
 #include "nd-nm-device-registry.h"
 #include "nd-dummy-provider.h"
 #include "nd-wfd-mice-provider.h"
+#include "nd-cc-provider.h"
 
 #include <gst/gst.h>
 
@@ -324,6 +325,7 @@ gnome_nd_window_constructed (GObject *obj)
 {
   g_autoptr(GError) error = NULL;
   g_autoptr(NdWFDMiceProvider) mice_provider = NULL;
+  g_autoptr(NdCCProvider) cc_provider = NULL;
   NdWindow *self = ND_WINDOW (obj);
 
   self->cancellable = g_cancellable_new ();
@@ -341,8 +343,9 @@ gnome_nd_window_constructed (GObject *obj)
   g_debug ("NdWindow: Got avahi client");
 
   mice_provider = nd_wfd_mice_provider_new (self->avahi_client);
+  cc_provider = nd_cc_provider_new (self->avahi_client);
 
-  if (!nd_wfd_mice_provider_browse (mice_provider, error))
+  if (!nd_wfd_mice_provider_browse (mice_provider, error) || !nd_cc_provider_browse (cc_provider, error))
     {
       g_warning ("NdWindow: Avahi client failed to browse: %s", error->message);
       return;
@@ -350,6 +353,7 @@ gnome_nd_window_constructed (GObject *obj)
 
   g_debug ("NdWindow: Got avahi browser");
   nd_meta_provider_add_provider (self->meta_provider, ND_PROVIDER (mice_provider));
+  nd_meta_provider_add_provider (self->meta_provider, ND_PROVIDER (cc_provider));
 }
 
 static void


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]