blob: 3c4642accc5ed2d549ad1bace42b3dd22292ce33 [file]
//
// Copyright 2020 Comcast Cable Communications Management, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0
#include "third_party/starboard/rdk/shared/drm/gst_decryptor_ocdm.h"
#if defined(HAS_OCDM)
#include "third_party/starboard/rdk/shared/drm/drm_system_ocdm.h"
#include "starboard/common/mutex.h"
#include "starboard/common/condition_variable.h"
#include <gst/gst.h>
#include <gst/base/gstbasetransform.h>
#include <gst/base/gstbytereader.h>
#include <opencdm/open_cdm.h>
namespace third_party {
namespace starboard {
namespace rdk {
namespace shared {
namespace drm {
namespace {
G_BEGIN_DECLS
#define COBALT_OCDM_DECRYPTOR_TYPE (cobalt_ocdm_decryptor_get_type())
#define COBALT_OCDM_DECRYPTOR(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), COBALT_OCDM_DECRYPTOR_TYPE, CobaltOcdmDecryptor))
#define COBALT_OCDM_DECRYPTOR_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), COBALT_OCDM_DECRYPTOR_TYPE, CobaltOcdmDecryptorClass))
typedef struct _CobaltOcdmDecryptor CobaltOcdmDecryptor;
typedef struct _CobaltOcdmDecryptorClass CobaltOcdmDecryptorClass;
typedef struct _CobaltOcdmDecryptorPrivate CobaltOcdmDecryptorPrivate;
GType cobalt_ocdm_decryptor_get_type(void);
struct _CobaltOcdmDecryptor {
GstBaseTransform parent;
CobaltOcdmDecryptorPrivate *priv;
};
struct _CobaltOcdmDecryptorClass {
GstBaseTransformClass parentClass;
};
G_END_DECLS
static GstStaticPadTemplate sinkTemplate =
GST_STATIC_PAD_TEMPLATE(
"sink",
GST_PAD_SINK,
GST_PAD_ALWAYS,
GST_STATIC_CAPS_ANY);
static GstStaticPadTemplate srcTemplate =
GST_STATIC_PAD_TEMPLATE(
"src",
GST_PAD_SRC,
GST_PAD_ALWAYS,
GST_STATIC_CAPS_ANY);
GST_DEBUG_CATEGORY(cobalt_ocdm_decryptor_debug_category);
#define GST_CAT_DEFAULT cobalt_ocdm_decryptor_debug_category
struct _CobaltOcdmDecryptorPrivate : public DrmSystemOcdm::Observer {
_CobaltOcdmDecryptorPrivate() {
#ifndef GST_DISABLE_GST_DEBUG
if (gst_debug_category_get_threshold(GST_CAT_DEFAULT) >= GST_LEVEL_DEBUG) {
decrypt_dur_.reserve( 300 );
}
#endif
}
~_CobaltOcdmDecryptorPrivate() {
if (drm_system_)
drm_system_->RemoveObserver(this);
if (current_key_id_) {
gst_buffer_unref(current_key_id_);
current_key_id_ = nullptr;
}
if (cached_caps_) {
gst_caps_unref(cached_caps_);
cached_caps_ = nullptr;
}
}
// DrmSystemOcdm::Observer
void OnKeyReady(const uint8_t* key, size_t key_len) override {
#ifndef GST_DISABLE_GST_DEBUG
if (gst_debug_category_get_threshold(GST_CAT_DEFAULT) >= GST_LEVEL_DEBUG) {
gchar *md5sum = g_compute_checksum_for_data(G_CHECKSUM_MD5, key, key_len);
GST_DEBUG("key ready: %s", md5sum);
g_free(md5sum);
}
#endif
::starboard::ScopedLock lock(mutex_);
if (awaiting_key_info_)
condition_.Signal();
}
GstFlowReturn Decrypt(
CobaltOcdmDecryptor* self, GstBuffer* buffer,
GstBuffer* subsamples, uint32_t subsample_count,
GstBuffer* iv, GstBuffer* key) {
gint64 start = 0;
#ifndef GST_DISABLE_GST_DEBUG
const GstDebugLevel debug_level = gst_debug_category_get_threshold(GST_CAT_DEFAULT);
if (debug_level >= GST_LEVEL_TRACE) {
gchar *md5sum = 0;
GstMapInfo map_info;
if (gst_buffer_map(key, &map_info, GST_MAP_READ)) {
md5sum = g_compute_checksum_for_data(G_CHECKSUM_MD5, map_info.data, map_info.size);
gst_buffer_unmap(key, &map_info);
}
uint32_t total_clear = 0;
uint32_t total_encrypted = 0;
GstMapInfo sample_map;
if (gst_buffer_map(subsamples, &sample_map, GST_MAP_READ)) {
GstByteReader* reader = gst_byte_reader_new(sample_map.data, sample_map.size);
for (uint32_t i = 0; i < subsample_count; ++i) {
uint16_t clear = 0;
uint32_t encrypted = 0;
gst_byte_reader_get_uint16_be(reader, &clear);
gst_byte_reader_get_uint32_be(reader, &encrypted);
total_clear += clear;
total_encrypted += encrypted;
}
gst_byte_reader_free(reader);
gst_buffer_unmap(subsamples, &sample_map);
}
GST_TRACE_OBJECT(self, "buf=(%" GST_PTR_FORMAT "), "
"subsample_count=%u, subsamples=(%p), clear=%u, encrypted=%u, iv=(%p), key=(%p : %s)",
buffer, subsample_count, subsamples, total_clear, total_encrypted, iv, key, md5sum);
g_free(md5sum);
}
#endif
if ( !drm_system_ ) {
GstContext* context = gst_element_get_context(GST_ELEMENT(self), "cobalt-drm-system");
if (context) {
const GValue* value = gst_structure_get_value(gst_context_get_structure(context), "drm-system-instance");
DrmSystemOcdm* drm_system = reinterpret_cast<DrmSystemOcdm*>(value ? g_value_get_pointer(value) : nullptr);
SetDrmSystem( drm_system );
}
if (!drm_system_) {
GST_ELEMENT_ERROR (self, STREAM, DECRYPT, ("No DRM System instance"), (NULL));
return GST_FLOW_ERROR;
}
}
GstMapInfo map_info;
if (FALSE == gst_buffer_map(key, &map_info, GST_MAP_READ)) {
GST_ELEMENT_ERROR (self, STREAM, DECRYPT, ("Failed to map kid buffer"), (NULL));
return GST_FLOW_NOT_SUPPORTED;
} else {
if (!current_key_id_ || gst_buffer_memcmp(current_key_id_, 0, map_info.data, map_info.size) != 0) {
if (debug_level >= GST_LEVEL_DEBUG) {
gchar *md5sum = g_compute_checksum_for_data(G_CHECKSUM_MD5, map_info.data, map_info.size);
GST_DEBUG_OBJECT(self, "Got buffer protected with key %s", md5sum);
g_free(md5sum);
}
::starboard::ScopedLock lock(mutex_);
current_session_id_.clear();
if (current_key_id_) {
gst_buffer_unref(current_key_id_);
current_key_id_ = nullptr;
}
while(true) {
if (is_flushing_ || is_active_ == false)
break;
current_session_id_ = drm_system_->SessionIdByKeyId(map_info.data, map_info.size);
if (!current_session_id_.empty()) {
current_key_id_ = gst_buffer_copy(key);
break;
}
GST_DEBUG_OBJECT(self, "Session id is empty, waiting");
awaiting_key_info_ = &map_info;
condition_.Wait();
awaiting_key_info_ = nullptr;
}
if (debug_level >= GST_LEVEL_DEBUG) {
gchar *md5sum = g_compute_checksum_for_data(G_CHECKSUM_MD5, (const guchar*)current_session_id_.c_str(), current_session_id_.size());
GST_DEBUG_OBJECT(self, "Using session with id '%s'", md5sum);
g_free(md5sum);
}
}
gst_buffer_unmap(key, &map_info);
}
if ( current_session_id_.empty() ) {
if ( is_flushing_ ) {
GST_DEBUG_OBJECT(self, "flushing");
return GST_FLOW_FLUSHING;
}
if ( !is_active_ ) {
GST_DEBUG_OBJECT(self, "inactive");
return GST_BASE_TRANSFORM_FLOW_DROPPED;
}
GST_ELEMENT_ERROR (self, STREAM, DECRYPT_NOKEY, ("No session found"), (NULL));
return GST_FLOW_NOT_SUPPORTED;
}
GstCaps *caps = nullptr;
gst_caps_replace(&caps, cached_caps_);
if ( !caps ) {
GstPad* sink_pad = gst_element_get_static_pad(GST_ELEMENT(self), "sink");
caps = gst_pad_get_current_caps(sink_pad);
gst_object_unref(sink_pad);
GST_DEBUG_OBJECT(self, "using new caps for decrypt = %" GST_PTR_FORMAT, caps);
SetCachedCaps( caps );
}
#ifndef GST_DISABLE_GST_DEBUG
if ( debug_level >= GST_LEVEL_DEBUG ) {
start = g_get_monotonic_time();
}
#endif
int rc = drm_system_->Decrypt(
current_session_id_, buffer,
subsamples, subsample_count,
iv, key, caps);
if ( caps ) {
gst_caps_unref(caps);
caps = nullptr;
}
if ( rc != 0 ) {
if ( rc == (int)ERROR_INVALID_SESSION ) {
GST_DEBUG_OBJECT(self, "Invalid session. Probably due to player shutdown.");
return GST_BASE_TRANSFORM_FLOW_DROPPED;
}
GST_ELEMENT_ERROR (self, STREAM, DECRYPT, ("Decryption failed"), ("error code = %d", rc));
return GST_FLOW_ERROR;
}
if ( start ) {
gint64 dur_ms = (g_get_monotonic_time() - start) / 1000;
decrypt_dur_.push_back(dur_ms);
total_time_ += dur_ms;
total_size_ += gst_buffer_get_size(buffer);
++buf_count_;
if ( buf_count_ == 300 ) {
std::sort(decrypt_dur_.begin(), decrypt_dur_.end());
uint64_t median = decrypt_dur_[ decrypt_dur_.size() / 2 ];
uint64_t max_time = *decrypt_dur_.rbegin();
uint64_t total_time = total_time_; total_time_ = 0;
uint64_t total_size = total_size_; total_size_ = 0;
uint32_t buf_count = buf_count_; buf_count_ = 0;
decrypt_dur_.resize(0);
GST_DEBUG_OBJECT(self,
"%s decrypt time: "
"avg = %" G_GUINT64_FORMAT " ms, "
"median = %" G_GUINT64_FORMAT " ms, "
"max = %" G_GUINT64_FORMAT " ms, "
"size = %" G_GUINT64_FORMAT " kb, "
"buf count = %u",
IsVideo() ? "video" : "audio",
(total_time / buf_count),
median,
max_time,
(total_size / 1024),
buf_count);
}
}
return GST_FLOW_OK;
}
void SetDrmSystem(DrmSystemOcdm* system) {
if (drm_system_)
drm_system_->RemoveObserver(this);
drm_system_ = system;
if (drm_system_)
drm_system_->AddObserver(this);
}
void SetIsFlushing(bool is_flushing) {
::starboard::ScopedLock lock(mutex_);
is_flushing_ = is_flushing;
condition_.Signal();
}
void SetActive(bool is_active) {
::starboard::ScopedLock lock(mutex_);
is_active_ = is_active;
condition_.Signal();
}
void SetCachedCaps(GstCaps* caps) {
gst_caps_replace(&cached_caps_, caps);
if ( caps ) {
const GstStructure *s;
const gchar *media_type;
s = gst_caps_get_structure (caps, 0);
media_type = gst_structure_get_name (s);
is_video_ = g_str_has_prefix(media_type, "video");
}
}
bool IsVideo() const { return is_video_; }
private:
::starboard::Mutex mutex_;
::starboard::ConditionVariable condition_ { mutex_ };
GstCaps* cached_caps_ { nullptr };
GstMapInfo* awaiting_key_info_ { nullptr };
GstBuffer* current_key_id_ { nullptr };
std::string current_session_id_;
DrmSystemOcdm* drm_system_ { nullptr };
bool is_flushing_ { false };
bool is_active_ { true };
bool is_video_ { false };
std::vector<uint64_t> decrypt_dur_;
uint64_t total_time_ { 0 };
uint64_t total_size_ { 0 };
uint32_t buf_count_ { 0 };
};
#define cobalt_ocdm_decryptor_parent_class parent_class
G_DEFINE_TYPE_WITH_PRIVATE(CobaltOcdmDecryptor, cobalt_ocdm_decryptor, GST_TYPE_BASE_TRANSFORM);
static void cobalt_ocdm_decryptor_finalize(GObject*);
static GstCaps* cobalt_ocdm_decryptor_transform_caps(GstBaseTransform*, GstPadDirection, GstCaps*, GstCaps*);
static GstFlowReturn cobalt_ocdm_decryptor_transform_ip(GstBaseTransform* base, GstBuffer* buffer);
static gboolean cobalt_ocdm_decryptor_sink_event(GstBaseTransform* base, GstEvent* event);
static gboolean cobalt_ocdm_decryptor_stop(GstBaseTransform *base);
static gboolean cobalt_ocdm_decryptor_start(GstBaseTransform *base);
static void cobalt_ocdm_decryptor_set_context(GstElement* element, GstContext* context);
static GstStateChangeReturn cobalt_ocdm_decryptor_change_state(GstElement* element, GstStateChange transition);
static void cobalt_ocdm_decryptor_class_init(CobaltOcdmDecryptorClass* klass) {
GST_DEBUG_CATEGORY_INIT(
cobalt_ocdm_decryptor_debug_category,
"cobaltocdm", 0, "OCDM Decryptor for Cobalt");
GObjectClass* gobject_class = G_OBJECT_CLASS(klass);
gobject_class->finalize = GST_DEBUG_FUNCPTR(cobalt_ocdm_decryptor_finalize);
GstElementClass* element_class = GST_ELEMENT_CLASS(klass);
element_class->set_context = GST_DEBUG_FUNCPTR(cobalt_ocdm_decryptor_set_context);
element_class->change_state = GST_DEBUG_FUNCPTR(cobalt_ocdm_decryptor_change_state);
gst_element_class_add_pad_template(element_class, gst_static_pad_template_get(&sinkTemplate));
gst_element_class_add_pad_template(element_class, gst_static_pad_template_get(&srcTemplate));
gst_element_class_set_static_metadata(
element_class, "OCDM Decryptor.",
GST_ELEMENT_FACTORY_KLASS_DECRYPTOR,
"Decryptor element for Cobalt.", "Comcast");
GstBaseTransformClass* base_transform_class = GST_BASE_TRANSFORM_CLASS(klass);
base_transform_class->transform_caps = GST_DEBUG_FUNCPTR(cobalt_ocdm_decryptor_transform_caps);
base_transform_class->transform_ip = GST_DEBUG_FUNCPTR(cobalt_ocdm_decryptor_transform_ip);
base_transform_class->transform_ip_on_passthrough = FALSE;
base_transform_class->sink_event = GST_DEBUG_FUNCPTR(cobalt_ocdm_decryptor_sink_event);
base_transform_class->start = GST_DEBUG_FUNCPTR(cobalt_ocdm_decryptor_start);
base_transform_class->stop = GST_DEBUG_FUNCPTR(cobalt_ocdm_decryptor_stop);
}
static void cobalt_ocdm_decryptor_init(CobaltOcdmDecryptor* self) {
CobaltOcdmDecryptorPrivate* priv = reinterpret_cast<CobaltOcdmDecryptorPrivate*>(
cobalt_ocdm_decryptor_get_instance_private(self));
self->priv = new (priv) CobaltOcdmDecryptorPrivate();
GstBaseTransform* base = GST_BASE_TRANSFORM(self);
gst_base_transform_set_in_place(base, TRUE);
gst_base_transform_set_passthrough(base, FALSE);
gst_base_transform_set_gap_aware(base, FALSE);
}
static void cobalt_ocdm_decryptor_finalize(GObject* object) {
CobaltOcdmDecryptor* self = COBALT_OCDM_DECRYPTOR(object);
CobaltOcdmDecryptorPrivate* priv = reinterpret_cast<CobaltOcdmDecryptorPrivate*>(
cobalt_ocdm_decryptor_get_instance_private(self));
priv->~CobaltOcdmDecryptorPrivate();
GST_CALL_PARENT(G_OBJECT_CLASS, finalize, (object));
}
static GstCaps* cobalt_ocdm_decryptor_transform_caps(GstBaseTransform* base, GstPadDirection direction, GstCaps* caps, GstCaps* filter) {
if (direction == GST_PAD_UNKNOWN)
return nullptr;
CobaltOcdmDecryptor* self = COBALT_OCDM_DECRYPTOR(base);
CobaltOcdmDecryptorPrivate* priv = reinterpret_cast<CobaltOcdmDecryptorPrivate*>(
cobalt_ocdm_decryptor_get_instance_private(self));
GST_DEBUG_OBJECT(self, "Transform in direction: %s, caps %" GST_PTR_FORMAT ", filter %" GST_PTR_FORMAT,
direction == GST_PAD_SINK ? "GST_PAD_SINK" : "GST_PAD_SRC", caps, filter);
priv->SetCachedCaps( nullptr );
return GST_BASE_TRANSFORM_CLASS(parent_class)->transform_caps(base, direction, caps, filter);
}
static GstFlowReturn cobalt_ocdm_decryptor_transform_ip(GstBaseTransform* base, GstBuffer* buffer) {
CobaltOcdmDecryptor* self = COBALT_OCDM_DECRYPTOR(base);
CobaltOcdmDecryptorPrivate* priv = reinterpret_cast<CobaltOcdmDecryptorPrivate*>(
cobalt_ocdm_decryptor_get_instance_private(self));
GST_TRACE_OBJECT(self, "Transform in place buf=(%" GST_PTR_FORMAT ")", buffer);
GstProtectionMeta* protection_meta = reinterpret_cast<GstProtectionMeta*>(gst_buffer_get_protection_meta(buffer));
if (!protection_meta) {
GST_TRACE_OBJECT(self, "Clear sample");
return GST_FLOW_OK;
}
GstFlowReturn ret = GST_FLOW_NOT_SUPPORTED;
GstStructure* info = protection_meta->info;
GstBuffer* subsamples = nullptr;
GstBuffer* iv = nullptr;
GstBuffer* key = nullptr;
uint32_t subsample_count = 0u;
const GValue* value = nullptr;
value = gst_structure_get_value(info, "kid");
if (!value) {
GST_ELEMENT_ERROR (self, STREAM, DECRYPT_NOKEY, ("No key ID available for encrypted sample"), (NULL));
goto exit;
}
key = gst_value_get_buffer(value);
value = gst_structure_get_value(info, "iv");
if (!value) {
GST_ELEMENT_ERROR (self, STREAM, DECRYPT_NOKEY, ("Failed to get IV buffer"), (NULL));
goto exit;
}
iv = gst_value_get_buffer(value);
if (!gst_structure_get_uint(info, "subsample_count", &subsample_count)) {
GST_ELEMENT_ERROR (self, STREAM, DECRYPT, ("Failed to get subsamples_count"), (NULL));
goto exit;
}
if (subsample_count) {
value = gst_structure_get_value(info, "subsamples");
if (!value) {
GST_ELEMENT_ERROR (self, STREAM, DECRYPT, ("Failed to get subsamples buffer"), (NULL));
goto exit;
}
subsamples = gst_value_get_buffer(value);
}
if (!gst_structure_has_field(info, "initWithLast15")) {
gst_structure_set (info, "initWithLast15", G_TYPE_UINT, 0, NULL);
}
ret = priv->Decrypt(self, buffer, subsamples, subsample_count, iv, key);
GST_TRACE_OBJECT(self, "ret=%s", gst_flow_get_name(ret));
exit:
gst_buffer_remove_meta(buffer, reinterpret_cast<GstMeta*>(protection_meta));
return ret;
}
static void cobalt_ocdm_decryptor_set_context(GstElement* element, GstContext* context) {
CobaltOcdmDecryptor* self = COBALT_OCDM_DECRYPTOR(element);
CobaltOcdmDecryptorPrivate* priv = reinterpret_cast<CobaltOcdmDecryptorPrivate*>(
cobalt_ocdm_decryptor_get_instance_private(self));
if (gst_context_has_context_type(context, "cobalt-drm-system")) {
const GValue* value = gst_structure_get_value(gst_context_get_structure(context), "drm-system-instance");
DrmSystemOcdm* drm_system = reinterpret_cast<DrmSystemOcdm*>(value ? g_value_get_pointer(value) : nullptr);
priv->SetDrmSystem( drm_system );
GST_DEBUG_OBJECT(self, "got drm system %p", drm_system);
return;
}
GST_ELEMENT_CLASS(parent_class)->set_context(element, context);
}
static GstStateChangeReturn cobalt_ocdm_decryptor_change_state(GstElement* element, GstStateChange transition) {
CobaltOcdmDecryptor* self = COBALT_OCDM_DECRYPTOR(element);
CobaltOcdmDecryptorPrivate* priv = reinterpret_cast<CobaltOcdmDecryptorPrivate*>(
cobalt_ocdm_decryptor_get_instance_private(self));
switch (transition) {
case GST_STATE_CHANGE_READY_TO_PAUSED:
priv->SetActive(true);
break;
case GST_STATE_CHANGE_PAUSED_TO_READY:
priv->SetActive(false);
break;
default:
break;
}
GstStateChangeReturn result = GST_ELEMENT_CLASS(parent_class)->change_state(element, transition);
return result;
}
static gboolean cobalt_ocdm_decryptor_sink_event(GstBaseTransform* base, GstEvent* event) {
CobaltOcdmDecryptor* self = COBALT_OCDM_DECRYPTOR(base);
CobaltOcdmDecryptorPrivate* priv = reinterpret_cast<CobaltOcdmDecryptorPrivate*>(
cobalt_ocdm_decryptor_get_instance_private(self));
switch (GST_EVENT_TYPE(event)) {
case GST_EVENT_FLUSH_START: {
GST_DEBUG_OBJECT(self, "flushing");
priv->SetIsFlushing(true);
break;
}
case GST_EVENT_FLUSH_STOP: {
GST_DEBUG_OBJECT(self, "flushing done");
priv->SetIsFlushing(false);
break;
default:
break;
}
}
return GST_BASE_TRANSFORM_CLASS(parent_class)->sink_event(base, event);
}
static gboolean cobalt_ocdm_decryptor_stop(GstBaseTransform *base) {
CobaltOcdmDecryptor* self = COBALT_OCDM_DECRYPTOR(base);
CobaltOcdmDecryptorPrivate* priv = reinterpret_cast<CobaltOcdmDecryptorPrivate*>(
cobalt_ocdm_decryptor_get_instance_private(self));
priv->SetActive(false);
return TRUE;
}
static gboolean cobalt_ocdm_decryptor_start(GstBaseTransform *base) {
CobaltOcdmDecryptor* self = COBALT_OCDM_DECRYPTOR(base);
CobaltOcdmDecryptorPrivate* priv = reinterpret_cast<CobaltOcdmDecryptorPrivate*>(
cobalt_ocdm_decryptor_get_instance_private(self));
priv->SetActive(true);
return TRUE;
}
} // namespace
GstElement *CreateDecryptorElement(const gchar* name) {
return GST_ELEMENT ( g_object_new (COBALT_OCDM_DECRYPTOR_TYPE, name) );
}
} // namespace drm
} // namespace shared
} // namespace rdk
} // namespace starboard
} // namespace third_party
#else //defined(HAS_OCDM)
namespace third_party {
namespace starboard {
namespace rdk {
namespace shared {
namespace drm {
GstElement *CreateDecryptorElement(const gchar* name) {
return nullptr;
}
} // namespace drm
} // namespace shared
} // namespace rdk
} // namespace starboard
} // namespace third_party
#endif //defined(HAS_OCDM)