qtdemux: add context for a preferred protection

qtdemux selected the first system corresponding to a working GStreamer
decryptor. With this change, before selecting that decryptor, qtdemux
will check if it has context (a preferred decryptor id) and if not, it
will request it.

The request includes track-id, available key system ids for the
available decryptors and even the events so that the init data is
accessible.

[eocanha@igalia.com: select the preferred protection system even if not available]

Test "4. ClearKeyVideo" in YouTube leanback EME conformance tests 2016 for
H.264[1] uses a media file[2] with cenc encryption which embeds 'pssh' boxes
with the init data for the Playready and Widevine encryption systems, but not
for the ClearKey encryption system (as defined by the EMEv0.1b spec[3] and with
the encryption system id defined in [4]).

Instead, the ClearKey encryption system is manually selected by the web page
code (even if not originally detected by qtdemux) and the proper decryption key
is dispatched to the decryptor, which can then decrypt the video successfully.

[1] http://yt-dash-mse-test.commondatastorage.googleapis.com/unit-tests/2016.html?test_type=encryptedmedia-test&webm=false
[2] http://yt-dash-mse-test.commondatastorage.googleapis.com/unit-tests/media/car_cenc-20120827-86.mp4
[3] https://dvcs.w3.org/hg/html-media/raw-file/eme-v0.1b/encrypted-media/encrypted-media.html#simple-decryption-clear-key
[4] https://www.w3.org/Bugs/Public/show_bug.cgi?id=24027#c2

https://bugzilla.gnome.org/show_bug.cgi?id=770107
diff --git a/gst/isomp4/qtdemux.c b/gst/isomp4/qtdemux.c
index 705bdf7..da22463 100644
--- a/gst/isomp4/qtdemux.c
+++ b/gst/isomp4/qtdemux.c
@@ -511,6 +511,8 @@
 #endif
 static GstStateChangeReturn gst_qtdemux_change_state (GstElement * element,
     GstStateChange transition);
+static void gst_qtdemux_set_context (GstElement * element,
+    GstContext * context);
 static gboolean qtdemux_sink_activate (GstPad * sinkpad, GstObject * parent);
 static gboolean qtdemux_sink_activate_mode (GstPad * sinkpad,
     GstObject * parent, GstPadMode mode, gboolean active);
@@ -600,6 +602,7 @@
   gstelement_class->set_index = GST_DEBUG_FUNCPTR (gst_qtdemux_set_index);
   gstelement_class->get_index = GST_DEBUG_FUNCPTR (gst_qtdemux_get_index);
 #endif
+  gstelement_class->set_context = GST_DEBUG_FUNCPTR (gst_qtdemux_set_context);
 
   gst_tag_register_musicbrainz_tags ();
 
@@ -658,6 +661,7 @@
   qtdemux->cenc_aux_info_sizes = NULL;
   qtdemux->cenc_aux_sample_count = 0;
   qtdemux->protection_system_ids = NULL;
+  qtdemux->preferred_protection_system_id = NULL;
   g_queue_init (&qtdemux->protection_event_queue);
   gst_segment_init (&qtdemux->segment, GST_FORMAT_TIME);
   qtdemux->tag_list = gst_tag_list_new_empty ();
@@ -2121,6 +2125,10 @@
       g_ptr_array_free (qtdemux->protection_system_ids, TRUE);
       qtdemux->protection_system_ids = NULL;
     }
+    if (qtdemux->preferred_protection_system_id) {
+      g_free (qtdemux->preferred_protection_system_id);
+      qtdemux->preferred_protection_system_id = NULL;
+    }
   } else if (qtdemux->mss_mode) {
     gst_flow_combiner_reset (qtdemux->flowcombiner);
     for (n = 0; n < qtdemux->n_streams; n++)
@@ -2607,6 +2615,28 @@
 }
 
 static void
+gst_qtdemux_set_context (GstElement * element, GstContext * context)
+{
+  GstQTDemux *qtdemux = GST_QTDEMUX (element);
+
+  g_return_if_fail (GST_IS_CONTEXT (context));
+
+  if (gst_context_has_context_type (context,
+          "drm-preferred-decryption-system-id")) {
+    const GstStructure *s;
+
+    s = gst_context_get_structure (context);
+    g_free (qtdemux->preferred_protection_system_id);
+    qtdemux->preferred_protection_system_id =
+        g_strdup (gst_structure_get_string (s, "decryption-system-id"));
+    GST_DEBUG_OBJECT (element, "set preferred decryption system to %s",
+        qtdemux->preferred_protection_system_id);
+  }
+
+  GST_ELEMENT_CLASS (parent_class)->set_context (element, context);
+}
+
+static void
 qtdemux_parse_ftyp (GstQTDemux * qtdemux, const guint8 * buffer, gint length)
 {
   /* counts as header data */
@@ -3847,6 +3877,8 @@
   event = gst_event_new_protection (sysid_string, pssh,
       (parent_box_type == FOURCC_moov) ? "isobmff/moov" : "isobmff/moof");
   for (i = 0; i < qtdemux->n_streams; ++i) {
+    GST_TRACE_OBJECT (qtdemux,
+        "adding protection event for stream %d and system %s", i, sysid_string);
     g_queue_push_tail (&qtdemux->streams[i]->protection_scheme_event_queue,
         gst_event_ref (event));
   }
@@ -5563,6 +5595,8 @@
     GstEvent *event;
 
     while ((event = g_queue_pop_head (&stream->protection_scheme_event_queue))) {
+      GST_TRACE_OBJECT (stream->pad, "pushing protection event: %"
+          GST_PTR_FORMAT, event);
       gst_pad_push_event (stream->pad, event);
     }
 
@@ -7757,11 +7791,141 @@
 }
 
 static gboolean
+pad_query (const GValue * item, GValue * value, gpointer user_data)
+{
+  GstPad *pad = g_value_get_object (item);
+  GstQuery *query = user_data;
+  gboolean res;
+
+  res = gst_pad_peer_query (pad, query);
+
+  if (res) {
+    g_value_set_boolean (value, TRUE);
+    return FALSE;
+  }
+
+  GST_INFO_OBJECT (pad, "pad peer query failed");
+  return TRUE;
+}
+
+static gboolean
+gst_qtdemux_run_query (GstElement * element, GstQuery * query,
+    GstPadDirection direction)
+{
+  GstIterator *it;
+  GstIteratorFoldFunction func = pad_query;
+  GValue res = { 0, };
+
+  g_value_init (&res, G_TYPE_BOOLEAN);
+  g_value_set_boolean (&res, FALSE);
+
+  /* Ask neighbor */
+  if (direction == GST_PAD_SRC)
+    it = gst_element_iterate_src_pads (element);
+  else
+    it = gst_element_iterate_sink_pads (element);
+
+  while (gst_iterator_fold (it, func, &res, query) == GST_ITERATOR_RESYNC)
+    gst_iterator_resync (it);
+
+  gst_iterator_free (it);
+
+  return g_value_get_boolean (&res);
+}
+
+static void
+gst_qtdemux_request_protection_context (GstQTDemux * qtdemux,
+    QtDemuxStream * stream)
+{
+  GstQuery *query;
+  GstContext *ctxt;
+  GstElement *element = GST_ELEMENT (qtdemux);
+  GstStructure *st;
+  gchar **filtered_sys_ids;
+  GValue event_list = G_VALUE_INIT;
+  GList *walk;
+
+  /* 1. Check if we already have the context. */
+  if (qtdemux->preferred_protection_system_id != NULL) {
+    GST_LOG_OBJECT (element,
+        "already have the protection context, no need to request it again");
+    return;
+  }
+
+  g_ptr_array_add (qtdemux->protection_system_ids, NULL);
+  filtered_sys_ids = gst_protection_filter_systems_by_available_decryptors (
+      (const gchar **) qtdemux->protection_system_ids->pdata);
+  g_ptr_array_remove_index (qtdemux->protection_system_ids,
+      qtdemux->protection_system_ids->len - 1);
+  GST_TRACE_OBJECT (qtdemux, "detected %u protection systems, we have "
+      "decryptors for %u of them, running context request",
+      qtdemux->protection_system_ids->len, g_strv_length (filtered_sys_ids));
+
+  if (stream->protection_scheme_event_queue.length) {
+    GST_TRACE_OBJECT (qtdemux, "using stream event queue, length %u",
+        stream->protection_scheme_event_queue.length);
+    walk = stream->protection_scheme_event_queue.tail;
+  } else {
+    GST_TRACE_OBJECT (qtdemux, "using demuxer event queue, length %u",
+        qtdemux->protection_event_queue.length);
+    walk = qtdemux->protection_event_queue.tail;
+  }
+
+  g_value_init (&event_list, GST_TYPE_LIST);
+  for (; walk; walk = g_list_previous (walk)) {
+    GValue *event_value = g_new0 (GValue, 1);
+    g_value_init (event_value, GST_TYPE_EVENT);
+    g_value_set_boxed (event_value, walk->data);
+    gst_value_list_append_and_take_value (&event_list, event_value);
+  }
+
+  /*  2a) Query downstream with GST_QUERY_CONTEXT for the context and
+   *      check if downstream already has a context of the specific type
+   *  2b) Query upstream as above.
+   */
+  query = gst_query_new_context ("drm-preferred-decryption-system-id");
+  st = gst_query_writable_structure (query);
+  gst_structure_set (st, "track-id", G_TYPE_UINT, stream->track_id,
+      "stream-encryption-systems", G_TYPE_STRV, filtered_sys_ids, NULL);
+  gst_structure_set_value (st, "stream-encryption-events", &event_list);
+  if (gst_qtdemux_run_query (element, query, GST_PAD_SRC)) {
+    gst_query_parse_context (query, &ctxt);
+    GST_INFO_OBJECT (element, "found context (%p) in downstream query", ctxt);
+    gst_element_set_context (element, ctxt);
+  } else if (gst_qtdemux_run_query (element, query, GST_PAD_SINK)) {
+    gst_query_parse_context (query, &ctxt);
+    GST_INFO_OBJECT (element, "found context (%p) in upstream query", ctxt);
+    gst_element_set_context (element, ctxt);
+  } else {
+    /* 3) Post a GST_MESSAGE_NEED_CONTEXT message on the bus with
+     *    the required context type and afterwards check if a
+     *    usable context was set now as in 1). The message could
+     *    be handled by the parent bins of the element and the
+     *    application.
+     */
+    GstMessage *msg;
+
+    GST_INFO_OBJECT (element, "posting need context message");
+    msg = gst_message_new_need_context (GST_OBJECT_CAST (element),
+        "drm-preferred-decryption-system-id");
+    st = (GstStructure *) gst_message_get_structure (msg);
+    gst_structure_set (st, "track-id", G_TYPE_UINT, stream->track_id,
+        "stream-encryption-systems", G_TYPE_STRV, filtered_sys_ids, NULL);
+    gst_structure_set_value (st, "stream-encryption-events", &event_list);
+    gst_element_post_message (element, msg);
+  }
+
+  g_strfreev (filtered_sys_ids);
+  g_value_unset (&event_list);
+  gst_query_unref (query);
+}
+
+static gboolean
 gst_qtdemux_configure_protected_caps (GstQTDemux * qtdemux,
     QtDemuxStream * stream)
 {
   GstStructure *s;
-  const gchar *selected_system;
+  const gchar *selected_system = NULL;
 
   g_return_val_if_fail (qtdemux != NULL, FALSE);
   g_return_val_if_fail (stream != NULL, FALSE);
@@ -7777,17 +7941,41 @@
         "cenc protection system information has been found");
     return FALSE;
   }
-  g_ptr_array_add (qtdemux->protection_system_ids, NULL);
-  selected_system = gst_protection_select_system ((const gchar **)
-      qtdemux->protection_system_ids->pdata);
-  g_ptr_array_remove_index (qtdemux->protection_system_ids,
-      qtdemux->protection_system_ids->len - 1);
+
+  gst_qtdemux_request_protection_context (qtdemux, stream);
+  if (qtdemux->preferred_protection_system_id != NULL) {
+    const gchar *preferred_system_array[] =
+        { qtdemux->preferred_protection_system_id, NULL };
+
+    selected_system = gst_protection_select_system (preferred_system_array);
+
+    if (selected_system) {
+      GST_TRACE_OBJECT (qtdemux, "selected preferred system %s",
+          qtdemux->preferred_protection_system_id);
+    } else {
+      GST_WARNING_OBJECT (qtdemux, "could not select preferred system %s "
+          "because there is no available decryptor",
+          qtdemux->preferred_protection_system_id);
+    }
+  }
+
+  if (!selected_system) {
+    g_ptr_array_add (qtdemux->protection_system_ids, NULL);
+    selected_system = gst_protection_select_system ((const gchar **)
+        qtdemux->protection_system_ids->pdata);
+    g_ptr_array_remove_index (qtdemux->protection_system_ids,
+        qtdemux->protection_system_ids->len - 1);
+  }
+
   if (!selected_system) {
     GST_ERROR_OBJECT (qtdemux, "stream is protected, but no "
         "suitable decryptor element has been found");
     return FALSE;
   }
 
+  GST_DEBUG_OBJECT (qtdemux, "selected protection system is %s",
+      selected_system);
+
   s = gst_caps_get_structure (CUR_STREAM (stream)->caps, 0);
   if (!gst_structure_has_name (s, "application/x-cenc")) {
     gst_structure_set (s,
diff --git a/gst/isomp4/qtdemux.h b/gst/isomp4/qtdemux.h
index ad4da3e..0793723 100644
--- a/gst/isomp4/qtdemux.h
+++ b/gst/isomp4/qtdemux.h
@@ -151,6 +151,7 @@
   guint64 cenc_aux_info_offset;
   guint8 *cenc_aux_info_sizes;
   guint32 cenc_aux_sample_count;
+  gchar *preferred_protection_system_id;
 
 
   /*