// Copyright (C) 2022 Amlogic, Inc. All rights reserved. // // All information contained herein is Amlogic confidential. // // This software is provided to you pursuant to Software License // Agreement (SLA) with Amlogic Inc ("Amlogic"). This software may be // used only in accordance with the terms of this agreement. // // Redistribution and use in source and binary forms, with or without // modification is strictly prohibited without prior written permission // from Amlogic. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #ifdef SECMEM #include #endif #include "gstwlcdmi.h" #include "wlcdmi.h" #include #define ARRAY_SIZE(x) (sizeof(x)/sizeof(x[0])) #define WIDEVINE_SYSTEM_ID "edef8ba9-79d6-4ace-a3c8-27dcd51d21ed" #define PLAYREADY_SYSTEM_ID "9a04f079-9840-4286-ab92-e65be0885f95" #define CLEARKEY_SYSTEM_ID "e2719d58-a985-b3c9-781a-b030af78d30e" #define VERIMATRIX_SYSTEM_ID "9a27dd82-fde2-4725-8cbc-4234aa06ec09" #define PLAYREADY_OLD_SYSTEM_ID "79f0049a-4098-8642-ab92-e65be0885f95" #define IV_SIZE 16 #define GST_CAPS_FEATURE_MEMORY_DMABUF "memory:DMABuf" struct _GstWLCdmi { GstBaseTransform element; WLCdmiSession *session; GData *cryptos; gchar *license_url; gchar *proxy; guint key_type; gboolean secure; gboolean external_allocator; GstAllocator *allocator; gboolean key_ready; gboolean key_error; GMutex mutex; GCond condition; guint audio_valid_type; guint scheme; gboolean is_vmx; guint origin; gchar last_iv[IV_SIZE]; gboolean is_playready; gboolean need_parser; // for vp9/av1 link to secure parse }; struct _GstWLCdmiClass { GstBaseTransformClass parent_class; /* Signals */ GstAllocator* (*need_allocator) (GstWLCdmi *self, gboolean is_4k, uint8_t decoder_format); guint* (*custom_event) (GstWLCdmi *self, guint type, gchar *pbEvent, guint cbEvent); }; static struct { const gchar *system; const gchar *scheme; } scheme_systems[] = { {PLAYREADY_SYSTEM_ID, "application/x-cenc"}, {WIDEVINE_SYSTEM_ID, "application/x-cenc"}, {VERIMATRIX_SYSTEM_ID, "application/x-cenc"}, {PLAYREADY_OLD_SYSTEM_ID, "application/x-cenc"}, {NULL, "application/x-aes128-cbc"}, {NULL, "application/x-webm-enc"}, }; static struct { const gchar *format; const gchar *scheme; const gboolean au_byte_stream; } scheme_formats[] = { {"video/x-h264", "application/x-cenc", FALSE}, {"video/x-h264", "application/x-aes128-cbc", TRUE}, {"video/x-h265", "application/x-cenc", FALSE}, {"video/x-h265", "application/x-aes128-cbc", TRUE}, {"audio/mpeg", "application/x-cenc", FALSE}, {"audio/mpeg", "application/x-aes128-cbc", FALSE}, {"audio/x-ac3", "application/x-aes128-cbc", FALSE}, {"video/x-vp9", "application/x-webm-enc", FALSE}, {"video/x-vp9", "application/x-cenc", FALSE}, {"video/x-av1", "application/x-webm-enc", FALSE}, {"video/x-av1", "application/x-cenc", FALSE}, {"video/mpegts", "application/x-aes128-cbc", FALSE}, {"audio/x-vorbis", "application/x-webm-enc", FALSE}, }; enum { PROP_0, PROP_LICENSE_URL, PROP_PROXY, PROP_KEY_TYPE, PROP_EXTERNAL_ALLOCATOR, PROP_NEED_PARSER }; enum { SIGNAL_NEED_ALLOCATOR, SIGNAL_CUSTOM_EVENT, LAST_SIGNAL }; GST_DEBUG_CATEGORY_STATIC (gst_wlcdmi_debug); #define GST_CAT_DEFAULT gst_wlcdmi_debug static GstStaticPadTemplate srctemplate = GST_STATIC_PAD_TEMPLATE ("src", GST_PAD_SRC, GST_PAD_ALWAYS, GST_STATIC_CAPS ("audio/mpeg; " "audio/x-ac3; " "audio/x-vorbis; " "video/x-h264; " "video/x-h265; " "video/x-vp9; " "video/x-av1; " "video/mpegts; " "video/x-h264(" GST_CAPS_FEATURE_MEMORY_SECMEM_MEMORY "); " "video/x-h265(" GST_CAPS_FEATURE_MEMORY_SECMEM_MEMORY "); " "video/x-vp9(" GST_CAPS_FEATURE_MEMORY_SECMEM_MEMORY "); " "video/x-av1(" GST_CAPS_FEATURE_MEMORY_SECMEM_MEMORY "); " "video/x-vp9(" GST_CAPS_FEATURE_MEMORY_DMABUF "); " "video/x-av1(" GST_CAPS_FEATURE_MEMORY_DMABUF "); " "video/mpegts(" GST_CAPS_FEATURE_MEMORY_SECMEM_MEMORY "); " )); #define gst_wlcdmi_parent_class parent_class G_DEFINE_TYPE(GstWLCdmi, gst_wlcdmi, GST_TYPE_BASE_TRANSFORM); static wlcdmi_callback_t gst_wlcdmi_callback; static GstCaps * gst_wlcdmi_supported_caps(); static void gst_wlcdmi_set_property (GObject * object, guint prop_id, const GValue * value, GParamSpec * pspec); static void gst_wlcdmi_get_property (GObject * object, guint prop_id, GValue * value, GParamSpec * pspec); static void gst_wlcdmi_finalize(GObject *object); static gboolean gst_wlcdmi_start(GstBaseTransform *trans); static gboolean gst_wlcdmi_stop(GstBaseTransform *trans); static GstCaps* gst_wlcdmi_transform_caps(GstBaseTransform *trans, GstPadDirection direction, GstCaps *caps, GstCaps *filter); static gboolean gst_wlcdmi_set_caps(GstBaseTransform *trans, GstCaps *incaps, GstCaps *outcaps); static GstFlowReturn gst_wlcdmi_transform(GstBaseTransform *trans, GstBuffer *inbuf, GstBuffer *outbuf); static GstFlowReturn gst_wlcdmi_prepare_output_buffer(GstBaseTransform * trans, GstBuffer *input, GstBuffer **outbuf); static gboolean gst_wlcdmi_sink_event(GstBaseTransform *trans, GstEvent *event); static guint gst_wlcdmi_signals[LAST_SIGNAL] = { 0 }; static void gst_wlcdmi_class_init (GstWLCdmiClass * klass) { GObjectClass *gobject_class = (GObjectClass *) klass; GstElementClass *element_class = (GstElementClass *) klass; GstBaseTransformClass *base_class = GST_BASE_TRANSFORM_CLASS(klass); gobject_class->set_property = GST_DEBUG_FUNCPTR(gst_wlcdmi_set_property); gobject_class->get_property = GST_DEBUG_FUNCPTR(gst_wlcdmi_get_property); gobject_class->finalize = GST_DEBUG_FUNCPTR(gst_wlcdmi_finalize); base_class->start = GST_DEBUG_FUNCPTR(gst_wlcdmi_start); base_class->stop = GST_DEBUG_FUNCPTR(gst_wlcdmi_stop); base_class->transform_caps = GST_DEBUG_FUNCPTR(gst_wlcdmi_transform_caps); base_class->set_caps = GST_DEBUG_FUNCPTR(gst_wlcdmi_set_caps); base_class->transform = GST_DEBUG_FUNCPTR(gst_wlcdmi_transform); base_class->prepare_output_buffer = GST_DEBUG_FUNCPTR(gst_wlcdmi_prepare_output_buffer); base_class->sink_event = GST_DEBUG_FUNCPTR(gst_wlcdmi_sink_event); base_class->passthrough_on_same_caps = FALSE; base_class->transform_ip_on_passthrough = FALSE; g_object_class_install_property(gobject_class, PROP_LICENSE_URL, g_param_spec_string("license-url", "License url", "Set license url", NULL, G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property(gobject_class, PROP_PROXY, g_param_spec_string("proxy", "Proxy", "Set proxy", NULL, G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property(gobject_class, PROP_KEY_TYPE, g_param_spec_uint("key-type", "Key type", "Set key type", 0, 4, 0, G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property(gobject_class, PROP_EXTERNAL_ALLOCATOR, g_param_spec_boolean("external-allocator", "External allocator", "Use external allocator", FALSE, G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property(gobject_class, PROP_NEED_PARSER, g_param_spec_boolean("need-parser", "Need parser to add head", "Whether need to link secure parse for vp9 and av1", FALSE, G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS)); #ifdef SECMEM gst_wlcdmi_signals[SIGNAL_NEED_ALLOCATOR] = g_signal_new ("need-allocator", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET (GstWLCdmiClass, need_allocator), NULL, NULL, g_cclosure_marshal_generic, GST_TYPE_SECMEM_ALLOCATOR, 2, G_TYPE_BOOLEAN, G_TYPE_UCHAR); #endif gst_wlcdmi_signals[SIGNAL_CUSTOM_EVENT] = g_signal_new ("custom-event", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET (GstWLCdmiClass, custom_event), NULL, NULL, g_cclosure_marshal_generic, G_TYPE_UINT, 3, G_TYPE_UINT, G_TYPE_POINTER, G_TYPE_UINT); gst_element_class_set_static_metadata(element_class, "Decrypt content encrypted using DRM Encryption", GST_ELEMENT_FACTORY_KLASS_DECRYPTOR, "Decrypts media that has been encrypted using DRM.", "Tao Guo "); gst_element_class_add_pad_template (element_class, gst_static_pad_template_get (&srctemplate)); gst_element_class_add_pad_template (element_class, gst_pad_template_new ("sink", GST_PAD_SINK, GST_PAD_ALWAYS, gst_wlcdmi_supported_caps ())); } static void gst_wlcdmi_init(GstWLCdmi * self) { self->session = NULL; self->license_url = NULL; self->proxy = NULL; self->key_type = 0; self->secure = FALSE; self->external_allocator = FALSE; self->allocator = NULL; self->key_ready = FALSE; self->key_error = FALSE; self->audio_valid_type = 0; self->scheme = WLCDMI_SCHEME_DEFAULT; self->is_vmx = FALSE; self->is_playready = FALSE; self->need_parser = FALSE; memset(self->last_iv, 0, sizeof(self->last_iv)); g_mutex_init(&self->mutex); g_cond_init(&self->condition); g_datalist_init (&self->cryptos); } void gst_wlcdmi_set_property (GObject * object, guint prop_id, const GValue * value, GParamSpec * pspec) { GstWLCdmi *self = GST_WLCDMI(object); GST_DEBUG_OBJECT(self, "%s", __func__); switch (prop_id) { case PROP_LICENSE_URL: GST_OBJECT_LOCK(self); self->license_url = g_value_dup_string(value); GST_INFO("self->license_url %s", self->license_url); GST_OBJECT_UNLOCK(self); break; case PROP_PROXY: GST_OBJECT_LOCK(self); self->proxy = g_value_dup_string(value); GST_INFO("self->proxy %s", self->proxy); GST_OBJECT_UNLOCK(self); break; case PROP_KEY_TYPE: GST_OBJECT_LOCK(self); self->key_type = g_value_get_uint(value); GST_INFO("self->key_type %d", self->key_type); GST_OBJECT_UNLOCK(self); break; case PROP_EXTERNAL_ALLOCATOR: GST_OBJECT_LOCK(self); self->external_allocator = g_value_get_boolean(value); GST_INFO("self->external_allocator %d", self->external_allocator); GST_OBJECT_UNLOCK(self); break; case PROP_NEED_PARSER: GST_OBJECT_LOCK(self); self->need_parser = g_value_get_boolean(value); GST_INFO("self->need_parser %d", self->need_parser); GST_OBJECT_UNLOCK(self); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } void gst_wlcdmi_get_property (GObject * object, guint prop_id, GValue * value, GParamSpec * pspec) { GstWLCdmi *self = GST_WLCDMI(object); GST_DEBUG_OBJECT(self, "%s", __func__); switch (prop_id) { case PROP_LICENSE_URL: GST_OBJECT_LOCK(self); g_value_set_string(value, self->license_url); GST_OBJECT_UNLOCK(self); break; case PROP_PROXY: GST_OBJECT_LOCK(self); g_value_set_string(value, self->proxy); GST_OBJECT_UNLOCK(self); break; case PROP_KEY_TYPE: GST_OBJECT_LOCK(self); g_value_set_uint(value, self->key_type); GST_OBJECT_UNLOCK(self); break; case PROP_EXTERNAL_ALLOCATOR: GST_OBJECT_LOCK(self); g_value_set_boolean(value, self->external_allocator); GST_OBJECT_UNLOCK(self); break; case PROP_NEED_PARSER: GST_OBJECT_LOCK(self); g_value_set_boolean(value, self->need_parser); GST_OBJECT_UNLOCK(self); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } void gst_wlcdmi_finalize(GObject *object) { GstWLCdmi *self = GST_WLCDMI(object); GST_DEBUG_OBJECT(self, "%s", __func__); g_free(self->license_url); self->license_url = NULL; g_free(self->proxy); self->proxy = NULL; } gboolean gst_wlcdmi_start(GstBaseTransform *trans) { GstWLCdmi *self = GST_WLCDMI(trans); GST_DEBUG_OBJECT(self, "%s", __func__); return TRUE; } gboolean gst_wlcdmi_stop(GstBaseTransform *trans) { GstWLCdmi *self = GST_WLCDMI(trans); GST_DEBUG_OBJECT(self, "%s", __func__); g_datalist_clear(&self->cryptos); if (self->session) wlcdmi_session_close(self->session); if (self->allocator && !self->external_allocator) gst_object_unref(self->allocator); g_cond_clear(&self->condition); g_mutex_clear(&self->mutex); return TRUE; } static void gst_wlcdmi_crypto_destroy(gpointer data) { WLCdmiCrypto *crypto = (WLCdmiCrypto *)data; GST_INFO("destroy crypto"); wlcdmi_crypto_close(crypto); } static void gst_wlcdmi_caps_add_unique_structure(GstCaps *caps, GstStructure *in) { guint size = gst_caps_get_size(caps); gboolean dup = FALSE; for (guint i = 0; i < size; i++) { GstStructure *structure = gst_caps_get_structure(caps, i); if (gst_structure_is_equal(structure, in)) { dup = TRUE; break; } } if (dup) { gst_structure_free(in); } else { gst_caps_append_structure(caps, in); } } static void gst_wlcdmi_caps_add_protect_system(GstCaps *caps, GstStructure *in) { for (int i = 0; i < ARRAY_SIZE(scheme_systems); i++) { if (strstr(scheme_systems[i].scheme, gst_structure_get_name(in))) { GstStructure *tmp = gst_structure_copy(in); if (scheme_systems[i].system) { gst_structure_set (tmp, "protection-system", G_TYPE_STRING, scheme_systems[i].system, NULL); } gst_wlcdmi_caps_add_unique_structure(caps, tmp); } } gst_structure_free(in); } static void gst_wlcdmi_caps_add_format(GstCaps *caps, GstStructure *in) { for (int i = 0; i < ARRAY_SIZE(scheme_formats); i++) { if (!strcmp(scheme_formats[i].format, gst_structure_get_name (in))) { GstStructure *tmp = gst_structure_copy(in); gst_structure_set (tmp, "original-media-type", G_TYPE_STRING, scheme_formats[i].format, NULL); if (scheme_formats[i].au_byte_stream) { gst_structure_set (tmp, "stream-format", G_TYPE_STRING, "byte-stream", "alignment", G_TYPE_STRING, "au", "parsed", G_TYPE_BOOLEAN, TRUE, NULL); } gst_structure_set_name(tmp, scheme_formats[i].scheme); gst_wlcdmi_caps_add_protect_system(caps, tmp); } } gst_structure_free(in); } static GstCaps * gst_wlcdmi_supported_caps() { GstCaps* caps = gst_caps_new_empty(); for (int i = 0; i < ARRAY_SIZE(scheme_formats); i++) { GstStructure *tmp = gst_structure_new(scheme_formats[i].scheme, "original-media-type", G_TYPE_STRING, scheme_formats[i].format, NULL); if (scheme_formats[i].au_byte_stream) { gst_structure_set (tmp, "stream-format", G_TYPE_STRING, "byte-stream", "alignment", G_TYPE_STRING, "au", "parsed", G_TYPE_BOOLEAN, TRUE, NULL); } gst_wlcdmi_caps_add_protect_system(caps, tmp); } return caps; } GstCaps* gst_wlcdmi_transform_caps(GstBaseTransform *trans, GstPadDirection direction, GstCaps *caps, GstCaps *filter) { GstWLCdmi *self = GST_WLCDMI(trans); #if 1 GstCaps *srccaps = NULL, *sinkcaps = NULL; srccaps = gst_pad_get_pad_template_caps(GST_BASE_TRANSFORM_SRC_PAD(trans)); sinkcaps = gst_pad_get_pad_template_caps(GST_BASE_TRANSFORM_SINK_PAD(trans)); #endif GstCaps* transformed_caps = gst_caps_new_empty(); GST_DEBUG_OBJECT (self, "transform_caps direction:%d caps:%" GST_PTR_FORMAT " filter: %" GST_PTR_FORMAT, direction, caps, filter); guint size = gst_caps_get_size(caps); for (int i = 0; i < size; i++) { GstStructure *in = gst_caps_get_structure(caps, i); GstStructure *out = NULL; if (direction == GST_PAD_SINK) { if (!gst_structure_has_field(in, "original-media-type")) continue; out = gst_structure_copy(in); gst_structure_set_name(out, gst_structure_get_string(out, "original-media-type")); gst_structure_remove_fields(out, "original-media-type", "protection-system", "encryption-algorithm", "cipher-mode", "encoding-scope", NULL); gst_wlcdmi_caps_add_unique_structure(transformed_caps, out); } else if (direction == GST_PAD_SRC) { out = gst_structure_copy(in); gst_structure_remove_fields(out, "base-profile", "profile", "codec_data", "rate", "height", "width", "framerate", "level", "pixel-aspect-ratio", "mpegversion", "channels", "framed", "stream-format", NULL); gst_wlcdmi_caps_add_format(transformed_caps, out); } } #ifdef SECMEM if (direction == GST_PAD_SINK) { guint nb_caps = gst_caps_get_size(transformed_caps); for (guint i = 0; i < nb_caps; ++i) { GstStructure *st = gst_caps_get_structure(transformed_caps, i); const gchar *name = gst_structure_get_name(st); if (g_str_has_prefix (name, "video/")) { if ((g_str_has_prefix(name, "video/x-vp9") || g_str_has_prefix(name, "video/x-av1")) && !self->need_parser) { gst_caps_set_features(transformed_caps, i, gst_caps_features_from_string(GST_CAPS_FEATURE_MEMORY_DMABUF)); } else { gst_caps_set_features(transformed_caps, i, gst_caps_features_from_string(GST_CAPS_FEATURE_MEMORY_SECMEM_MEMORY)); } } } } #endif #if 1 if (gst_caps_is_empty(caps)) { if (direction == GST_PAD_SINK) gst_caps_replace(&transformed_caps, gst_caps_copy(srccaps)); else if (direction == GST_PAD_SRC) gst_caps_replace(&transformed_caps, gst_caps_copy(sinkcaps)); } #endif if (filter) { GstCaps *intersection = NULL; GST_LOG_OBJECT(self, "Using filter caps %" GST_PTR_FORMAT, filter); intersection = gst_caps_intersect_full(transformed_caps, filter, GST_CAPS_INTERSECT_FIRST); gst_caps_replace(&transformed_caps, intersection); } GST_DEBUG_OBJECT (self, "transformed_caps direction:%d return:%" GST_PTR_FORMAT, direction, transformed_caps); return transformed_caps; } gboolean gst_wlcdmi_set_caps(GstBaseTransform *trans, GstCaps *incaps, GstCaps *outcaps) { GstWLCdmi *self = GST_WLCDMI(trans); GST_INFO_OBJECT (self, "set_caps incaps:%" GST_PTR_FORMAT " outcaps:%" GST_PTR_FORMAT, incaps, outcaps); guint size = gst_caps_get_size(incaps); for (int i = 0; i < size; i++) { GstStructure *in_structure = gst_caps_get_structure(incaps, i); if (!gst_structure_has_field(in_structure, "original-media-type")) continue; const gchar* media_type = gst_structure_get_string(in_structure, "original-media-type"); if (!gst_structure_has_field(in_structure, "cipher-mode")) { GST_WARNING_OBJECT (self, "no cipher-mode"); } else { gchar* pbCipherMode = (gchar *)gst_structure_get_string(in_structure, "cipher-mode"); if (pbCipherMode) { if (strcasecmp(pbCipherMode, "cenc") == 0) self->scheme = WLCDMI_SCHEME_CENC; else if (strcasecmp(pbCipherMode, "cens") == 0) self->scheme = WLCDMI_SCHEME_CENS; else if (strcasecmp(pbCipherMode, "cbc1") == 0) self->scheme = WLCDMI_SCHEME_CBC1; else if (strcasecmp(pbCipherMode, "cbcs") == 0) self->scheme = WLCDMI_SCHEME_CBCS; else self->scheme = WLCDMI_SCHEME_DEFAULT; } } GST_DEBUG_OBJECT(self, "scheme %d", self->scheme); #ifdef SECMEM if (media_type && g_str_has_prefix (media_type, "audio/")) { if (g_str_has_prefix (media_type, "audio/mpeg")) { self->audio_valid_type = AUD_VALID_TYPE_AAC_ADTS; } else if (g_str_has_prefix (media_type, "audio/x-ac3")) { self->audio_valid_type = AUD_VALID_TYPE_AC3; } GST_DEBUG_OBJECT (self, "audio_valid_type:%d ", self->audio_valid_type); } uint8_t format = SECMEM_CODEC_DEFAULT; if (media_type && g_str_has_prefix (media_type, "video/")) { self->secure = TRUE; if (g_str_has_prefix (media_type, "video/x-vp9")) { format = SECMEM_DECODER_VP9; } else if (g_str_has_prefix (media_type, "video/x-av1")) { format = SECMEM_DECODER_AV1; } else if (g_str_has_prefix (media_type, "video/mpegts")) { format = SECMEM_DECODER_MPEGTS; } } if (self->secure && !self->allocator) { gboolean is_4k = FALSE; int width = 0, height = 0; gst_structure_get_int(in_structure, "width", &width); gst_structure_get_int(in_structure, "height", &height); if ((width * height) > (1920 * 1088)) is_4k = TRUE; GST_DEBUG_OBJECT(self, "allocator %d", self->scheme); if (self->external_allocator) { g_signal_emit (self, gst_wlcdmi_signals[SIGNAL_NEED_ALLOCATOR], 0, is_4k, format, &self->allocator); } else { self->allocator = gst_secmem_allocator_new(is_4k, format); } } #endif } return TRUE; } GstFlowReturn gst_wlcdmi_transform(GstBaseTransform *trans, GstBuffer *inbuf, GstBuffer *outbuf) { GstFlowReturn result = GST_FLOW_OK; GstWLCdmi *self = GST_WLCDMI(trans); GstProtectionMeta *meta = NULL; if (!inbuf) { GST_DEBUG_OBJECT(self, "Failed to get writable buffer"); return GST_FLOW_ERROR; } meta = gst_buffer_get_protection_meta(inbuf); if (meta) { guint iv_size = 0; gboolean encrypted = FALSE; guint subsample_count = 0; const GValue *value = NULL; GstBuffer *kid = NULL; GstBuffer *iv_buffer = NULL; GstBuffer *subsamples = NULL; GstMemory *mem = NULL; int mem_fd = -1; if (self->allocator) { mem = gst_buffer_peek_memory(outbuf, 0); mem_fd = gst_dmabuf_memory_get_fd(mem); if (mem_fd < 0) { GST_ERROR_OBJECT(self, "got invalid fd %d mem %p", mem_fd, mem); result = GST_FLOW_NOT_SUPPORTED; goto beach; } } // GST_INFO_OBJECT(self, gst_structure_to_string(meta->info)); if (!gst_structure_get_uint(meta->info, "iv_size", &iv_size)) { GST_ERROR_OBJECT(self, "failed to get iv_size"); result = GST_FLOW_NOT_SUPPORTED; goto beach; } if (!gst_structure_get_boolean(meta->info, "encrypted", &encrypted)) { GST_ERROR_OBJECT(self, "failed to get encrypted flag"); result = GST_FLOW_NOT_SUPPORTED; goto beach; } if (!encrypted || (iv_size != 8 && iv_size != 16)) { GST_ERROR_OBJECT(self, "incorrect encrypted flag %d invalid iv_size %d ", encrypted, iv_size); goto beach; } if (!self->key_ready) { gint64 end_time = -1; g_mutex_lock(&self->mutex); end_time = g_get_monotonic_time() + 10 * G_TIME_SPAN_SECOND; if (!g_cond_wait_until(&self->condition, &self->mutex, end_time)) { g_mutex_unlock(&self->mutex); GST_ERROR_OBJECT(self, "failed to get key ready status"); result = GST_FLOW_ERROR; goto beach; } g_mutex_unlock(&self->mutex); } if (self->key_error) { GST_ERROR_OBJECT(self, "decrypt failed: key error"); result = GST_FLOW_ERROR; goto beach; } value = gst_structure_get_value(meta->info, "kid"); if (!value) { GST_ERROR_OBJECT(self, "failed to get kid for sample"); result = GST_FLOW_NOT_SUPPORTED; goto beach; } kid = gst_value_get_buffer(value); value = gst_structure_get_value(meta->info, "iv"); if (!value) { GST_ERROR_OBJECT(self, "failed to get IV for sample"); result = GST_FLOW_NOT_SUPPORTED; goto beach; } iv_buffer = gst_value_get_buffer(value); if (!gst_structure_get_uint(meta->info, "subsample_count", &subsample_count)) { subsample_count = 0; } if (subsample_count > 0 ) { value = gst_structure_get_value(meta->info, "subsamples"); if (value) { subsamples = gst_value_get_buffer(value); } else { GST_INFO_OBJECT(self, "no value"); } } else { GST_DEBUG_OBJECT(self, "single subsample"); subsample_count = 1; } GstMapInfo map_data = GST_MAP_INFO_INIT; uint32_t *subsample_array = NULL; do { GstMapInfo map_iv = GST_MAP_INFO_INIT; GstMapInfo map_kid = GST_MAP_INFO_INIT; gst_buffer_map(inbuf, &map_data, GST_MAP_READWRITE); subsample_array = malloc(subsample_count * sizeof(uint32_t) * 2); if (!subsample_array) { GST_ERROR_OBJECT(self, "alloc subsample array failed"); break; } if (subsamples) { GstMapInfo map_subsample = GST_MAP_INFO_INIT; guint16 clear = 0; guint32 enc = 0; gst_buffer_map(subsamples, &map_subsample, GST_MAP_READ); GstByteReader* reader = gst_byte_reader_new(map_subsample.data, map_subsample.size); for (guint i = 0; i < subsample_count; i++) { gst_byte_reader_get_uint16_be(reader, &clear); gst_byte_reader_get_uint32_be(reader, &enc); GST_DEBUG_OBJECT(self, "subsample %d %d", clear, enc); subsample_array[i * 2] = clear; subsample_array[i * 2 + 1] = enc; } gst_buffer_unmap(subsamples, &map_subsample); } else { GST_DEBUG_OBJECT(self, "single subsample"); subsample_array[0] = 0; subsample_array[1] = map_data.size; } uint32_t pattern_array[2] = {0}; if (!gst_structure_get_uint(meta->info, "crypt_byte_block", &pattern_array[0])) { GST_DEBUG_OBJECT(self, "use default crypt block 0"); } if (!gst_structure_get_uint(meta->info, "skip_byte_block", &pattern_array[1])) { GST_DEBUG_OBJECT(self, "use default skip block 0"); } GST_DEBUG_OBJECT(self, "pattern %d %d", pattern_array[0], pattern_array[1]); do { gst_buffer_map(kid, &map_kid, GST_MAP_READ); gchar *key = g_base64_encode(map_kid.data, map_kid.size); GQuark quark = g_quark_from_string(key); WLCdmiCrypto *crypto = g_datalist_id_get_data(&self->cryptos, quark); guint64 offset = GST_BUFFER_OFFSET(inbuf); gst_buffer_map(iv_buffer, &map_iv, GST_MAP_READ); GST_DEBUG_OBJECT(self, "buffer offset %" PRId64 ", ivsize=%zd, datasize=%zd, data %p.", offset, map_iv.size, map_data.size, map_data.data); GstCaps *sinkcaps = gst_pad_get_current_caps (trans->sinkpad); GstStructure *structure = gst_caps_get_structure (sinkcaps, 0); if (self->scheme == WLCDMI_SCHEME_CBC1 && gst_structure_has_name (structure, "application/x-aes128-cbc")) { if (offset == 0) memcpy(self->last_iv, map_iv.data, map_iv.size); } else { memcpy(self->last_iv, map_iv.data, map_iv.size); } gchar tmp_iv[IV_SIZE]; if (self->scheme == WLCDMI_SCHEME_CBC1 && gst_structure_has_name (structure, "application/x-aes128-cbc")) { memcpy(tmp_iv, map_data.data + map_data.size - map_iv.size, map_iv.size); } if (crypto) { wlcdmi_crypto_decrypt_pattern(crypto, map_iv.size, (uint8_t *)self->last_iv, subsample_count, subsample_array, pattern_array, map_data.size, (uint8_t *)map_data.data, mem_fd); } else { GST_WARNING_OBJECT(self, "crypto not exist in datalist, create new now"); crypto = wlcdmi_session_open_crypto(self->session, map_kid.data, map_kid.size, self->secure); g_datalist_id_set_data_full(&self->cryptos, quark, crypto, gst_wlcdmi_crypto_destroy); wlcdmi_crypto_decrypt_pattern(crypto, map_iv.size, (uint8_t *)self->last_iv, subsample_count, subsample_array, pattern_array, map_data.size, (uint8_t *)map_data.data, mem_fd); } if (self->scheme == WLCDMI_SCHEME_CBC1 && gst_structure_has_name (structure, "application/x-aes128-cbc")) { memcpy(self->last_iv, tmp_iv, IV_SIZE); } gst_buffer_unmap(iv_buffer, &map_iv); g_free(key); gst_buffer_unmap(kid, &map_kid); } while(0); } while (0); if (subsample_array) free(subsample_array); gst_buffer_unmap(inbuf, &map_data); } else { GST_LOG_OBJECT(self, "no meta"); #ifdef SECMEM if (self->allocator) gst_buffer_copy_to_secmem(outbuf, inbuf); #endif } beach: return result; } GstFlowReturn gst_wlcdmi_prepare_output_buffer(GstBaseTransform * trans, GstBuffer *input, GstBuffer **outbuf) { GstWLCdmi *self = GST_WLCDMI(trans); if (self->allocator) { *outbuf = gst_buffer_new_allocate(self->allocator, gst_buffer_get_size(input), NULL); g_return_val_if_fail(*outbuf != NULL, GST_FLOW_ERROR); GST_DEBUG_OBJECT(self, "alloc from %p size:%" G_GSIZE_FORMAT " -> %" G_GSIZE_FORMAT "", self->allocator, gst_buffer_get_size(input), gst_buffer_get_size(*outbuf)); GST_BASE_TRANSFORM_CLASS(parent_class)->copy_metadata (trans, input, *outbuf); } else if (self->secure) { GST_ERROR_OBJECT(self, "no secure allocator"); return GST_FLOW_ERROR; } else { *outbuf = input; } return GST_FLOW_OK; } void gst_wlcdmi_set_parameter(GstBaseTransform * trans) { GstWLCdmi *self = GST_WLCDMI(trans); if (self->session) { GST_INFO_OBJECT(self, "set parameter audio_valid_type=%d,license_url=%s ", self->audio_valid_type, self->license_url); if (self->audio_valid_type) wlcdmi_session_set_parameter(self->session, WLCDMI_PARAM_AUDIO_CODEC_TYPE, (uint8_t *)&self->audio_valid_type, sizeof(guint)); if (self->license_url) { if (self->is_vmx) { wlcdmi_session_set_parameter(self->session, WLCDMI_PARAM_VMX_SERVER_ADDRESS, (uint8_t *)self->license_url, strlen(self->license_url) + 1); } } } } gboolean gst_wlcdmi_sink_event(GstBaseTransform *trans, GstEvent *event) { gboolean result = FALSE; GstWLCdmi *self = GST_WLCDMI(trans); const gchar *system_id = NULL; GstBuffer *data = NULL; const gchar *origin = NULL; switch (GST_EVENT_TYPE(event)) { case GST_EVENT_PROTECTION: gst_event_parse_protection (event, &system_id, &data, &origin); GST_INFO_OBJECT(self, "received protection event: system id=%s, origin=%s", system_id, origin); gchar *system_id_lower = NULL; if (system_id) { system_id_lower = g_ascii_strdown(system_id, -1); system_id = system_id_lower; } // gst_util_dump_buffer(data); if (!self->session) { GstMapInfo map = GST_MAP_INFO_INIT; gst_buffer_map(data, &map, GST_MAP_READ); if (system_id != NULL) { if (!strcmp(system_id, VERIMATRIX_SYSTEM_ID)) { self->is_vmx = TRUE; } else if (!strcmp(system_id, PLAYREADY_SYSTEM_ID) || !strcmp(system_id, PLAYREADY_OLD_SYSTEM_ID)) { self->is_playready = TRUE; system_id = "playready"; } } if (!strcmp(origin, "dash/mpd")) { const char *init_data_start = NULL, *init_data_end = NULL; init_data_start = strstr((const char *)map.data, ""); init_data_end = strstr((const char *)map.data, ""); if (init_data_start && init_data_end) { gsize init_data_length = 0; char *init_data = g_strndup(init_data_start + 11, init_data_end - init_data_start - 11); if (init_data) { g_base64_decode_inplace(init_data, &init_data_length); self->session = wlcdmi_open_session(system_id, self->key_type, self->scheme, (uint8_t *)init_data, init_data_length, NULL, 0, &gst_wlcdmi_callback, self); gst_wlcdmi_set_parameter(trans); wlcdmi_session_run(self->session); g_free(init_data); } } else { init_data_start = strstr((const char *)map.data, ""); init_data_end = strstr((const char *)map.data, ""); if (init_data_start && init_data_end) { gsize init_data_length = 0; char *init_data = g_strndup(init_data_start + 10, init_data_end - init_data_start - 10); if (init_data) { g_base64_decode_inplace(init_data, &init_data_length); self->session = wlcdmi_open_session(system_id, self->key_type, self->scheme, (uint8_t *)init_data, init_data_length, NULL, 0, &gst_wlcdmi_callback, self); gst_wlcdmi_set_parameter(trans); wlcdmi_session_run(self->session); g_free(init_data); } } } } else if (!strcmp(origin, "isobmff/moov")) { self->session = wlcdmi_open_session(system_id, self->key_type, self->scheme, (uint8_t *)map.data, map.size, NULL, 0, &gst_wlcdmi_callback, self); gst_wlcdmi_set_parameter(trans); wlcdmi_session_run(self->session); } else if (!strcmp(origin, "hls/sample-aes")) { const char *init_data_start = NULL, *init_data_end = NULL; init_data_start = strstr((const char *)map.data, "data:text/plain;base64,"); if (init_data_start) init_data_end = strchr(init_data_start, '"'); if (init_data_start && init_data_end) { gsize init_data_length = 0; char *init_data = g_strndup(init_data_start + 23, init_data_end - init_data_start - 23); if (init_data) { g_base64_decode_inplace(init_data, &init_data_length); self->session = wlcdmi_open_session(system_id, self->key_type, self->scheme, (uint8_t *)init_data, init_data_length, NULL, 0, &gst_wlcdmi_callback, self); gst_wlcdmi_set_parameter(trans); wlcdmi_session_run(self->session); g_free(init_data); } } else { GST_WARNING_OBJECT(self, "sampleAES:null protection system id: use default vmx system"); self->is_vmx = TRUE; self->session = wlcdmi_open_session("verimatrix", self->key_type, self->scheme, (uint8_t *)map.data, map.size, NULL, 0, &gst_wlcdmi_callback, self); gst_wlcdmi_set_parameter(trans); wlcdmi_session_run(self->session); } } else if (strcmp(origin, "hls/aes-128") == 0) { GST_INFO_OBJECT(self, "aes128:null protection system id: use default vmx system"); self->is_vmx = TRUE; self->session = wlcdmi_open_session("verimatrix", self->key_type, self->scheme, (uint8_t *)map.data, map.size, NULL, 0, &gst_wlcdmi_callback, self); gst_wlcdmi_set_parameter(trans); wlcdmi_session_run(self->session); } else if (!strcmp(origin, "matroskademux")) { GST_WARNING_OBJECT(self, "widevine matroskademux"); self->session = wlcdmi_open_session("widevine", self->key_type, self->scheme, (uint8_t *)map.data, map.size, NULL, 0, &gst_wlcdmi_callback, self); gst_wlcdmi_set_parameter(trans); wlcdmi_session_run(self->session); } gst_buffer_unmap(data, &map); } gst_event_unref(event); if (system_id_lower) { g_free(system_id_lower); } result = TRUE; break; case GST_EVENT_CUSTOM_DOWNSTREAM_OOB: default: result = GST_BASE_TRANSFORM_CLASS(parent_class)->sink_event(trans, event); break; } return result; } static size_t gst_wlcdmi_curl_data_callback(void *contents, size_t size, size_t nmemb, GstBuffer *response) { size_t newLength = size * nmemb; GstBuffer *chunk = gst_buffer_new_and_alloc(newLength); if (chunk) { gst_buffer_fill(chunk, 0, contents, newLength); gst_buffer_append(response, chunk); } return newLength; } static gpointer gst_wlcdmi_key_message_async_task(gpointer data) { CURL *curl = curl_easy_init(); CURLcode code = CURLE_OK; GstMapInfo map = GST_MAP_INFO_INIT; WLCdmiSession *session = NULL; GstBuffer *request = NULL; GstStructure *structure = (GstStructure *)data; GstBuffer *response = gst_buffer_new(); struct curl_slist *headers = NULL; const gchar *url = gst_structure_get_string(structure, "license-url"); const gchar *proxy = gst_structure_get_string(structure, "proxy"); const GValue *value = gst_structure_get_value(structure, "request"); gboolean playready; if (!value) { GST_ERROR("no request"); goto beach; } request = gst_value_get_buffer(value); value = gst_structure_get_value(structure, "session"); if (!value) { GST_ERROR("no session"); goto beach; } session = g_value_get_object(value); if (!curl) { GST_ERROR("curl is none"); goto beach; } gst_buffer_map(request, &map, GST_MAP_READ); if (gst_structure_get_boolean(structure, "playready", &playready) && playready) { headers = curl_slist_append(headers, "Content-Type: text/xml"); headers = curl_slist_append(headers, "SOAPAction: http://schemas.microsoft.com/DRM/2007/03/protocols/AcquireLicense"); } else { headers = curl_slist_append(headers, "Content-Type: application/octet-stream"); } curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, map.data); curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, map.size); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, gst_wlcdmi_curl_data_callback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, response); // timeout curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L); curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 5L); if (proxy && strlen(proxy)) curl_easy_setopt(curl, CURLOPT_PROXY, proxy); GST_INFO("send key request url %s\n", url); if (proxy) GST_INFO("send key request proxy %s\n", proxy); GST_INFO("send key request map.size %d map.data %s\n", map.size, g_base64_encode(map.data, map.size)); code = curl_easy_perform(curl); gst_buffer_unmap(request, &map); if (code != CURLE_OK) { if (code == CURLE_OPERATION_TIMEDOUT) { GST_ERROR("curl timeout when requesting \"%s\"", url); } else if (code == CURLE_COULDNT_CONNECT) { GST_ERROR("curl connect timeout \"%s\"", url); } else { GST_ERROR("curl failed \"%s\", code: %d, msg: %s", url, code, curl_easy_strerror(code)); } goto beach; } if (gst_buffer_get_size(response) == 0) { GST_ERROR("error response"); goto beach; } gst_buffer_map(response, &map, GST_MAP_READ); GST_INFO("process key response map.size %d \t %s\n", map.size, g_base64_encode(map.data, map.size)); wlcdmi_session_update(session, map.data, map.size); gst_buffer_unmap(response, &map); beach: gst_structure_free(structure); if (curl) curl_easy_cleanup(curl); if (headers) curl_slist_free_all(headers); if (session) g_object_unref(session); if (request) gst_buffer_unref(request); gst_buffer_unref(response); g_thread_exit(NULL); return NULL; } #if (GST_VERSION_MAJOR <= 1 && GST_VERSION_MINOR < 20) GstBuffer * gst_buffer_new_memdup (gconstpointer data, gsize size) { gpointer data2 = g_memdup (data, size); return gst_buffer_new_wrapped_full (0, data2, size, 0, size, data2, g_free); } #endif void gst_wlcdmi_key_message(WLCdmiSession *session, void* pUserData, const char *pbUrl, const uint8_t *pbChallenge, const uint32_t cbChallenge) { GstWLCdmi *self = GST_WLCDMI(pUserData); const gchar *url = NULL; GST_INFO("handle key request"); if (self->is_vmx) goto beach; if (self->license_url && strlen(self->license_url)) { url = self->license_url; } else if (pbUrl && strlen(pbUrl)) { url = pbUrl; } else { GST_ERROR("no license url"); goto beach; } GstStructure *data = gst_structure_new_empty("key-request"); GstBuffer *request = gst_buffer_new_memdup(pbChallenge, cbChallenge); gst_structure_set(data, "license-url", G_TYPE_STRING, url, "proxy", G_TYPE_STRING, self->proxy, "session", G_TYPE_OBJECT, g_object_ref(session), "request", GST_TYPE_BUFFER, request, "playready", G_TYPE_BOOLEAN, self->is_playready, NULL); g_thread_new("fetch_key", gst_wlcdmi_key_message_async_task, data); beach: return; } void gst_wlcdmi_key_status(WLCdmiSession *session, void* pUserData, const char *pbStatus, const uint8_t *pbKeyId, const uint8_t cbKeyId) { GstWLCdmi *self = GST_WLCDMI(pUserData); gchar *key = g_base64_encode(pbKeyId, cbKeyId); GQuark quark = g_quark_from_string(key); WLCdmiCrypto *crypto = g_datalist_id_get_data(&self->cryptos, quark); if (!strcmp("KeyUsable", pbStatus)) { if (!crypto) { crypto = wlcdmi_session_open_crypto(session, pbKeyId, cbKeyId, self->secure); g_datalist_id_set_data_full(&self->cryptos, quark, crypto, gst_wlcdmi_crypto_destroy); } } else if (!strcmp("KeyReleased", pbStatus)) { if (crypto) { g_datalist_id_remove_data(&self->cryptos, quark); wlcdmi_crypto_close(crypto); } } g_free(key); } void gst_wlcdmi_key_ready(WLCdmiSession *session, void* pUserData) { GstWLCdmi *self = GST_WLCDMI(pUserData); g_mutex_lock(&self->mutex); self->key_ready = TRUE; g_cond_signal(&self->condition); g_mutex_unlock(&self->mutex); GST_INFO_OBJECT(self, "key ready"); } void gst_wlcdmi_key_error(WLCdmiSession *session, void* pUserData, uint32_t dwError, const char *pbMessage) { GstWLCdmi *self = GST_WLCDMI(pUserData); g_mutex_lock(&self->mutex); self->key_error = TRUE; g_cond_signal(&self->condition); g_mutex_unlock(&self->mutex); GST_ERROR_OBJECT(self, "key error %d %s", dwError, pbMessage); } void gst_wlcdmi_custom_event(WLCdmiSession *session, void* pUserData, uint32_t type, const char *pbEvent, const uint32_t cbEvent) { GstWLCdmi *self = GST_WLCDMI(pUserData); GST_INFO("type %d pbEvent %p cbEvent %d \n", type, pbEvent, cbEvent); guint ret; g_signal_emit (self, gst_wlcdmi_signals[SIGNAL_CUSTOM_EVENT], 0, type, pbEvent, cbEvent, &ret); } static wlcdmi_callback_t gst_wlcdmi_callback = { gst_wlcdmi_key_message, gst_wlcdmi_key_status, gst_wlcdmi_key_ready, gst_wlcdmi_key_error, gst_wlcdmi_custom_event }; static gboolean wlcdmi_init (GstPlugin * wlcdmi) { GST_DEBUG_CATEGORY_INIT(gst_wlcdmi_debug, "wlcdmi", 0, GST_PACKAGE_NAME); return gst_element_register(wlcdmi, "wlcdmi", GST_RANK_PRIMARY + 1, GST_TYPE_WLCDMI); } GST_PLUGIN_DEFINE( GST_VERSION_MAJOR, GST_VERSION_MINOR, wlcdmi, GST_PACKAGE_NAME, wlcdmi_init, PACKAGE_VERSION, GST_LICENSE, PACKAGE, GST_PACKAGE_ORIGIN )