| // |
| // 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/player/player_internal.h" |
| |
| #include <inttypes.h> |
| #include <stdint.h> |
| #include <math.h> |
| |
| #include <glib.h> |
| #include <gst/gst.h> |
| #include <gst/app/gstappsrc.h> |
| #include <gst/audio/audio.h> |
| #include <gst/audio/streamvolume.h> |
| #include <gst/base/gstbytewriter.h> |
| #include <gst/base/gstflowcombiner.h> |
| #include <gst/video/video.h> |
| #include <gst/base/gstbasetransform.h> |
| #include <gst/base/gstbaseparse.h> |
| |
| #include <map> |
| #include <string> |
| #include <vector> |
| #include <algorithm> |
| #include <functional> |
| #include <cstring> |
| |
| #include "starboard/once.h" |
| #include "starboard/common/mutex.h" |
| #include "starboard/common/condition_variable.h" |
| #include "starboard/thread.h" |
| #include "starboard/time.h" |
| #include "starboard/memory.h" |
| #include "starboard/drm.h" |
| #include "starboard/common/log.h" |
| #include "third_party/starboard/rdk/shared/media/gst_media_utils.h" |
| #include "third_party/starboard/rdk/shared/hang_detector.h" |
| #include "third_party/starboard/rdk/shared/drm/gst_decryptor_ocdm.h" |
| |
| namespace third_party { |
| namespace starboard { |
| namespace rdk { |
| namespace shared { |
| namespace player { |
| |
| static constexpr int kMaxNumberOfSamplesPerWrite = 1; |
| static const char kCustomInstantRateChangeEventName[] = "custom-instant-rate-change"; |
| static const char kDidReceiveFirstSegmentMsgName[] = "did-receive-first-segment"; |
| static const char kDidReachBufferingTargetMsgName[] = "did-reach-buffering-target"; |
| |
| // static |
| int Player::MaxNumberOfSamplesPerWrite() { |
| return kMaxNumberOfSamplesPerWrite; |
| } |
| |
| using third_party::starboard::rdk::shared::drm::CreateDecryptorElement; |
| using third_party::starboard::rdk::shared::media::CodecToGstCaps; |
| |
| // **************************** GST/GLIB Helpers **************************** // |
| |
| namespace { |
| |
| GST_DEBUG_CATEGORY(cobalt_gst_player_debug); |
| #define GST_CAT_DEFAULT cobalt_gst_player_debug |
| |
| #if !defined(GST_HAS_HDR_SUPPORT) && GST_CHECK_VERSION(1, 18, 0) |
| #define GST_HAS_HDR_SUPPORT 1 |
| #endif |
| |
| static void PrintGstCaps(GstCaps* caps); |
| static GstElement* CreatePayloader(); |
| static GstElement* CreateGstElement(const gchar* factory_name, const gchar* name_format, ...) G_GNUC_PRINTF (2, 3); |
| |
| static GSourceFuncs SourceFunctions = { |
| // prepare |
| nullptr, |
| // check |
| nullptr, |
| // dispatch |
| [](GSource* source, GSourceFunc callback, gpointer userData) -> gboolean { |
| if (g_source_get_ready_time(source) == -1) |
| return G_SOURCE_CONTINUE; |
| g_source_set_ready_time(source, -1); |
| return callback(userData); |
| }, |
| // finalize |
| nullptr, |
| // closure_callback |
| nullptr, |
| // closure_marshall |
| nullptr, |
| }; |
| |
| unsigned getGstPlayFlag(const char* nick) { |
| static GFlagsClass* flagsClass = static_cast<GFlagsClass*>( |
| g_type_class_ref(g_type_from_name("GstPlayFlags"))); |
| SB_DCHECK(flagsClass); |
| |
| GFlagsValue* flag = g_flags_get_value_by_nick(flagsClass, nick); |
| if (!flag) |
| return 0; |
| |
| return flag->value; |
| } |
| |
| bool isRialtoEnabled() { |
| static bool is_rialto_enabled = false; |
| static gsize init = 0; |
| |
| if (g_once_init_enter (&init)) { |
| GstElementFactory *factory = gst_element_factory_find("rialtomsevideosink"); |
| if (factory) { |
| guint rank = gst_plugin_feature_get_rank((GstPluginFeature *)factory); |
| gst_object_unref(GST_OBJECT(factory)); |
| is_rialto_enabled = rank >= GST_RANK_PRIMARY; |
| } |
| g_once_init_leave (&init, 1); |
| } |
| |
| return is_rialto_enabled; |
| } |
| |
| bool enableNativeAudio() { |
| static bool enable_native_audio = false; |
| static gsize init = 0; |
| |
| if (g_once_init_enter (&init)) { |
| GstElementFactory* factory = gst_element_factory_find("brcmaudiosink"); |
| if (factory) { |
| gst_object_unref(GST_OBJECT(factory)); |
| enable_native_audio = true; |
| } |
| else { |
| enable_native_audio = isRialtoEnabled(); |
| } |
| g_once_init_leave (&init, 1); |
| } |
| |
| return enable_native_audio; |
| } |
| |
| G_BEGIN_DECLS |
| |
| #define GST_COBALT_TYPE_SRC (gst_cobalt_src_get_type()) |
| #define GST_COBALT_SRC(obj) \ |
| (G_TYPE_CHECK_INSTANCE_CAST((obj), GST_COBALT_TYPE_SRC, GstCobaltSrc)) |
| #define GST_COBALT_SRC_CLASS(obj) \ |
| (G_TYPE_CHECK_CLASS_CAST((klass), GST_COBALT_TYPE_SRC, GstCobaltSrcClass)) |
| #define GST_IS_COLABT_SRC(obj) \ |
| (G_TYPE_CHECK_INSTANCE_TYPE((obj), GST_COBALT_TYPE_SRC)) |
| #define GST_IS_COBALT_SRC_CLASS(klass) \ |
| (G_TYPE_CHECK_CLASS_TYPE((klass), GST_COBALT_TYPE_SRC)) |
| |
| typedef struct _GstCobaltSrc GstCobaltSrc; |
| typedef struct _GstCobaltSrcClass GstCobaltSrcClass; |
| typedef struct _GstCobaltSrcPrivate GstCobaltSrcPrivate; |
| |
| struct _GstCobaltSrc { |
| GstBin parent; |
| GstCobaltSrcPrivate* priv; |
| }; |
| |
| struct _GstCobaltSrcClass { |
| GstBinClass parentClass; |
| }; |
| |
| GType gst_cobalt_src_get_type(void); |
| |
| G_END_DECLS |
| |
| struct _GstCobaltSrcPrivate { |
| gchar* uri; |
| guint pad_number; |
| gboolean async_start; |
| gboolean async_done; |
| GstFlowCombiner* flow_combiner; |
| }; |
| |
| enum { PROP_0, PROP_LOCATION }; |
| |
| static GstStaticPadTemplate src_template = |
| GST_STATIC_PAD_TEMPLATE("src_%u", |
| GST_PAD_SRC, |
| GST_PAD_SOMETIMES, |
| GST_STATIC_CAPS_ANY); |
| |
| static void gst_cobalt_src_uri_handler_init(gpointer gIface, |
| gpointer ifaceData); |
| #define gst_cobalt_src_parent_class parent_class |
| G_DEFINE_TYPE_WITH_CODE(GstCobaltSrc, |
| gst_cobalt_src, |
| GST_TYPE_BIN, |
| G_ADD_PRIVATE(GstCobaltSrc) |
| G_IMPLEMENT_INTERFACE(GST_TYPE_URI_HANDLER, |
| gst_cobalt_src_uri_handler_init)); |
| |
| static void gst_cobalt_src_init(GstCobaltSrc* src) { |
| GstCobaltSrcPrivate* priv = (GstCobaltSrcPrivate*)gst_cobalt_src_get_instance_private(src); |
| new (priv) GstCobaltSrcPrivate(); |
| src->priv = priv; |
| src->priv->pad_number = 0; |
| src->priv->async_start = FALSE; |
| src->priv->async_done = FALSE; |
| src->priv->flow_combiner = gst_flow_combiner_new(); |
| g_object_set(GST_BIN(src), "message-forward", TRUE, NULL); |
| } |
| |
| static void gst_cobalt_src_dispose(GObject* object) { |
| GST_CALL_PARENT(G_OBJECT_CLASS, dispose, (object)); |
| } |
| |
| static void gst_cobalt_src_finalize(GObject* object) { |
| GstCobaltSrc* src = GST_COBALT_SRC(object); |
| GstCobaltSrcPrivate* priv = src->priv; |
| |
| g_free(priv->uri); |
| gst_flow_combiner_free(priv->flow_combiner); |
| priv->~GstCobaltSrcPrivate(); |
| |
| GST_CALL_PARENT(G_OBJECT_CLASS, finalize, (object)); |
| } |
| |
| static void gst_cobalt_src_set_property(GObject* object, |
| guint propID, |
| const GValue* value, |
| GParamSpec* pspec) { |
| GstCobaltSrc* src = GST_COBALT_SRC(object); |
| |
| switch (propID) { |
| case PROP_LOCATION: |
| gst_uri_handler_set_uri(reinterpret_cast<GstURIHandler*>(src), |
| g_value_get_string(value), 0); |
| break; |
| default: |
| G_OBJECT_WARN_INVALID_PROPERTY_ID(object, propID, pspec); |
| break; |
| } |
| } |
| |
| static void gst_cobalt_src_get_property(GObject* object, |
| guint propID, |
| GValue* value, |
| GParamSpec* pspec) { |
| GstCobaltSrc* src = GST_COBALT_SRC(object); |
| GstCobaltSrcPrivate* priv = src->priv; |
| |
| GST_OBJECT_LOCK(src); |
| switch (propID) { |
| case PROP_LOCATION: |
| g_value_set_string(value, priv->uri); |
| break; |
| default: |
| G_OBJECT_WARN_INVALID_PROPERTY_ID(object, propID, pspec); |
| break; |
| } |
| GST_OBJECT_UNLOCK(src); |
| } |
| |
| // uri handler interface |
| static GstURIType gst_cobalt_src_uri_get_type(GType) { |
| return GST_URI_SRC; |
| } |
| |
| const gchar* const* gst_cobalt_src_get_protocols(GType) { |
| static const char* protocols[] = {"cobalt", 0}; |
| return protocols; |
| } |
| |
| static gchar* gst_cobalt_src_get_uri(GstURIHandler* handler) { |
| GstCobaltSrc* src = GST_COBALT_SRC(handler); |
| gchar* ret; |
| |
| GST_OBJECT_LOCK(src); |
| ret = g_strdup(src->priv->uri); |
| GST_OBJECT_UNLOCK(src); |
| return ret; |
| } |
| |
| static gboolean gst_cobalt_src_set_uri(GstURIHandler* handler, |
| const gchar* uri, |
| GError** error) { |
| GstCobaltSrc* src = GST_COBALT_SRC(handler); |
| GstCobaltSrcPrivate* priv = src->priv; |
| |
| if (GST_STATE(src) >= GST_STATE_PAUSED) { |
| GST_ERROR_OBJECT(src, "URI can only be set in states < PAUSED"); |
| return FALSE; |
| } |
| |
| GST_OBJECT_LOCK(src); |
| |
| g_free(priv->uri); |
| priv->uri = 0; |
| |
| if (!uri) { |
| GST_OBJECT_UNLOCK(src); |
| return TRUE; |
| } |
| |
| priv->uri = g_strdup(uri); |
| GST_OBJECT_UNLOCK(src); |
| return TRUE; |
| } |
| |
| static void gst_cobalt_src_uri_handler_init(gpointer gIface, gpointer) { |
| GstURIHandlerInterface* iface = (GstURIHandlerInterface*)gIface; |
| |
| iface->get_type = gst_cobalt_src_uri_get_type; |
| iface->get_protocols = gst_cobalt_src_get_protocols; |
| iface->get_uri = gst_cobalt_src_get_uri; |
| iface->set_uri = gst_cobalt_src_set_uri; |
| } |
| |
| static gboolean gst_cobalt_src_query_with_parent(GstPad* pad, |
| GstObject* parent, |
| GstQuery* query) { |
| gboolean result = FALSE; |
| |
| switch (GST_QUERY_TYPE(query)) { |
| default: { |
| GstPad* target = gst_ghost_pad_get_target(GST_GHOST_PAD_CAST(pad)); |
| // Forward the query to the proxy target pad. |
| if (target) |
| result = gst_pad_query(target, query); |
| gst_object_unref(target); |
| break; |
| } |
| } |
| |
| return result; |
| } |
| |
| static GstFlowReturn gst_cobalt_src_chain_with_parent(GstPad* pad, GstObject* parent, GstBuffer* buffer) { |
| GstCobaltSrc* src = GST_COBALT_SRC(gst_object_get_parent(parent)); |
| GstFlowReturn ret = gst_proxy_pad_chain_default(pad, GST_OBJECT(src), buffer); |
| if (ret != GST_FLOW_FLUSHING) |
| ret = gst_flow_combiner_update_pad_flow(src->priv->flow_combiner, pad, ret); |
| gst_object_unref(src); |
| return ret; |
| } |
| |
| static void gst_cobalt_src_handle_message(GstBin* bin, GstMessage* message) { |
| GstCobaltSrc* src = GST_COBALT_SRC(GST_ELEMENT(bin)); |
| |
| switch (GST_MESSAGE_TYPE(message)) { |
| case GST_MESSAGE_EOS: { |
| gboolean emit_eos = TRUE; |
| GstPad* pad = gst_element_get_static_pad( |
| GST_ELEMENT(GST_MESSAGE_SRC(message)), "src"); |
| |
| GST_DEBUG_OBJECT(src, "EOS received from %s", |
| GST_MESSAGE_SRC_NAME(message)); |
| g_object_set_data(G_OBJECT(pad), "is-eos", GINT_TO_POINTER(1)); |
| gst_object_unref(pad); |
| for (guint i = 0; i < src->priv->pad_number; i++) { |
| gchar* name = g_strdup_printf("src_%u", i); |
| GstPad* src_pad = gst_element_get_static_pad(GST_ELEMENT(src), name); |
| GstPad* target = gst_ghost_pad_get_target(GST_GHOST_PAD_CAST(src_pad)); |
| gint is_eos = |
| GPOINTER_TO_INT(g_object_get_data(G_OBJECT(target), "is-eos")); |
| gst_object_unref(target); |
| gst_object_unref(src_pad); |
| g_free(name); |
| |
| if (!is_eos) { |
| emit_eos = FALSE; |
| break; |
| } |
| } |
| |
| gst_message_unref(message); |
| |
| if (emit_eos) { |
| GST_DEBUG_OBJECT(src, |
| "All appsrc elements are EOS, emitting event now."); |
| gst_element_send_event(GST_ELEMENT(bin), gst_event_new_eos()); |
| } |
| break; |
| } |
| default: |
| GST_BIN_CLASS(parent_class)->handle_message(bin, message); |
| break; |
| } |
| } |
| |
| void gst_cobalt_src_setup_and_add_app_src(SbMediaType media_type, |
| GstElement* element, |
| GstElement* appsrc, |
| GstAppSrcCallbacks* callbacks, |
| gpointer user_data, |
| gboolean inject_decryptor) { |
| const uint32_t kAudioMaxBytes = 256 * 1024; |
| const uint32_t kVideoMaxBytes = 8 * 1024 * 1024; |
| |
| uint32_t max_bytes = (media_type == kSbMediaTypeVideo) ? kVideoMaxBytes : kAudioMaxBytes; |
| |
| g_object_set(appsrc, |
| "block", FALSE, |
| "format", GST_FORMAT_TIME, |
| "min-percent", 50, |
| nullptr); |
| gst_app_src_set_stream_type(GST_APP_SRC(appsrc), GST_APP_STREAM_TYPE_SEEKABLE); |
| gst_app_src_set_emit_signals(GST_APP_SRC(appsrc), FALSE); |
| gst_app_src_set_callbacks(GST_APP_SRC(appsrc), callbacks, user_data, nullptr); |
| gst_app_src_set_max_bytes(GST_APP_SRC(appsrc), max_bytes); |
| gst_base_src_set_format(GST_BASE_SRC(appsrc), GST_FORMAT_TIME); |
| |
| GstCobaltSrc* src = GST_COBALT_SRC(element); |
| gchar* name = g_strdup_printf("src_%u", src->priv->pad_number); |
| src->priv->pad_number++; |
| gst_bin_add(GST_BIN(element), appsrc); |
| |
| GstElement* src_elem = appsrc; |
| GstElement* decryptor = inject_decryptor ? CreateDecryptorElement(nullptr) : nullptr; |
| GstElement* payloader = nullptr; |
| if (decryptor && media_type == kSbMediaTypeVideo && !isRialtoEnabled()) { |
| payloader = CreatePayloader(); |
| } |
| |
| if (decryptor) { |
| GST_DEBUG_OBJECT(element, "Injecting decryptor element %" GST_PTR_FORMAT, decryptor); |
| |
| gst_bin_add(GST_BIN(element), decryptor); |
| gst_element_sync_state_with_parent(decryptor); |
| gst_element_link(src_elem, decryptor); |
| src_elem = decryptor; |
| } |
| |
| if (payloader) { |
| GST_DEBUG_OBJECT(element, "Injecting payloader element %" GST_PTR_FORMAT, payloader); |
| |
| if (GST_IS_BASE_TRANSFORM(payloader)) { |
| gst_base_transform_set_in_place(GST_BASE_TRANSFORM(payloader), TRUE); |
| } |
| |
| gst_bin_add(GST_BIN(element), payloader); |
| gst_element_sync_state_with_parent(payloader); |
| gst_element_link(src_elem, payloader); |
| src_elem = payloader; |
| } |
| |
| { |
| GstElement* queue = gst_element_factory_make("queue", nullptr); |
| g_object_set ( |
| G_OBJECT (queue), |
| "max-size-buffers", 60, |
| "max-size-bytes", 0, |
| "max-size-time", (gint64) 0, |
| "silent", TRUE, |
| nullptr); |
| gst_bin_add(GST_BIN(element), queue); |
| gst_element_sync_state_with_parent(queue); |
| gst_element_link(src_elem, queue); |
| src_elem = queue; |
| } |
| |
| GstPad* target_pad = gst_element_get_static_pad(src_elem, "src"); |
| GstPad* pad = gst_ghost_pad_new(name, target_pad); |
| |
| GstMessage *msg = gst_message_new_application( |
| GST_OBJECT(appsrc), gst_structure_new_empty(kDidReceiveFirstSegmentMsgName)); |
| gst_pad_add_probe ( |
| pad, GST_PAD_PROBE_TYPE_EVENT_DOWNSTREAM, |
| [](GstPad * pad, GstPadProbeInfo * info, gpointer data) -> GstPadProbeReturn { |
| GstEvent *event = GST_PAD_PROBE_INFO_EVENT(info); |
| if ( GST_EVENT_TYPE(event) == GST_EVENT_SEGMENT ) { |
| GstPad *peer_pad = gst_pad_get_peer(pad); |
| if (peer_pad) { |
| gst_pad_send_event(peer_pad, gst_event_ref(event)); |
| gst_event_replace ( |
| (GstEvent **) &info->data, |
| gst_event_new_sink_message(kDidReceiveFirstSegmentMsgName, GST_MESSAGE(data))); |
| gst_object_unref(peer_pad); |
| } |
| return GST_PAD_PROBE_REMOVE; |
| } |
| return GST_PAD_PROBE_OK; |
| }, msg, [](gpointer data) { gst_message_unref(GST_MESSAGE(data)); }); |
| |
| // Allocation query can stall streaming thread if sent during pre-roll. |
| // Drop the query since we don't use proposed allocation anyway. |
| gst_pad_add_probe ( |
| pad, GST_PAD_PROBE_TYPE_QUERY_DOWNSTREAM, |
| [](GstPad * pad, GstPadProbeInfo * info, gpointer data) -> GstPadProbeReturn { |
| GstQuery* query = GST_PAD_PROBE_INFO_QUERY(info); |
| if (GST_QUERY_TYPE (query) == GST_QUERY_ALLOCATION) { |
| GST_TRACE_OBJECT(pad, "Drop allocation query"); |
| return GST_PAD_PROBE_DROP; |
| } |
| return GST_PAD_PROBE_OK; |
| }, nullptr, nullptr); |
| |
| auto proxypad = GST_PAD(gst_proxy_pad_get_internal(GST_PROXY_PAD(pad))); |
| gst_flow_combiner_add_pad(src->priv->flow_combiner, proxypad); |
| gst_pad_set_chain_function(proxypad, static_cast<GstPadChainFunction>(gst_cobalt_src_chain_with_parent)); |
| gst_object_unref(proxypad); |
| |
| gst_pad_set_query_function(pad, gst_cobalt_src_query_with_parent); |
| gst_pad_set_active(pad, TRUE); |
| |
| gst_element_add_pad(element, pad); |
| GST_OBJECT_FLAG_SET(pad, GST_PAD_FLAG_NEED_PARENT); |
| |
| gst_element_sync_state_with_parent(appsrc); |
| |
| g_free(name); |
| gst_object_unref(target_pad); |
| } |
| |
| static void gst_cobalt_src_do_async_start(GstCobaltSrc* src) { |
| GstCobaltSrcPrivate* priv = src->priv; |
| if (priv->async_done) |
| return; |
| priv->async_start = TRUE; |
| GST_BIN_CLASS(parent_class) |
| ->handle_message(GST_BIN(src), |
| gst_message_new_async_start(GST_OBJECT(src))); |
| } |
| |
| static void gst_cobalt_src_do_async_done(GstCobaltSrc* src) { |
| GstCobaltSrcPrivate* priv = src->priv; |
| if (priv->async_start) { |
| GST_BIN_CLASS(parent_class) |
| ->handle_message( |
| GST_BIN(src), |
| gst_message_new_async_done(GST_OBJECT(src), GST_CLOCK_TIME_NONE)); |
| priv->async_start = FALSE; |
| priv->async_done = TRUE; |
| } |
| } |
| |
| void gst_cobalt_src_all_app_srcs_added(GstElement* element) { |
| GstCobaltSrc* src = GST_COBALT_SRC(element); |
| |
| GST_DEBUG_OBJECT(src, |
| "===> All sources registered, completing state-change " |
| "(TID:%d)", |
| SbThreadGetId()); |
| gst_element_no_more_pads(element); |
| gst_cobalt_src_do_async_done(src); |
| } |
| |
| static GstStateChangeReturn gst_cobalt_src_change_state( |
| GstElement* element, |
| GstStateChange transition) { |
| GstStateChangeReturn ret = GST_STATE_CHANGE_SUCCESS; |
| GstCobaltSrc* src = GST_COBALT_SRC(element); |
| GstCobaltSrcPrivate* priv = src->priv; |
| |
| switch (transition) { |
| case GST_STATE_CHANGE_READY_TO_PAUSED: |
| gst_cobalt_src_do_async_start(src); |
| break; |
| default: |
| break; |
| } |
| |
| ret = GST_ELEMENT_CLASS(parent_class)->change_state(element, transition); |
| if (G_UNLIKELY(ret == GST_STATE_CHANGE_FAILURE)) { |
| gst_cobalt_src_do_async_done(src); |
| return ret; |
| } |
| |
| switch (transition) { |
| case GST_STATE_CHANGE_READY_TO_PAUSED: { |
| if (!priv->async_done) |
| ret = GST_STATE_CHANGE_ASYNC; |
| break; |
| } |
| case GST_STATE_CHANGE_PAUSED_TO_READY: { |
| gst_cobalt_src_do_async_done(src); |
| break; |
| } |
| default: |
| break; |
| } |
| |
| return ret; |
| } |
| |
| static void gst_cobalt_src_class_init(GstCobaltSrcClass* klass) { |
| GObjectClass* oklass = G_OBJECT_CLASS(klass); |
| GstElementClass* eklass = GST_ELEMENT_CLASS(klass); |
| GstBinClass* bklass = GST_BIN_CLASS(klass); |
| |
| oklass->dispose = gst_cobalt_src_dispose; |
| oklass->finalize = gst_cobalt_src_finalize; |
| oklass->set_property = gst_cobalt_src_set_property; |
| oklass->get_property = gst_cobalt_src_get_property; |
| |
| gst_element_class_add_pad_template( |
| eklass, gst_static_pad_template_get(&src_template)); |
| gst_element_class_set_metadata(eklass, "Cobalt source element", "Source", |
| "Handles data incoming from the Cobalt player", |
| "Pawel Stanek <p.stanek@metrological.com>"); |
| g_object_class_install_property( |
| oklass, PROP_LOCATION, |
| g_param_spec_string( |
| "location", "location", "Location to read from", 0, |
| (GParamFlags)(G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS))); |
| bklass->handle_message = GST_DEBUG_FUNCPTR(gst_cobalt_src_handle_message); |
| eklass->change_state = GST_DEBUG_FUNCPTR(gst_cobalt_src_change_state); |
| } |
| |
| void ParseMaxVideoCapabilities(const char* caps_str, uint32_t *w, uint32_t *h, uint32_t *f) { |
| auto parse_entry = [&w, &h, &f](std::string& entry) { |
| auto end = std::remove_if(entry.begin(), entry.end(), |
| [](unsigned char c) { return std::isspace(c) || c == '"' || c == '\''; }); |
| if (end != entry.end()) { |
| *end = '\0'; |
| } |
| auto mid = std::find(entry.begin(), end, '='); |
| if (mid == entry.begin() || (mid + 1) == end) { |
| return; |
| } |
| auto key_len = std::distance(entry.begin(), mid); |
| if ( f && ::strncasecmp("framerate", entry.c_str(), key_len) == 0 ) { |
| *f = strtoul(&(*(mid + 1)), nullptr, 10); |
| } |
| else if ( w && ::strncasecmp("width", entry.c_str(), key_len) == 0 ) { |
| *w = strtoul(&(*(mid + 1)), nullptr, 10); |
| } |
| else if ( h && ::strncasecmp("height", entry.c_str(), key_len) == 0 ) { |
| *h = strtoul(&(*(mid + 1)), nullptr, 10); |
| } |
| }; |
| std::istringstream ss(caps_str); |
| std::string txt; |
| while(std::getline(ss, txt, ';')) { |
| parse_entry(txt); |
| } |
| } |
| |
| #if defined(GST_HAS_HDR_SUPPORT) && GST_HAS_HDR_SUPPORT |
| static GstVideoColorRange RangeIdToGstVideoColorRange(SbMediaRangeId value) { |
| switch (value) { |
| case kSbMediaRangeIdLimited: |
| return GST_VIDEO_COLOR_RANGE_16_235; |
| case kSbMediaRangeIdFull: |
| return GST_VIDEO_COLOR_RANGE_0_255; |
| default: |
| case kSbMediaRangeIdUnspecified: |
| return GST_VIDEO_COLOR_RANGE_UNKNOWN; |
| } |
| } |
| |
| static GstVideoColorMatrix MatrixIdToGstVideoColorMatrix(SbMediaMatrixId value) { |
| switch (value) { |
| case kSbMediaMatrixIdRgb: |
| return GST_VIDEO_COLOR_MATRIX_RGB; |
| case kSbMediaMatrixIdBt709: |
| return GST_VIDEO_COLOR_MATRIX_BT709; |
| case kSbMediaMatrixIdFcc: |
| return GST_VIDEO_COLOR_MATRIX_FCC; |
| case kSbMediaMatrixIdBt470Bg: |
| case kSbMediaMatrixIdSmpte170M: |
| return GST_VIDEO_COLOR_MATRIX_BT601; |
| case kSbMediaMatrixIdSmpte240M: |
| return GST_VIDEO_COLOR_MATRIX_SMPTE240M; |
| case kSbMediaMatrixIdBt2020NonconstantLuminance: |
| return GST_VIDEO_COLOR_MATRIX_BT2020; |
| case kSbMediaMatrixIdUnspecified: |
| default: |
| return GST_VIDEO_COLOR_MATRIX_UNKNOWN; |
| } |
| } |
| |
| static GstVideoTransferFunction TransferIdToGstVideoTransferFunction(SbMediaTransferId value) { |
| switch (value) { |
| case kSbMediaTransferIdBt709: |
| case kSbMediaTransferIdSmpte170M: |
| return GST_VIDEO_TRANSFER_BT709; |
| case kSbMediaTransferIdGamma22: |
| return GST_VIDEO_TRANSFER_GAMMA22; |
| case kSbMediaTransferIdGamma28: |
| return GST_VIDEO_TRANSFER_GAMMA28; |
| case kSbMediaTransferIdSmpte240M: |
| return GST_VIDEO_TRANSFER_SMPTE240M; |
| case kSbMediaTransferIdLinear: |
| return GST_VIDEO_TRANSFER_GAMMA10; |
| case kSbMediaTransferIdLog: |
| return GST_VIDEO_TRANSFER_LOG100; |
| case kSbMediaTransferIdLogSqrt: |
| return GST_VIDEO_TRANSFER_LOG316; |
| case kSbMediaTransferIdIec6196621: |
| return GST_VIDEO_TRANSFER_SRGB; |
| case kSbMediaTransferId10BitBt2020: |
| return GST_VIDEO_TRANSFER_BT2020_10; |
| case kSbMediaTransferId12BitBt2020: |
| return GST_VIDEO_TRANSFER_BT2020_12; |
| case kSbMediaTransferIdSmpteSt2084: |
| return GST_VIDEO_TRANSFER_SMPTE2084; |
| case kSbMediaTransferIdAribStdB67: |
| return GST_VIDEO_TRANSFER_ARIB_STD_B67; |
| case kSbMediaTransferIdUnspecified: |
| default: |
| return GST_VIDEO_TRANSFER_UNKNOWN; |
| } |
| } |
| |
| static GstVideoColorPrimaries PrimaryIdToGstVideoColorPrimaries(SbMediaPrimaryId value) { |
| switch (value) { |
| case kSbMediaPrimaryIdBt709: |
| return GST_VIDEO_COLOR_PRIMARIES_BT709; |
| case kSbMediaPrimaryIdBt470M: |
| return GST_VIDEO_COLOR_PRIMARIES_BT470M; |
| case kSbMediaPrimaryIdBt470Bg: |
| return GST_VIDEO_COLOR_PRIMARIES_BT470BG; |
| case kSbMediaPrimaryIdSmpte170M: |
| return GST_VIDEO_COLOR_PRIMARIES_SMPTE170M; |
| case kSbMediaPrimaryIdSmpte240M: |
| return GST_VIDEO_COLOR_PRIMARIES_SMPTE240M; |
| case kSbMediaPrimaryIdFilm: |
| return GST_VIDEO_COLOR_PRIMARIES_FILM; |
| case kSbMediaPrimaryIdBt2020: |
| return GST_VIDEO_COLOR_PRIMARIES_BT2020; |
| case kSbMediaPrimaryIdUnspecified: |
| default: |
| return GST_VIDEO_COLOR_PRIMARIES_UNKNOWN; |
| } |
| } |
| |
| static void AddColorMetadataToGstCaps(GstCaps* caps, const SbMediaColorMetadata& color_metadata) { |
| GstVideoColorimetry colorimetry; |
| colorimetry.range = RangeIdToGstVideoColorRange(color_metadata.range); |
| colorimetry.matrix = MatrixIdToGstVideoColorMatrix(color_metadata.matrix); |
| colorimetry.transfer = TransferIdToGstVideoTransferFunction(color_metadata.transfer); |
| colorimetry.primaries = PrimaryIdToGstVideoColorPrimaries(color_metadata.primaries); |
| |
| if (colorimetry.range != GST_VIDEO_COLOR_RANGE_UNKNOWN || |
| colorimetry.matrix != GST_VIDEO_COLOR_MATRIX_UNKNOWN || |
| colorimetry.transfer != GST_VIDEO_TRANSFER_UNKNOWN || |
| colorimetry.primaries != GST_VIDEO_COLOR_PRIMARIES_UNKNOWN) { |
| gchar *tmp = |
| gst_video_colorimetry_to_string (&colorimetry); |
| gst_caps_set_simple (caps, "colorimetry", G_TYPE_STRING, tmp, NULL); |
| GST_DEBUG ("Setting \"colorimetry\" to %s", tmp); |
| g_free (tmp); |
| } |
| GstVideoMasteringDisplayInfo mastering_display_info; |
| gst_video_mastering_display_info_init (&mastering_display_info); |
| |
| mastering_display_info.display_primaries[0].x = (guint16)(color_metadata.mastering_metadata.primary_r_chromaticity_x * 50000); |
| mastering_display_info.display_primaries[0].y = (guint16)(color_metadata.mastering_metadata.primary_r_chromaticity_y * 50000); |
| mastering_display_info.display_primaries[1].x = (guint16)(color_metadata.mastering_metadata.primary_g_chromaticity_x * 50000); |
| mastering_display_info.display_primaries[1].y = (guint16)(color_metadata.mastering_metadata.primary_g_chromaticity_y * 50000); |
| mastering_display_info.display_primaries[2].x = (guint16)(color_metadata.mastering_metadata.primary_b_chromaticity_x * 50000); |
| mastering_display_info.display_primaries[2].y = (guint16)(color_metadata.mastering_metadata.primary_b_chromaticity_y * 50000); |
| mastering_display_info.white_point.x = (guint16)(color_metadata.mastering_metadata.white_point_chromaticity_x * 50000); |
| mastering_display_info.white_point.y = (guint16)(color_metadata.mastering_metadata.white_point_chromaticity_y * 50000); |
| mastering_display_info.max_display_mastering_luminance = (guint32)ceil(color_metadata.mastering_metadata.luminance_max); |
| mastering_display_info.min_display_mastering_luminance = (guint32)ceil(color_metadata.mastering_metadata.luminance_min); |
| |
| gchar *tmp = |
| gst_video_mastering_display_info_to_string(&mastering_display_info); |
| gst_caps_set_simple (caps, "mastering-display-info", G_TYPE_STRING, tmp, NULL); |
| GST_DEBUG ("Setting \"mastering-display-info\" to %s", tmp); |
| g_free (tmp); |
| if (color_metadata.max_cll && color_metadata.max_fall) { |
| GstVideoContentLightLevel content_light_level; |
| content_light_level.max_content_light_level = color_metadata.max_cll; |
| content_light_level.max_frame_average_light_level = color_metadata.max_fall; |
| gchar *tmp = gst_video_content_light_level_to_string(&content_light_level); |
| gst_caps_set_simple (caps, "content-light-level", G_TYPE_STRING, tmp, NULL); |
| GST_DEBUG ("setting \"content-light-level\" to %s", tmp); |
| g_free (tmp); |
| } |
| } |
| #else |
| static void AddColorMetadataToGstCaps(GstCaps*, const SbMediaColorMetadata&) {} |
| #endif |
| |
| static int CompareColorMetadata(const SbMediaColorMetadata& lhs, const SbMediaColorMetadata& rhs) { |
| return memcmp(&lhs, &rhs, sizeof(SbMediaColorMetadata)); |
| } |
| |
| #if SB_API_VERSION >= 15 |
| static void AddVideoInfoToGstCaps(const SbMediaVideoStreamInfo& info, GstCaps* caps) { |
| #else |
| static void AddVideoInfoToGstCaps(const SbMediaVideoSampleInfo& info, GstCaps* caps) { |
| #endif |
| AddColorMetadataToGstCaps(caps, info.color_metadata); |
| gst_caps_set_simple (caps, |
| "width", G_TYPE_INT, info.frame_width, |
| "height", G_TYPE_INT, info.frame_height, |
| NULL); |
| |
| if (info.max_video_capabilities && *info.max_video_capabilities) { |
| uint32_t framerate = 0; |
| ParseMaxVideoCapabilities(info.max_video_capabilities, nullptr, nullptr, &framerate); |
| gst_caps_set_simple (caps, "framerate", GST_TYPE_FRACTION, framerate, 1, NULL); |
| } |
| } |
| |
| static void PrintPositionPerSink(GstElement* element, GstDebugLevel level) |
| { |
| #ifndef GST_DISABLE_GST_DEBUG |
| if (gst_debug_category_get_threshold(GST_CAT_DEFAULT) < level) |
| return; |
| #endif |
| |
| auto fold_func = [](const GValue *vitem, GValue*, gpointer data) -> gboolean { |
| GstDebugLevel level = (GstDebugLevel)GPOINTER_TO_INT(data); |
| GstObject *item = GST_OBJECT(g_value_get_object (vitem)); |
| if (GST_IS_BIN (item)) { |
| PrintPositionPerSink(GST_ELEMENT(item), level); |
| } |
| else if (GST_IS_BASE_SINK(item)) { |
| GstElement* el = GST_ELEMENT(item); |
| gint64 position = GST_CLOCK_TIME_NONE; |
| GstQuery* query = gst_query_new_position(GST_FORMAT_TIME); |
| if (gst_element_query(el, query)) { |
| gst_query_parse_position(query, 0, &position); |
| } |
| gst_query_unref(query); |
| GST_CAT_LEVEL_LOG (GST_CAT_DEFAULT, level, el, |
| "Position from %s : %" GST_TIME_FORMAT, GST_ELEMENT_NAME(el), GST_TIME_ARGS(position)); |
| } |
| return TRUE; |
| }; |
| |
| GstBin *bin = GST_BIN_CAST (element); |
| GstIterator *iter = gst_bin_iterate_sinks (bin); |
| |
| bool keep_going = true; |
| while (keep_going) { |
| GstIteratorResult ires; |
| ires = gst_iterator_fold (iter, fold_func, NULL, GINT_TO_POINTER(level)); |
| switch (ires) { |
| case GST_ITERATOR_RESYNC: |
| gst_iterator_resync (iter); |
| break; |
| default: |
| keep_going = false; |
| break; |
| } |
| } |
| gst_iterator_free (iter); |
| } |
| |
| static void PrintGstCaps(GstCaps* caps) { |
| #ifndef GST_DISABLE_GST_DEBUG |
| if (gst_debug_category_get_threshold(GST_CAT_DEFAULT) >= GST_LEVEL_INFO) { |
| gchar *caps_str = gst_caps_to_string(caps); |
| GST_INFO("caps: %s", caps_str); |
| g_free(caps_str); |
| } |
| #endif |
| } |
| |
| static GstElement* CreatePayloader() { |
| static GstElementFactory* factory = nullptr; |
| static gsize init = 0; |
| |
| if (g_once_init_enter (&init)) { |
| factory = gst_element_factory_find("svppay"); |
| g_once_init_leave (&init, 1); |
| } |
| |
| if (!factory) { |
| GST_DEBUG("svppay not found"); |
| return nullptr; |
| } |
| |
| return gst_element_factory_create(factory, nullptr); |
| } |
| |
| |
| static GstElement* CreateGstElement(const gchar* factory_name, const gchar* name_format, ...) { |
| GstElement *result; |
| gchar *name; |
| va_list args; |
| va_start(args, name_format); |
| name = g_strdup_vprintf(name_format, args); |
| va_end(args); |
| result = gst_element_factory_make(factory_name, name); |
| g_free(name); |
| return result; |
| } |
| |
| } // namespace |
| |
| // ********************************* Player ******************************** // |
| namespace { |
| |
| const int kMaxIvSize = 16; |
| |
| enum class MediaType { |
| kNone = 0, |
| kAudio = 1, |
| kVideo = 2, |
| kBoth = kAudio | kVideo |
| }; |
| |
| struct Task { |
| virtual ~Task() {} |
| virtual void Do() = 0; |
| virtual void PrintInfo() = 0; |
| }; |
| |
| static const char* PlayerStateToStr(SbPlayerState state) { |
| #define CASE(x) case x: return #x |
| switch(state) { |
| CASE(kSbPlayerStateInitialized); |
| CASE(kSbPlayerStatePrerolling); |
| CASE(kSbPlayerStatePresenting); |
| CASE(kSbPlayerStateEndOfStream); |
| CASE(kSbPlayerStateDestroyed); |
| } |
| #undef CASE |
| return "unknown"; |
| } |
| |
| static const char* DecoderStateToStr(SbPlayerDecoderState state) { |
| #define CASE(x) case x: return #x |
| switch(state) { |
| CASE(kSbPlayerDecoderStateNeedsData); |
| } |
| #undef CASE |
| return "unknown"; |
| } |
| |
| class PlayerStatusTask : public Task { |
| public: |
| PlayerStatusTask(SbPlayerStatusFunc func, |
| SbPlayer player, |
| int ticket, |
| void* ctx, |
| SbPlayerState state) { |
| this->func_ = func; |
| this->player_ = player; |
| this->ticket_ = ticket; |
| this->ctx_ = ctx; |
| this->state_ = state; |
| } |
| |
| ~PlayerStatusTask() override {} |
| |
| void Do() override { func_(player_, ctx_, state_, ticket_); } |
| |
| void PrintInfo() override { |
| GST_TRACE("PlayerStatusTask state:%d (%s), ticket:%d", state_, PlayerStateToStr(state_), ticket_); |
| } |
| |
| private: |
| SbPlayerStatusFunc func_; |
| SbPlayer player_; |
| int ticket_; |
| void* ctx_; |
| SbPlayerState state_; |
| }; |
| |
| class PlayerDestroyedTask : public PlayerStatusTask { |
| public: |
| PlayerDestroyedTask(SbPlayerStatusFunc func, |
| SbPlayer player, |
| int ticket, |
| void* ctx, |
| GMainLoop* loop) |
| : PlayerStatusTask(func, player, ticket, ctx, kSbPlayerStateDestroyed) { |
| this->loop_ = loop; |
| } |
| |
| ~PlayerDestroyedTask() override {} |
| |
| void Do() override { |
| PlayerStatusTask::Do(); |
| g_main_loop_quit(loop_); |
| } |
| |
| void PrintInfo() override { |
| GST_TRACE("PlayerDestroyedTask: START"); |
| PlayerStatusTask::PrintInfo(); |
| GST_TRACE("PlayerDestroyedTask: END"); |
| } |
| |
| private: |
| GMainLoop* loop_; |
| }; |
| |
| class DecoderStatusTask : public Task { |
| public: |
| DecoderStatusTask(SbPlayerDecoderStatusFunc func, |
| SbPlayer player, |
| int ticket, |
| void* ctx, |
| SbPlayerDecoderState state, |
| MediaType media) { |
| this->func_ = func; |
| this->player_ = player; |
| this->ticket_ = ticket; |
| this->ctx_ = ctx; |
| this->state_ = state; |
| this->media_ = media; |
| } |
| |
| ~DecoderStatusTask() override {} |
| |
| void Do() override { |
| if ((static_cast<int>(media_) & static_cast<int>(MediaType::kAudio)) != 0) |
| func_(player_, ctx_, kSbMediaTypeAudio, state_, ticket_); |
| if ((static_cast<int>(media_) & static_cast<int>(MediaType::kVideo)) != 0) |
| func_(player_, ctx_, kSbMediaTypeVideo, state_, ticket_); |
| } |
| |
| void PrintInfo() override { |
| GST_TRACE("DecoderStatusTask state:%d (%s), ticket:%d, media:%d", state_, |
| DecoderStateToStr(state_), ticket_, static_cast<int>(media_)); |
| } |
| |
| private: |
| SbPlayerDecoderStatusFunc func_; |
| SbPlayer player_; |
| int ticket_; |
| void* ctx_; |
| SbPlayerDecoderState state_; |
| MediaType media_; |
| }; |
| |
| class PlayerErrorTask : public Task { |
| public: |
| PlayerErrorTask(SbPlayerErrorFunc func, |
| SbPlayer player, |
| void* ctx, |
| SbPlayerError error, |
| const char* msg) { |
| this->func_ = func; |
| this->player_ = player; |
| this->ctx_ = ctx; |
| this->error_ = error; |
| this->msg_ = msg; |
| } |
| |
| ~PlayerErrorTask() override {} |
| |
| void Do() override { func_(player_, ctx_, error_, msg_.c_str()); } |
| |
| void PrintInfo() override { GST_TRACE("PlayerErrorTask"); } |
| |
| private: |
| SbPlayerErrorFunc func_; |
| SbPlayer player_; |
| SbPlayerError error_; |
| void* ctx_; |
| std::string msg_; |
| }; |
| |
| class FunctionTask: public Task { |
| public: |
| FunctionTask(std::function<void()> && fn, const char debug_msg[]) |
| : func_(std::move(fn)) |
| , msg_(debug_msg) { } |
| |
| ~FunctionTask() override {} |
| |
| void Do() override { func_(); } |
| |
| void PrintInfo() override { GST_TRACE("FunctionTask: %s", msg_); } |
| private: |
| std::function<void()> func_; |
| const char* msg_; |
| }; |
| |
| class PlayerImpl : public Player { |
| public: |
| PlayerImpl(SbPlayer player, |
| SbWindow window, |
| SbMediaVideoCodec video_codec, |
| SbMediaAudioCodec audio_codec, |
| SbDrmSystem drm_system, |
| #if SB_API_VERSION >= 15 |
| const SbMediaAudioStreamInfo& audio_info, |
| #else // SB_API_VERSION >= 15 |
| const SbMediaAudioSampleInfo& audio_info, |
| #endif |
| const char* max_video_capabilities, |
| SbPlayerDeallocateSampleFunc sample_deallocate_func, |
| SbPlayerDecoderStatusFunc decoder_status_func, |
| SbPlayerStatusFunc player_status_func, |
| SbPlayerErrorFunc player_error_func, |
| void* context, |
| SbPlayerOutputMode output_mode, |
| SbDecodeTargetGraphicsContextProvider* provider); |
| ~PlayerImpl() override; |
| |
| // Player |
| void MarkEOS(SbMediaType stream_type) override; |
| void WriteSample(SbMediaType sample_type, |
| const SbPlayerSampleInfo* sample_infos, |
| int number_of_sample_infos) override; |
| void SetVolume(double volume) override; |
| void Seek(SbTime seek_to_timestamp, int ticket) override; |
| bool SetRate(double rate) override; |
| #if SB_API_VERSION >= 15 |
| void GetInfo(SbPlayerInfo* info) override; |
| #else // SB_API_VERSION >= 15 |
| void GetInfo(SbPlayerInfo2* info) override; |
| #endif |
| void SetBounds(int zindex, int x, int y, int w, int h) override; |
| |
| GstElement* GetPipeline() const { return pipeline_; } |
| bool IsValid() const { return SbThreadIsValid(playback_thread_); } |
| bool HasMaxVideoCaps() const { return !max_video_capabilities_.empty(); } |
| void AudioConfigurationChanged(); |
| |
| private: |
| enum class State { |
| kNull, |
| kInitial, |
| kInitialPreroll, |
| kPrerollAfterSeek, |
| kPresenting, |
| kEnded, |
| }; |
| |
| static const char* PrivatePlayerStateToStr(State state) { |
| #define CASE(x) case x: return #x |
| switch(state) { |
| CASE(State::kNull); |
| CASE(State::kInitial); |
| CASE(State::kInitialPreroll); |
| CASE(State::kPrerollAfterSeek); |
| CASE(State::kPresenting); |
| } |
| #undef CASE |
| return "unknown"; |
| } |
| |
| enum MediaTimestampIndex { |
| kAudioIndex, |
| kVideoIndex, |
| kMediaNumber, |
| }; |
| |
| struct DispatchData { |
| DispatchData& operator=(DispatchData&) = delete; |
| DispatchData(const DispatchData&) = delete; |
| |
| DispatchData(Task* task, GSource* src) : task_(task), src_(src) { |
| SB_DCHECK(task_ && src_); |
| } |
| |
| ~DispatchData() { |
| delete task_; |
| g_source_unref(src_); |
| } |
| |
| Task* task() const { return task_; } |
| |
| private: |
| Task* task_{nullptr}; |
| GSource* src_{nullptr}; |
| }; |
| |
| class PendingSample { |
| public: |
| PendingSample() = delete; |
| PendingSample& operator=(const PendingSample&) = delete; |
| PendingSample(const PendingSample&) = delete; |
| |
| PendingSample& operator=(PendingSample&& other) { |
| type_ = other.type_; |
| buffer_ = other.buffer_; |
| other.buffer_ = nullptr; |
| serial_ = other.serial_; |
| other.serial_ = 0; |
| timestamp_ = other.timestamp_; |
| other.timestamp_ = GST_CLOCK_TIME_NONE; |
| return *this; |
| } |
| |
| PendingSample(PendingSample&& other) { operator=(std::move(other)); } |
| |
| PendingSample(SbMediaType type, |
| GstBuffer* buffer, |
| uint64_t serial) |
| : type_(type), |
| buffer_(buffer), |
| serial_(serial), |
| timestamp_(GST_BUFFER_TIMESTAMP(buffer_)) { |
| SB_DCHECK(gst_buffer_is_writable(buffer)); |
| } |
| |
| ~PendingSample() { |
| if (buffer_) |
| gst_buffer_unref(buffer_); |
| } |
| |
| SbMediaType Type() const { return type_; } |
| GstClockTime Timestamp() const { return timestamp_; } |
| GstBuffer* CopyBuffer() const { return gst_buffer_copy_deep(buffer_); } |
| GstBuffer* TakeBuffer() { GstBuffer* res = buffer_; buffer_ = nullptr; return res; } |
| uint64_t SerialID() const { return serial_; } |
| |
| private: |
| SbMediaType type_; |
| GstBuffer* buffer_; |
| uint64_t serial_; |
| GstClockTime timestamp_; |
| }; |
| |
| struct PendingBounds { |
| PendingBounds() : x{0}, y{0}, w{0}, h{0} {} |
| PendingBounds(int ix, int iy, int iw, int ih) |
| : x{ix}, y{iy}, w{iw}, h{ih} {} |
| bool IsEmpty() { return w == 0 && h == 0; } |
| int x; |
| int y; |
| int w; |
| int h; |
| }; |
| |
| using PendingSamples = std::vector<PendingSample>; |
| using SamplesPendingKey = std::map<std::string, PendingSamples>; |
| |
| static gboolean BusMessageCallback(GstBus* bus, |
| GstMessage* message, |
| gpointer user_data); |
| static void* ThreadEntryPoint(void* context); |
| static gboolean WorkerTask(gpointer user_data); |
| static gboolean FinishSourceSetup(gpointer user_data); |
| static void AppSrcNeedData(GstAppSrc* src, guint length, gpointer user_data); |
| static void AppSrcEnoughData(GstAppSrc* src, gpointer user_data); |
| static gboolean AppSrcSeekData(GstAppSrc* src, |
| guint64 offset, |
| gpointer user_data); |
| static void SetupSource(GstElement* pipeline, |
| GstElement* source, |
| PlayerImpl* self); |
| static void SetupElement(GstElement* pipeline, |
| GstElement* element, |
| PlayerImpl* self); |
| static void OnVideoBufferUnderflow(PlayerImpl* self); |
| |
| bool ChangePipelineState(GstState state) const; |
| void DispatchOnWorkerThread(Task* task) const; |
| gint64 GetPosition() const; |
| bool WriteSample(SbMediaType sample_type, |
| GstBuffer* buffer, |
| uint64_t serial_id); |
| MediaType GetBothMediaTypeTakingCodecsIntoAccount() const; |
| void RecordTimestamp(SbMediaType type, SbTime timestamp); |
| SbTime MinTimestamp(MediaType* origin) const; |
| |
| void DecoderNeedsData(::starboard::ScopedLock&, MediaType media) const { |
| SB_DCHECK(media != MediaType::kNone); |
| |
| int need_data = static_cast<int>(media) & ~decoder_state_data_; |
| if (need_data == 0) { |
| GST_LOG_OBJECT(pipeline_, "Already sent 'kSbPlayerDecoderStateNeedsData' for media type: %d, ignoring new request", media); |
| return; |
| } |
| if ((eos_data_ & need_data) == need_data) { |
| GST_LOG_OBJECT(pipeline_, "Stream(%d) already ended, ignoring needs data request", need_data); |
| return; |
| } |
| |
| decoder_state_data_ |= need_data; |
| |
| #if 1 |
| if ((need_data & static_cast<int>(MediaType::kAudio)) != 0) |
| decoder_status_func_(player_, context_, kSbMediaTypeAudio, kSbPlayerDecoderStateNeedsData, ticket_); |
| if ((need_data & static_cast<int>(MediaType::kVideo)) != 0) |
| decoder_status_func_(player_, context_, kSbMediaTypeVideo, kSbPlayerDecoderStateNeedsData, ticket_); |
| #else |
| DispatchOnWorkerThread(new DecoderStatusTask( |
| decoder_status_func_, player_, ticket_, context_, |
| kSbPlayerDecoderStateNeedsData, static_cast<MediaType>(need_data))); |
| #endif |
| } |
| |
| gboolean HandleBusMessage(GstBus* bus, GstMessage* message); |
| void HandleApplicationMessage(GstBus* bus, GstMessage* message); |
| void WritePendingSamples(); |
| void CheckBuffering(gint64 position); |
| void ConfigureLimitedVideo(); |
| void SchedulePlayingStateUpdate(); |
| void AddBufferingProbe(GstClockTime target, int ticket); |
| void HandleInititialSeek(::starboard::ScopedLock&); |
| void DidEnd(); |
| |
| SbPlayer player_; |
| SbWindow window_; |
| SbMediaVideoCodec video_codec_; |
| SbMediaAudioCodec audio_codec_; |
| SbDrmSystem drm_system_; |
| std::string max_video_capabilities_; |
| SbPlayerDeallocateSampleFunc sample_deallocate_func_; |
| SbPlayerDecoderStatusFunc decoder_status_func_; |
| SbPlayerStatusFunc player_status_func_; |
| SbPlayerErrorFunc player_error_func_; |
| void* context_{nullptr}; |
| SbPlayerOutputMode output_mode_; |
| SbDecodeTargetGraphicsContextProvider* provider_{nullptr}; |
| GMainLoop* main_loop_{nullptr}; |
| GMainContext* main_loop_context_{nullptr}; |
| GstElement* source_{nullptr}; |
| GstElement* video_appsrc_{nullptr}; |
| GstElement* audio_appsrc_{nullptr}; |
| GstElement* pipeline_{nullptr}; |
| int source_setup_id_{-1}; |
| int bus_watch_id_{-1}; |
| SbThread playback_thread_ { kSbThreadInvalid }; |
| ::starboard::Mutex mutex_; |
| ::starboard::Mutex source_setup_mutex_; |
| ::starboard::Mutex seek_mutex_; |
| double rate_{1.0}; |
| int ticket_{SB_PLAYER_INITIAL_TICKET}; |
| mutable SbTime seek_position_{kSbTimeMax}; |
| SbTime max_sample_timestamps_[kMediaNumber]{0}; |
| SbTime min_sample_timestamp_{kSbTimeMax}; |
| MediaType min_sample_timestamp_origin_{MediaType::kNone}; |
| bool is_seek_pending_{false}; |
| double pending_rate_{.0}; |
| int has_enough_data_{static_cast<int>(MediaType::kBoth)}; |
| mutable int decoder_state_data_{static_cast<int>(MediaType::kNone)}; |
| int eos_data_{static_cast<int>(MediaType::kNone)}; |
| int total_video_frames_{0}; |
| int dropped_video_frames_{0}; |
| int frame_width_{0}; |
| int frame_height_{0}; |
| State state_{State::kNull}; |
| PendingSamples pending_samples_; |
| mutable gint64 cached_position_ns_{-1}; |
| PendingBounds pending_bounds_; |
| SbMediaColorMetadata color_metadata_{}; |
| bool force_stop_ { false }; |
| uint64_t samples_serial_[kMediaNumber] { 0 }; |
| |
| bool has_oob_write_pending_{false}; |
| ::starboard::ConditionVariable pending_oob_write_condition_ { mutex_ }; |
| |
| int hang_monitor_source_id_ { -1 }; |
| HangMonitor hang_monitor_ { "Player" }; |
| GstCaps* audio_caps_ { nullptr }; |
| GstCaps* video_caps_ { nullptr }; |
| SbTime buf_target_min_ts_ { kSbTimeMax }; |
| bool need_instant_rate_change_ { false }; |
| int need_first_segment_ack_ { static_cast<int>(MediaType::kBoth) }; |
| int buffering_state_ { 0 }; |
| mutable int playing_state_update_source_id_{0u}; |
| }; |
| |
| struct PlayerRegistry |
| { |
| ::starboard::Mutex mutex_; |
| std::vector<PlayerImpl*> players_; |
| |
| void Add(PlayerImpl *p) { |
| ::starboard::ScopedLock lock(mutex_); |
| auto it = std::find(players_.begin(), players_.end(), p); |
| if (it == players_.end()) { |
| players_.push_back(p); |
| } |
| } |
| |
| void Remove(PlayerImpl *p) { |
| ::starboard::ScopedLock lock(mutex_); |
| players_.erase(std::remove(players_.begin(), players_.end(), p), players_.end()); |
| } |
| |
| void ForceStop() { |
| std::vector<GstElement*> pipelines; |
| { |
| ::starboard::ScopedLock lock(mutex_); |
| for(const auto& p: players_) { |
| GstElement* pipeline = p->GetPipeline(); |
| if (pipeline) { |
| gst_object_ref(pipeline); |
| pipelines.push_back(pipeline); |
| } |
| } |
| } |
| for (GstElement* pipeline : pipelines) { |
| GstStructure* structure = gst_structure_new_empty("force-stop"); |
| gst_element_post_message(pipeline, gst_message_new_application(GST_OBJECT(pipeline), structure)); |
| gst_object_unref(pipeline); |
| } |
| } |
| |
| bool CanCreate(const char* max_video_capabilities) { |
| #if !defined(COBALT_BUILD_TYPE_GOLD) |
| bool has_max_video_caps_set = (max_video_capabilities && *max_video_capabilities); |
| ::starboard::ScopedLock lock(mutex_); |
| for(const auto& p: players_) { |
| if (p->HasMaxVideoCaps() == has_max_video_caps_set) |
| return false; |
| } |
| #endif |
| return true; |
| } |
| |
| void AudioConfigurationChanged() { |
| ::starboard::ScopedLock lock(mutex_); |
| for(const auto& p: players_) { |
| p->AudioConfigurationChanged(); |
| } |
| } |
| }; |
| SB_ONCE_INITIALIZE_FUNCTION(PlayerRegistry, GetPlayerRegistry); |
| |
| PlayerImpl::PlayerImpl(SbPlayer player, |
| SbWindow window, |
| SbMediaVideoCodec video_codec, |
| SbMediaAudioCodec audio_codec, |
| SbDrmSystem drm_system, |
| #if SB_API_VERSION >= 15 |
| const SbMediaAudioStreamInfo& audio_info, |
| #else // SB_API_VERSION >= 15 |
| const SbMediaAudioSampleInfo& audio_info, |
| #endif |
| const char* max_video_capabilities, |
| SbPlayerDeallocateSampleFunc sample_deallocate_func, |
| SbPlayerDecoderStatusFunc decoder_status_func, |
| SbPlayerStatusFunc player_status_func, |
| SbPlayerErrorFunc player_error_func, |
| void* context, |
| SbPlayerOutputMode output_mode, |
| SbDecodeTargetGraphicsContextProvider* provider) |
| : player_(player), |
| window_(window), |
| video_codec_(video_codec), |
| audio_codec_(audio_codec), |
| drm_system_(drm_system), |
| sample_deallocate_func_(sample_deallocate_func), |
| decoder_status_func_(decoder_status_func), |
| player_status_func_(player_status_func), |
| player_error_func_(player_error_func), |
| context_(context) { |
| |
| GST_DEBUG_CATEGORY_INIT(cobalt_gst_player_debug, "gstplayer", 0, |
| "Cobalt player"); |
| |
| static bool disable_audio = !!getenv("COBALT_DISABLE_AUDIO"); |
| if (disable_audio) |
| audio_codec_ = kSbMediaAudioCodecNone; |
| |
| main_loop_context_ = g_main_context_new (); |
| g_main_context_push_thread_default(main_loop_context_); |
| main_loop_ = g_main_loop_new(main_loop_context_, FALSE); |
| |
| GSource* src = g_timeout_source_new(hang_monitor_.GetResetInterval() / kSbTimeMillisecond); |
| g_source_set_callback(src, [] (gpointer data) ->gboolean { |
| PlayerImpl& player = *static_cast<PlayerImpl*>(data); |
| GstState state, pending; |
| GstStateChangeReturn result = gst_element_get_state(player.pipeline_, &state, &pending, 0); |
| gint64 position = player.GetPosition(); |
| GST_INFO_OBJECT( |
| player.pipeline_, |
| "Player state: %s (pending: %s, result: %s), position: %" GST_TIME_FORMAT "", |
| gst_element_state_get_name(state), |
| gst_element_state_get_name(pending), |
| gst_element_state_change_return_get_name(result), |
| GST_TIME_ARGS(position)); |
| player.hang_monitor_.Reset(); |
| return G_SOURCE_CONTINUE; |
| }, this, nullptr); |
| hang_monitor_source_id_ = g_source_attach(src, main_loop_context_); |
| g_source_unref(src); |
| |
| GST_INFO_OBJECT(pipeline_,"Creating player with max capabilities: '%s'", |
| max_video_capabilities); |
| |
| GstElementFactory* src_factory = gst_element_factory_find("cobaltsrc"); |
| if (!src_factory) { |
| gst_element_register(0, "cobaltsrc", GST_RANK_PRIMARY + 100, |
| GST_COBALT_TYPE_SRC); |
| } else { |
| gst_object_unref(src_factory); |
| } |
| |
| static int player_id = 0; |
| player_id++; |
| |
| pipeline_ = CreateGstElement("playbin", "media-pipeline-%d", player_id); |
| |
| unsigned flagAudio = getGstPlayFlag("audio"); |
| unsigned flagVideo = getGstPlayFlag("video"); |
| unsigned flagNativeVideo = getGstPlayFlag("native-video"); |
| unsigned flagNativeAudio = enableNativeAudio() ? getGstPlayFlag("native-audio") : 0; |
| |
| g_object_set(pipeline_, "flags", |
| flagAudio | flagVideo | flagNativeVideo | flagNativeAudio, |
| nullptr); |
| g_signal_connect(pipeline_, "source-setup", |
| G_CALLBACK(&PlayerImpl::SetupSource), this); |
| g_signal_connect(pipeline_, "element-setup", |
| G_CALLBACK(&PlayerImpl::SetupElement), this); |
| g_object_set(pipeline_, "uri", "cobalt://", nullptr); |
| |
| if (max_video_capabilities && *max_video_capabilities) { |
| max_video_capabilities_ = max_video_capabilities; |
| ConfigureLimitedVideo(); |
| } |
| |
| if (audio_codec_ == kSbMediaAudioCodecNone) { |
| has_enough_data_ &= ~static_cast<int>(MediaType::kAudio); |
| } |
| |
| if (video_codec_ == kSbMediaVideoCodecNone) { |
| has_enough_data_ &= ~static_cast<int>(MediaType::kVideo); |
| } |
| |
| need_first_segment_ack_ = has_enough_data_; |
| |
| GstBus* bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline_)); |
| bus_watch_id_ = gst_bus_add_watch(bus, &PlayerImpl::BusMessageCallback, this); |
| gst_object_unref(bus); |
| |
| video_appsrc_ = CreateGstElement("appsrc", "vidsrc-%d", player_id); |
| audio_appsrc_ = CreateGstElement("appsrc", "audsrc-%d", player_id); |
| |
| GstElement* playsink = (gst_bin_get_by_name(GST_BIN(pipeline_), "playsink")); |
| if (playsink) { |
| g_object_set(G_OBJECT(playsink), "send-event-mode", 0, nullptr); |
| g_object_unref(playsink); |
| } else { |
| GST_WARNING_OBJECT(pipeline_, "No playsink ?!?!?"); |
| } |
| |
| if (drm_system_) { |
| GstContext* context = gst_context_new("cobalt-drm-system", FALSE); |
| GstStructure* context_structure = gst_context_writable_structure(context); |
| gst_structure_set(context_structure, "drm-system-instance", G_TYPE_POINTER, drm_system_, nullptr); |
| gst_element_set_context(GST_ELEMENT(pipeline_), context); |
| gst_context_unref(context); |
| } |
| |
| ChangePipelineState(GST_STATE_READY); |
| g_main_context_pop_thread_default(main_loop_context_); |
| |
| if (gst_element_get_state(pipeline_, nullptr, nullptr, 0) == GST_STATE_CHANGE_FAILURE) |
| return; |
| |
| playback_thread_ = |
| SbThreadCreate(0, kSbThreadPriorityRealTime, kSbThreadNoAffinity, true, |
| "playback_thread", &PlayerImpl::ThreadEntryPoint, this); |
| if (SbThreadIsValid(playback_thread_)) { |
| while(!g_main_loop_is_running(main_loop_)) |
| g_usleep(1); |
| } |
| GetPlayerRegistry()->Add(this); |
| } |
| |
| PlayerImpl::~PlayerImpl() { |
| GetPlayerRegistry()->Remove(this); |
| |
| GST_INFO_OBJECT(pipeline_, "Destroying player"); |
| { |
| ::starboard::ScopedLock lock(source_setup_mutex_); |
| if (source_setup_id_ > -1) { |
| GSource* src = g_main_context_find_source_by_id(main_loop_context_, source_setup_id_); |
| g_source_destroy(src); |
| } |
| } |
| if (bus_watch_id_ > -1) { |
| GSource* src = g_main_context_find_source_by_id(main_loop_context_, bus_watch_id_); |
| g_source_destroy(src); |
| } |
| if (hang_monitor_source_id_ > -1) { |
| GSource* src = g_main_context_find_source_by_id(main_loop_context_, hang_monitor_source_id_); |
| g_source_destroy(src); |
| hang_monitor_.Reset(); |
| } |
| ChangePipelineState(GST_STATE_NULL); |
| GstBus* bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline_)); |
| gst_bus_set_sync_handler(bus, nullptr, nullptr, nullptr); |
| gst_object_unref(bus); |
| if (SbThreadIsValid(playback_thread_)) { |
| DispatchOnWorkerThread(new PlayerDestroyedTask( |
| player_status_func_, player_, ticket_, context_, main_loop_)); |
| SbThreadJoin(playback_thread_, nullptr); |
| } |
| if (audio_caps_) { |
| gst_caps_unref(audio_caps_); |
| } |
| if (video_caps_) { |
| gst_caps_unref(video_caps_); |
| } |
| g_main_loop_unref(main_loop_); |
| g_main_context_unref(main_loop_context_); |
| GST_INFO_OBJECT(pipeline_, "BYE BYE player"); |
| g_object_unref(pipeline_); |
| } |
| |
| // static |
| gboolean PlayerImpl::BusMessageCallback(GstBus* bus, |
| GstMessage* message, |
| gpointer user_data) { |
| PlayerImpl* self = static_cast<PlayerImpl*>(user_data); |
| return self->HandleBusMessage(bus, message); |
| } |
| |
| gboolean PlayerImpl::HandleBusMessage(GstBus* bus, GstMessage* message) { |
| GST_TRACE("%d", SbThreadGetId()); |
| GST_LOG_OBJECT(pipeline_, "Got GST message '%s' from '%s'", GST_MESSAGE_TYPE_NAME(message), GST_MESSAGE_SRC_NAME(message)); |
| |
| switch (GST_MESSAGE_TYPE(message)) { |
| case GST_MESSAGE_APPLICATION: { |
| HandleApplicationMessage(bus, message); |
| break; |
| } |
| |
| case GST_MESSAGE_EOS: |
| if (GST_MESSAGE_SRC(message) == GST_OBJECT(pipeline_)) { |
| GST_INFO_OBJECT(pipeline_, "EOS"); |
| DidEnd(); |
| } |
| break; |
| |
| case GST_MESSAGE_ERROR: { |
| GError* err = nullptr; |
| gchar* debug = nullptr; |
| gst_message_parse_error(message, &err, &debug); |
| |
| std::string file_name = "cobalt_"; |
| file_name += (GST_OBJECT_NAME(pipeline_)); |
| file_name += "_err_"; |
| file_name += std::to_string(err->code); |
| GST_DEBUG_BIN_TO_DOT_FILE_WITH_TS(GST_BIN(pipeline_), |
| GST_DEBUG_GRAPH_SHOW_ALL, |
| file_name.c_str()); |
| |
| |
| bool is_eos = (eos_data_ == (int)GetBothMediaTypeTakingCodecsIntoAccount()); |
| if (err->domain == GST_STREAM_ERROR && is_eos) { |
| GST_WARNING_OBJECT(pipeline_, "Got stream error. But all streams are ended, so reporting EOS. Error code %d: %s (%s).", |
| err->code, err->message, debug); |
| DidEnd(); |
| } else { |
| GST_ERROR_OBJECT(pipeline_, "Error %d: %s (%s)", err->code, err->message, debug); |
| DispatchOnWorkerThread(new PlayerErrorTask( |
| player_error_func_, player_, context_, |
| kSbPlayerErrorDecode, err->message)); |
| } |
| g_free(debug); |
| g_error_free(err); |
| break; |
| } |
| |
| case GST_MESSAGE_STATE_CHANGED: { |
| if (GST_MESSAGE_SRC(message) == GST_OBJECT(pipeline_)) { |
| GstState old_state, new_state, pending; |
| gst_message_parse_state_changed(message, &old_state, &new_state, |
| &pending); |
| GST_INFO_OBJECT(GST_MESSAGE_SRC(message), |
| "===> State changed (old: %s, new: %s, pending: %s)", |
| gst_element_state_get_name(old_state), |
| gst_element_state_get_name(new_state), |
| gst_element_state_get_name(pending)); |
| std::string file_name = "cobalt_"; |
| file_name += (GST_OBJECT_NAME(pipeline_)); |
| file_name += "_"; |
| file_name += gst_element_state_get_name(old_state); |
| file_name += "_"; |
| file_name += gst_element_state_get_name(new_state); |
| GST_DEBUG_BIN_TO_DOT_FILE_WITH_TS(GST_BIN(pipeline_), |
| GST_DEBUG_GRAPH_SHOW_ALL, |
| file_name.c_str()); |
| |
| if (GST_STATE(pipeline_) >= GST_STATE_PAUSED) { |
| int ticket = 0; |
| bool is_seek_pending = false; |
| bool is_rate_pending = false; |
| double rate = 0.; |
| SbTime pending_seek_pos = kSbTimeMax; |
| |
| { |
| ::starboard::ScopedLock lock(mutex_); |
| ticket = ticket_; |
| is_seek_pending = is_seek_pending_; |
| is_rate_pending = pending_rate_ != .0; |
| pending_seek_pos = seek_position_; |
| SB_DCHECK(!is_seek_pending || seek_position_ != kSbTimeMax); |
| rate = pending_rate_; |
| if (is_seek_pending && is_rate_pending) { |
| is_rate_pending = false; |
| rate_ = rate; |
| pending_rate_ = .0; |
| } |
| if (state_ == State::kPrerollAfterSeek || |
| state_ == State::kInitialPreroll) { |
| has_oob_write_pending_ |= is_seek_pending; |
| } |
| } |
| |
| if (video_codec_ != kSbMediaVideoCodecNone && !pending_bounds_.IsEmpty()) { |
| PendingBounds bounds = pending_bounds_; |
| pending_bounds_ = {}; |
| SetBounds(0, bounds.x, bounds.y, bounds.w, bounds.h); |
| } |
| |
| if (is_rate_pending && GST_STATE(pipeline_) == GST_STATE_PLAYING) { |
| GST_INFO_OBJECT(pipeline_,"Sending pending SetRate(rate=%lf)", rate); |
| SetRate(rate); |
| } else if (is_seek_pending) { |
| GST_INFO_OBJECT(pipeline_, "Sending pending Seek(position=%" PRId64 ", ticket=%d)", pending_seek_pos, ticket); |
| Seek(pending_seek_pos, ticket); |
| } |
| } |
| } |
| } break; |
| |
| case GST_MESSAGE_ASYNC_DONE: { |
| if (GST_MESSAGE_SRC(message) == GST_OBJECT(pipeline_)) { |
| GST_INFO_OBJECT(pipeline_, "===> ASYNC-DONE, pipeline state: %s, player state: %s", |
| gst_element_state_get_name(GST_STATE(pipeline_)), |
| PrivatePlayerStateToStr(state_)); |
| |
| ::starboard::Mutex &mutex = mutex_; |
| ::starboard::ScopedLock lock(mutex); |
| |
| if (state_ == State::kPrerollAfterSeek || |
| state_ == State::kInitialPreroll) { |
| |
| bool is_seek_pending = is_seek_pending_; |
| bool has_pending_samples = (pending_samples_.empty() == false) || has_oob_write_pending_; |
| |
| if (!is_seek_pending && has_pending_samples) { |
| |
| int prev_has_data = static_cast<int>(has_enough_data_); |
| has_enough_data_ = static_cast<int>(MediaType::kBoth); |
| |
| mutex.Release(); |
| GST_INFO_OBJECT(pipeline_, "===> Writing pending samples"); |
| WritePendingSamples(); |
| mutex.Acquire(); |
| |
| if (has_enough_data_ == static_cast<int>(MediaType::kBoth)) |
| has_enough_data_ = prev_has_data; |
| has_oob_write_pending_ = false; |
| pending_oob_write_condition_.Broadcast(); |
| } |
| GST_INFO_OBJECT(pipeline_, "===> Asuming preroll done"); |
| |
| // The below code is good but on BRCM the decoder reports old |
| // position for some time which makes some YTLB 2020 test failing. |
| // seek_position_ = kSbTimeMax; |
| DispatchOnWorkerThread(new PlayerStatusTask( |
| player_status_func_, player_, ticket_, |
| context_, kSbPlayerStatePresenting)); |
| state_ = State::kPresenting; |
| } |
| |
| SchedulePlayingStateUpdate(); |
| } |
| } break; |
| |
| case GST_MESSAGE_CLOCK_LOST: |
| ChangePipelineState(GST_STATE_PAUSED); |
| ChangePipelineState(GST_STATE_PLAYING); |
| break; |
| |
| case GST_MESSAGE_LATENCY: |
| gst_bin_recalculate_latency(GST_BIN(pipeline_)); |
| break; |
| |
| case GST_MESSAGE_QOS: { |
| const gchar *klass; |
| klass = gst_element_class_get_metadata ( |
| GST_ELEMENT_GET_CLASS (GST_MESSAGE_SRC(message)), |
| GST_ELEMENT_METADATA_KLASS); |
| if (g_strrstr(klass, "Video")) { |
| GstFormat format; |
| guint64 dropped = 0, processed = 0; |
| GstDebugLevel log_level = GST_LEVEL_DEBUG; |
| gst_message_parse_qos_stats(message, &format, &processed, &dropped); |
| if (format == GST_FORMAT_BUFFERS) { |
| ::starboard::ScopedLock lock(mutex_); |
| if (dropped_video_frames_ != static_cast<int>(dropped)) { |
| log_level = GST_LEVEL_INFO; |
| dropped_video_frames_ = static_cast<int>(dropped); |
| } |
| } |
| GST_CAT_LEVEL_LOG ( |
| GST_CAT_DEFAULT, log_level, pipeline_, |
| "QOS written = %d, processed = %" G_GUINT64_FORMAT ", dropped = %" G_GUINT64_FORMAT, |
| total_video_frames_, processed, dropped); |
| } |
| } break; |
| |
| default: |
| break; |
| } |
| |
| return TRUE; |
| } |
| |
| // static |
| void* PlayerImpl::ThreadEntryPoint(void* context) { |
| SB_DCHECK(context); |
| GST_TRACE("%d", SbThreadGetId()); |
| |
| PlayerImpl* self = reinterpret_cast<PlayerImpl*>(context); |
| self->state_ = State::kInitial; |
| |
| g_main_context_push_thread_default(self->main_loop_context_); |
| |
| self->hang_monitor_.Reset(); |
| |
| self->DispatchOnWorkerThread(new PlayerStatusTask( |
| self->player_status_func_, self->player_, self->ticket_, self->context_, |
| kSbPlayerStateInitialized)); |
| g_main_loop_run(self->main_loop_); |
| |
| return nullptr; |
| } |
| |
| void PlayerImpl::DispatchOnWorkerThread(Task* task) const { |
| GSource* src = g_source_new(&SourceFunctions, sizeof(GSource)); |
| g_source_set_ready_time(src, 0); |
| DispatchData* data = new DispatchData(task, src); |
| g_source_set_callback(src, |
| [](gpointer userData) -> gboolean { |
| DispatchData* data = |
| static_cast<DispatchData*>(userData); |
| GST_TRACE("%d", SbThreadGetId()); |
| data->task()->PrintInfo(); |
| data->task()->Do(); |
| return G_SOURCE_REMOVE; |
| }, |
| data, |
| [](gpointer userData) { |
| delete static_cast<DispatchData*>(userData); |
| }); |
| g_source_attach(src, main_loop_context_); |
| } |
| |
| // static |
| gboolean PlayerImpl::FinishSourceSetup(gpointer user_data) { |
| PlayerImpl* self = static_cast<PlayerImpl*>(user_data); |
| ::starboard::ScopedLock lock(self->source_setup_mutex_); |
| SB_DCHECK(self->source_); |
| bool has_drm_system = !!self->drm_system_; |
| GstElement* source = self->source_; |
| GstAppSrcCallbacks callbacks = {&PlayerImpl::AppSrcNeedData, |
| &PlayerImpl::AppSrcEnoughData, |
| &PlayerImpl::AppSrcSeekData, nullptr}; |
| if (self->audio_codec_ != kSbMediaAudioCodecNone) { |
| gst_cobalt_src_setup_and_add_app_src(kSbMediaTypeAudio, |
| source, self->audio_appsrc_, |
| &callbacks, self, has_drm_system); |
| } |
| if (self->video_codec_ != kSbMediaVideoCodecNone) { |
| gst_cobalt_src_setup_and_add_app_src(kSbMediaTypeVideo, |
| source, self->video_appsrc_, |
| &callbacks, self, has_drm_system); |
| } |
| gst_cobalt_src_all_app_srcs_added(self->source_); |
| self->source_setup_id_ = -1; |
| |
| return FALSE; |
| } |
| |
| // static |
| void PlayerImpl::AppSrcNeedData(GstAppSrc* src, |
| guint length, |
| gpointer user_data) { |
| PlayerImpl* self = static_cast<PlayerImpl*>(user_data); |
| |
| GST_LOG_OBJECT(src, "===> Gimme more data"); |
| |
| ::starboard::ScopedLock lock(self->mutex_); |
| int need_data = static_cast<int>(MediaType::kNone); |
| SB_DCHECK(src == GST_APP_SRC(self->video_appsrc_) || |
| src == GST_APP_SRC(self->audio_appsrc_)); |
| if (src == GST_APP_SRC(self->video_appsrc_)) { |
| self->has_enough_data_ &= ~static_cast<int>(MediaType::kVideo); |
| need_data |= static_cast<int>(MediaType::kVideo); |
| } else if (src == GST_APP_SRC(self->audio_appsrc_)) { |
| self->has_enough_data_ &= ~static_cast<int>(MediaType::kAudio); |
| need_data |= static_cast<int>(MediaType::kAudio); |
| } |
| |
| if (self->state_ == State::kPrerollAfterSeek) { |
| if (self->has_enough_data_ != static_cast<int>(MediaType::kNone)) { |
| GST_LOG_OBJECT(src, "Seeking. Waiting for other appsrcs."); |
| return; |
| } |
| |
| need_data = |
| static_cast<int>(self->GetBothMediaTypeTakingCodecsIntoAccount()); |
| } |
| |
| GST_LOG_OBJECT(src, "===> Really. Gimme more data need:%d", need_data); |
| self->DecoderNeedsData(lock, static_cast<MediaType>(need_data)); |
| } |
| |
| // static |
| void PlayerImpl::AppSrcEnoughData(GstAppSrc* src, gpointer user_data) { |
| PlayerImpl* self = static_cast<PlayerImpl*>(user_data); |
| |
| ::starboard::ScopedLock lock(self->mutex_); |
| |
| if (src == GST_APP_SRC(self->video_appsrc_)) |
| self->has_enough_data_ |= static_cast<int>(MediaType::kVideo); |
| else if (src == GST_APP_SRC(self->audio_appsrc_)) |
| self->has_enough_data_ |= static_cast<int>(MediaType::kAudio); |
| |
| GST_DEBUG_OBJECT(src, "===> Enough is enough (enough:%d)", |
| self->has_enough_data_); |
| } |
| |
| // static |
| gboolean PlayerImpl::AppSrcSeekData(GstAppSrc* src, |
| guint64 offset, |
| gpointer user_data) { |
| PlayerImpl* self = static_cast<PlayerImpl*>(user_data); |
| GST_DEBUG_OBJECT(src, "===> Seek on appsrc %" PRId64, offset); |
| |
| { |
| ::starboard::ScopedLock lock(self->mutex_); |
| if (self->state_ != State::kPrerollAfterSeek) { |
| GST_DEBUG_OBJECT(src, "Not seeking"); |
| return TRUE; |
| } |
| } |
| |
| PlayerImpl::AppSrcEnoughData(src, user_data); |
| return TRUE; |
| } |
| |
| // static |
| void PlayerImpl::SetupSource(GstElement* pipeline, |
| GstElement* source, |
| PlayerImpl* self) { |
| ::starboard::ScopedLock lock(self->source_setup_mutex_); |
| if (self->source_) |
| return; |
| self->source_ = source; |
| static constexpr int kAsyncSourceFinishTimeMs = 50; |
| GSource* src = g_timeout_source_new(kAsyncSourceFinishTimeMs); |
| g_source_set_callback(src, &PlayerImpl::FinishSourceSetup, self, nullptr); |
| self->source_setup_id_ = g_source_attach(src, self->main_loop_context_); |
| g_source_unref(src); |
| } |
| |
| // static |
| void PlayerImpl::OnVideoBufferUnderflow(PlayerImpl* self) |
| { |
| GST_WARNING_OBJECT(self->pipeline_, |
| "Decoder need data state = 0x%x," |
| " video appsrc level = %lld kb," |
| " audio appsrc level = %lld kb", |
| self->decoder_state_data_, |
| gst_app_src_get_current_level_bytes(GST_APP_SRC(self->video_appsrc_)) / 1024, |
| gst_app_src_get_current_level_bytes(GST_APP_SRC(self->audio_appsrc_)) / 1024); |
| } |
| |
| // static |
| void PlayerImpl::SetupElement(GstElement* pipeline, |
| GstElement* element, |
| PlayerImpl* self) { |
| if (GST_IS_BASE_SINK(element)) { |
| static bool disable_wait_video = !!getenv("COBALT_AML_DISABLE_WAIT_VIDEO"); |
| bool has_video = (self->video_codec_ != kSbMediaVideoCodecNone); |
| if (has_video && g_str_has_prefix(GST_ELEMENT_NAME(element), "amlhalasink") && !disable_wait_video) { |
| g_object_set(element, "wait-video", TRUE, "a-wait-timeout", 4000, nullptr); |
| } |
| else |
| if (has_video && g_str_has_prefix(GST_ELEMENT_NAME(element), "westerossink")) { |
| if (g_object_class_find_property(G_OBJECT_GET_CLASS(element), "zoom-mode")) { |
| GST_INFO_OBJECT(pipeline, "Setting westerossink zoom-mode to 0"); |
| g_object_set(element, "zoom-mode", 0, nullptr); |
| } |
| g_signal_connect_swapped( |
| G_OBJECT(element), "buffer-underflow-callback", |
| G_CALLBACK(OnVideoBufferUnderflow), self); |
| } |
| else |
| if (g_str_has_prefix(GST_ELEMENT_NAME(element), "brcmaudiosink")) { |
| g_object_set(G_OBJECT(element), "async", TRUE, nullptr); |
| } |
| } |
| |
| if (GST_IS_BASE_PARSE(element)) { |
| gst_base_parse_set_pts_interpolation(GST_BASE_PARSE(element), FALSE); |
| } |
| |
| const gchar *klass_str = gst_element_class_get_metadata(GST_ELEMENT_GET_CLASS(element), "klass"); |
| if (strstr(klass_str, "Sink")) { |
| GObjectClass *oclass = G_OBJECT_GET_CLASS(element); |
| |
| if (strstr(klass_str, "Video")) { |
| if (!self->max_video_capabilities_.empty() && |
| g_object_class_find_property(oclass, "maxVideoWidth") && |
| g_object_class_find_property(oclass, "maxVideoHeight")) { |
| uint32_t width = 0, height = 0; |
| ParseMaxVideoCapabilities(self->max_video_capabilities_.c_str(), &width, &height, nullptr); |
| g_object_set(G_OBJECT(element), "maxVideoWidth", width, "maxVideoHeight", height, nullptr); |
| } |
| } |
| |
| if (g_object_class_find_property(oclass, "has-drm")) { |
| g_object_set(G_OBJECT(element), "has-drm", self->drm_system_ != nullptr, nullptr); |
| } |
| } |
| } |
| |
| void PlayerImpl::MarkEOS(SbMediaType stream_type) { |
| GstElement* src = nullptr; |
| if (stream_type == kSbMediaTypeVideo) { |
| src = video_appsrc_; |
| } else { |
| src = audio_appsrc_; |
| } |
| |
| GST_INFO_OBJECT(src, "===> %d", SbThreadGetId()); |
| ::starboard::ScopedLock lock(mutex_); |
| if (state_ == State::kPrerollAfterSeek) |
| GST_DEBUG_OBJECT(src, "===> Mark EOS with State::kPrerollAfterSeek"); |
| |
| if (stream_type == kSbMediaTypeVideo) |
| eos_data_ |= static_cast<int>(MediaType::kVideo); |
| else |
| eos_data_ |= static_cast<int>(MediaType::kAudio); |
| |
| if (eos_data_ == static_cast<int>(GetBothMediaTypeTakingCodecsIntoAccount())) { |
| if (audio_codec_ != kSbMediaAudioCodecNone) |
| gst_app_src_end_of_stream(GST_APP_SRC(audio_appsrc_)); |
| if (video_codec_ != kSbMediaVideoCodecNone) |
| gst_app_src_end_of_stream(GST_APP_SRC(video_appsrc_)); |
| } |
| } |
| |
| bool PlayerImpl::WriteSample(SbMediaType sample_type, GstBuffer* buffer, uint64_t serial_id) { |
| GstElement* src = nullptr; |
| if (sample_type == kSbMediaTypeVideo) { |
| src = video_appsrc_; |
| } else { |
| src = audio_appsrc_; |
| } |
| |
| GstDebugLevel log_level = GST_LEVEL_TRACE; |
| { |
| ::starboard::ScopedLock lock(mutex_); |
| if (sample_type == kSbMediaTypeVideo) |
| decoder_state_data_ &= ~static_cast<int>(MediaType::kVideo); |
| else |
| decoder_state_data_ &= ~static_cast<int>(MediaType::kAudio); |
| |
| if (state_ < State::kPresenting) |
| log_level = GST_LEVEL_DEBUG; |
| } |
| |
| GST_CAT_LEVEL_LOG ( |
| GST_CAT_DEFAULT, log_level, src, |
| "SampleType:%d %" GST_TIME_FORMAT " id:%" PRIu64 " b:%p", |
| sample_type, GST_TIME_ARGS(GST_BUFFER_TIMESTAMP(buffer)), serial_id, buffer); |
| |
| gst_app_src_push_buffer(GST_APP_SRC(src), buffer); |
| |
| ::starboard::ScopedLock lock(mutex_); |
| // Wait for need-data to trigger instead. |
| if (state_ == State::kInitial || state_ == State::kInitialPreroll) |
| return true; |
| |
| MediaType media = sample_type == kSbMediaTypeVideo |
| ? MediaType::kVideo |
| : MediaType::kAudio; |
| |
| bool has_enough = |
| (sample_type == kSbMediaTypeVideo && |
| (has_enough_data_ & static_cast<int>(MediaType::kVideo)) != 0) || |
| (sample_type == kSbMediaTypeAudio && |
| (has_enough_data_ & static_cast<int>(MediaType::kAudio)) != 0); |
| |
| bool force_buf = has_enough && |
| (buf_target_min_ts_ != kSbTimeMax && |
| min_sample_timestamp_origin_ == media); |
| |
| if (!has_enough || force_buf) { |
| GST_LOG_OBJECT(src, "Asking for more (forced buffering? %s)", force_buf ? "yes" : "no"); |
| DecoderNeedsData(lock, media); |
| } else { |
| GST_LOG_OBJECT(src, "Has enough data"); |
| } |
| |
| return true; |
| } |
| |
| void PlayerImpl::WriteSample(SbMediaType sample_type, |
| const SbPlayerSampleInfo* sample_infos, |
| int number_of_sample_infos) { |
| static_assert(kMaxNumberOfSamplesPerWrite == 1, |
| "Adjust impl. to handle more samples after changing samples" |
| "count"); |
| SB_DCHECK(number_of_sample_infos == kMaxNumberOfSamplesPerWrite); |
| // For debuggin purposes it could be usefull to disable audio or video |
| // in this case just drop the sample |
| if (audio_codec_ == kSbMediaAudioCodecNone && sample_type == kSbMediaTypeAudio) { |
| sample_deallocate_func_(player_, context_, sample_infos[0].buffer); |
| return; |
| } |
| if (video_codec_ == kSbMediaVideoCodecNone && sample_type == kSbMediaTypeVideo) { |
| sample_deallocate_func_(player_, context_, sample_infos[0].buffer); |
| return; |
| } |
| |
| #if defined(SB_RDK_ZERO_COPY_SAMPLE_WRITE) && SB_RDK_ZERO_COPY_SAMPLE_WRITE |
| using BufferInfo = std::tuple<SbPlayerDeallocateSampleFunc, SbPlayer, void*, const void*>; |
| GstBuffer* buffer = |
| gst_buffer_new_wrapped_full( |
| static_cast<GstMemoryFlags>(0), |
| const_cast<gpointer> (sample_infos[0].buffer), |
| sample_infos[0].buffer_size, |
| 0, |
| sample_infos[0].buffer_size, |
| new BufferInfo(sample_deallocate_func_, player_, context_, sample_infos[0].buffer), |
| [](gpointer data) { |
| BufferInfo &info = *reinterpret_cast<BufferInfo*>(data); |
| auto deallocate_func = std::get<0>(info); |
| auto player = std::get<1>(info); |
| auto* context = std::get<2>(info); |
| const auto* buffer = std::get<3>(info); |
| deallocate_func(player, context, buffer); |
| delete &info; |
| }); |
| #else |
| GstBuffer* buffer = |
| gst_buffer_new_allocate(nullptr, sample_infos[0].buffer_size, nullptr); |
| gsize sz = gst_buffer_fill(buffer, 0, sample_infos[0].buffer, sample_infos[0].buffer_size); |
| SB_DCHECK(sz == sample_infos[0].buffer_size); |
| sample_deallocate_func_(player_, context_, sample_infos[0].buffer); |
| #endif |
| |
| GstClockTime timestamp = sample_infos[0].timestamp * kSbTimeNanosecondsPerMicrosecond; |
| GST_BUFFER_TIMESTAMP(buffer) = timestamp; |
| |
| if (sample_infos[0].type == kSbMediaTypeVideo) { |
| #if SB_API_VERSION >= 15 |
| const auto& info = sample_infos[0].video_sample_info.stream_info; |
| #else |
| const auto& info = sample_infos[0].video_sample_info; |
| #endif |
| if (frame_width_ != info.frame_width || |
| frame_height_ != info.frame_height || |
| CompareColorMetadata(color_metadata_, info.color_metadata) != 0) { |
| frame_width_ = info.frame_width; |
| frame_height_ = info.frame_height; |
| color_metadata_ = info.color_metadata; |
| auto caps = CodecToGstCaps(video_codec_); |
| if (!caps.empty()) { |
| GstCaps* gst_caps = gst_caps_from_string(caps[0].c_str()); |
| AddVideoInfoToGstCaps(info, gst_caps); |
| PrintGstCaps(gst_caps); |
| gst_app_src_set_caps(GST_APP_SRC(video_appsrc_), gst_caps); |
| gst_caps_replace(&video_caps_, gst_caps); |
| gst_caps_unref(gst_caps); |
| } |
| } |
| if (!sample_infos[0].video_sample_info.is_key_frame) { |
| GST_BUFFER_FLAG_SET (buffer, GST_BUFFER_FLAG_DELTA_UNIT); |
| } |
| } |
| else if (sample_infos[0].type == kSbMediaTypeAudio) { |
| SB_DCHECK (sample_infos[0].type == kSbMediaTypeAudio); |
| SB_DCHECK (audio_codec_ != kSbMediaAudioCodecNone); |
| |
| if ( audio_caps_ == nullptr ) { |
| #if SB_API_VERSION >= 15 |
| const auto& audio_info = sample_infos[0].audio_sample_info.stream_info; |
| #else |
| const auto& audio_info = sample_infos[0].audio_sample_info; |
| #endif |
| auto caps = CodecToGstCaps(audio_codec_, &audio_info); |
| if (!caps.empty() && caps[0].c_str()) { |
| GstCaps* gst_caps = gst_caps_from_string(caps[0].c_str()); |
| PrintGstCaps(gst_caps); |
| gst_app_src_set_caps(GST_APP_SRC(audio_appsrc_), gst_caps); |
| gst_caps_replace(&audio_caps_, gst_caps); |
| gst_caps_unref(gst_caps); |
| } |
| } |
| |
| #if SB_API_VERSION >= 15 |
| const guint64 kMaxGstClockTime = G_MAXUINT64 / G_GUINT64_CONSTANT (2); |
| const auto& info = sample_infos[0].audio_sample_info; |
| guint64 start_clip = 0, end_clip = 0; |
| |
| if (info.discarded_duration_from_front) |
| start_clip = (info.discarded_duration_from_front == kSbTimeMax) |
| ? kMaxGstClockTime : info.discarded_duration_from_front * kSbTimeNanosecondsPerMicrosecond; |
| |
| if (info.discarded_duration_from_back) |
| end_clip = (info.discarded_duration_from_back == kSbTimeMax) |
| ? kMaxGstClockTime : info.discarded_duration_from_back * kSbTimeNanosecondsPerMicrosecond; |
| |
| if (start_clip || end_clip) { |
| gst_buffer_add_audio_clipping_meta(buffer, GST_FORMAT_TIME, start_clip, end_clip); |
| GST_DEBUG_OBJECT(pipeline_, "Add audio clipping, start: %" PRIu64 ", end %" PRIu64, start_clip, end_clip); |
| } |
| #endif |
| } |
| |
| RecordTimestamp(sample_type, timestamp); |
| |
| if (sample_infos[0].drm_info) { |
| GST_LOG_OBJECT(pipeline_, "Encounterd encrypted %s sample", |
| sample_type == kSbMediaTypeVideo ? "video" : "audio"); |
| SB_DCHECK(drm_system_); |
| |
| GstBuffer* subsamples = nullptr; |
| GstBuffer* iv = nullptr; |
| GstBuffer* key = nullptr; |
| uint32_t subsamples_count = 0u; |
| uint32_t iv_size = 0u; |
| |
| const int8_t kEmptyArray[kMaxIvSize / 2] = {0}; |
| const auto encryption_scheme = sample_infos[0].drm_info->encryption_scheme; |
| const char* cipher_mode = |
| (encryption_scheme == kSbDrmEncryptionSchemeAesCtr) ? "cenc" : |
| (encryption_scheme == kSbDrmEncryptionSchemeAesCbc ? "cbcs" : "unknown"); |
| |
| GST_LOG_OBJECT(pipeline_, "Encryption cipher-mode: %s", cipher_mode); |
| |
| key = gst_buffer_new_allocate( |
| nullptr, sample_infos[0].drm_info->identifier_size, nullptr); |
| gst_buffer_fill(key, 0, sample_infos[0].drm_info->identifier, |
| sample_infos[0].drm_info->identifier_size); |
| |
| iv_size = sample_infos[0].drm_info->initialization_vector_size; |
| if (iv_size == kMaxIvSize && |
| memcmp(sample_infos[0].drm_info->initialization_vector + kMaxIvSize / 2, |
| kEmptyArray, kMaxIvSize / 2) == 0) { |
| iv_size /= 2; |
| } |
| iv = gst_buffer_new_allocate(nullptr, iv_size, nullptr); |
| gst_buffer_fill(iv, 0, sample_infos[0].drm_info->initialization_vector, iv_size); |
| |
| subsamples_count = sample_infos[0].drm_info->subsample_count; |
| if (subsamples_count) { |
| auto subsamples_raw_size = |
| subsamples_count * (sizeof(guint16) + sizeof(guint32)); |
| guint8* subsamples_raw = |
| static_cast<guint8*>(g_malloc(subsamples_raw_size)); |
| GstByteWriter writer; |
| gst_byte_writer_init_with_data(&writer, subsamples_raw, subsamples_raw_size, FALSE); |
| for (uint32_t i = 0; i < subsamples_count; ++i) { |
| if (!gst_byte_writer_put_uint16_be( |
| &writer, |
| sample_infos[0].drm_info->subsample_mapping[i].clear_byte_count)) |
| GST_ERROR_OBJECT(pipeline_, "Failed writing clear subsample info at %d", i); |
| if (!gst_byte_writer_put_uint32_be(&writer, |
| sample_infos[0] |
| .drm_info->subsample_mapping[i] |
| .encrypted_byte_count)) |
| GST_ERROR_OBJECT(pipeline_, "Failed writing encrypted subsample info at %d", i); |
| } |
| subsamples = gst_buffer_new_wrapped(subsamples_raw, subsamples_raw_size); |
| } |
| |
| GstStructure* info = gst_structure_new ( |
| "application/x-cenc", |
| "encrypted", G_TYPE_BOOLEAN, TRUE, |
| "kid", GST_TYPE_BUFFER, key, |
| "iv_size", G_TYPE_UINT, iv_size, |
| "iv", GST_TYPE_BUFFER, iv, |
| "subsample_count", G_TYPE_UINT, subsamples_count, |
| "subsamples", GST_TYPE_BUFFER, subsamples, |
| "cipher-mode", G_TYPE_STRING, cipher_mode, |
| NULL); |
| |
| if (encryption_scheme == kSbDrmEncryptionSchemeAesCbc) { |
| const auto& encryption_pattern = sample_infos[0].drm_info->encryption_pattern; |
| gst_structure_set(info, |
| "crypt_byte_block", G_TYPE_UINT, encryption_pattern.crypt_byte_block, |
| "skip_byte_block", G_TYPE_UINT, encryption_pattern.skip_byte_block, |
| NULL); |
| } |
| |
| gst_buffer_add_protection_meta(buffer, info); |
| |
| gst_buffer_unref(iv); |
| gst_buffer_unref(key); |
| gst_buffer_unref(subsamples); |
| } else { |
| GST_LOG_OBJECT(pipeline_, "Encounterd clear %s sample", |
| sample_type == kSbMediaTypeVideo ? "video" : "audio"); |
| } |
| |
| gint64 seek_pos_ns = GST_CLOCK_TIME_NONE; |
| uint64_t serial = 0; |
| bool keep_samples = false; |
| { |
| ::starboard::ScopedLock lock(mutex_); |
| keep_samples = is_seek_pending_; |
| serial = samples_serial_[ (sample_type == kSbMediaTypeVideo ? kVideoIndex : kAudioIndex) ]++; |
| if (sample_type == kSbMediaTypeVideo) |
| ++total_video_frames_; |
| if (seek_position_ != kSbTimeMax) |
| seek_pos_ns = seek_position_ * kSbTimeNanosecondsPerMicrosecond; |
| } |
| |
| if (GST_CLOCK_TIME_IS_VALID(seek_pos_ns) && seek_pos_ns > GST_BUFFER_TIMESTAMP(buffer)) { |
| // Set dummy duration to let sink drop out-of-segment samples |
| GST_BUFFER_DURATION (buffer) = GST_SECOND / 60; |
| GST_BUFFER_FLAG_SET (buffer, GST_BUFFER_FLAG_DECODE_ONLY); |
| } |
| |
| if (keep_samples) { |
| GST_INFO_OBJECT(pipeline_, "Pending flushing operation. Storing sample"); |
| GST_INFO_OBJECT(pipeline_, "SampleType:%d %" GST_TIME_FORMAT " id:%" PRIu64 " b:%" GST_PTR_FORMAT, |
| sample_type, GST_TIME_ARGS(GST_BUFFER_TIMESTAMP(buffer)), serial, buffer); |
| PendingSample sample(sample_type, buffer, serial); |
| buffer= nullptr; |
| ::starboard::ScopedLock lock(mutex_); |
| pending_samples_.emplace_back(std::move(sample)); |
| } |
| |
| { |
| // Let other thread finish writing |
| ::starboard::ScopedLock lock(mutex_); |
| while(has_oob_write_pending_) { |
| const auto kWaitTime = 10 * kSbTimeSecond; |
| if (!pending_oob_write_condition_.WaitTimed(kWaitTime)) { |
| GST_ERROR_OBJECT(pipeline_, "Pending write took too long, give up"); |
| has_oob_write_pending_ = false; |
| break; |
| } |
| } |
| } |
| |
| if (keep_samples) { |
| PendingSamples local_samples; |
| { |
| ::starboard::ScopedLock lock(mutex_); |
| local_samples.swap(pending_samples_); |
| } |
| |
| if(local_samples.empty()) { |
| GST_WARNING_OBJECT(pipeline_, "No pending samples"); |
| return; |
| } |
| |
| auto& sample = local_samples.back(); |
| |
| SB_CHECK(sample.Type() == sample_type); |
| |
| if (serial != sample.SerialID()) { |
| GST_WARNING_OBJECT(pipeline_, "Detected out-of-order sample. Expected serial: %" PRIu64 ", sample serial: %" PRIu64 "", |
| serial, sample.SerialID()); |
| } |
| |
| GstBuffer* buffer_copy = sample.CopyBuffer(); |
| if (!WriteSample(sample.Type(), buffer_copy, sample.SerialID())) { |
| gst_buffer_unref(buffer_copy); |
| } |
| |
| { |
| ::starboard::ScopedLock lock(mutex_); |
| std::move(local_samples.begin(), local_samples.end(), |
| std::back_inserter(pending_samples_)); |
| } |
| } else { |
| if (!WriteSample(sample_type, buffer, serial)) { |
| gst_buffer_unref(buffer); |
| } |
| } |
| |
| GST_LOG_OBJECT(pipeline_,"Wrote sample."); |
| } |
| |
| void PlayerImpl::SetVolume(double volume) { |
| GST_DEBUG_OBJECT(pipeline_, "volume %lf, TID %d", volume, SbThreadGetId()); |
| gst_stream_volume_set_volume(GST_STREAM_VOLUME(pipeline_), |
| GST_STREAM_VOLUME_FORMAT_LINEAR, volume); |
| } |
| |
| void PlayerImpl::HandleInititialSeek(::starboard::ScopedLock& lock) { |
| mutex_.DCheckAcquired(); |
| SB_DCHECK(state_ == State::kInitial || GST_STATE(pipeline_) < GST_STATE_PAUSED); |
| |
| if (state_ == State::kInitial) { |
| // This is the initial seek to 0 which will trigger data pumping. |
| SB_DCHECK(seek_position_ == .0); |
| state_ = State::kInitialPreroll; |
| DispatchOnWorkerThread( |
| new PlayerStatusTask(player_status_func_, player_, |
| ticket_, context_, |
| kSbPlayerStatePrerolling)); |
| seek_position_ = kSbTimeMax; |
| if (GST_STATE(pipeline_) < GST_STATE_PAUSED && |
| GST_STATE_PENDING(pipeline_) < GST_STATE_PAUSED) { |
| mutex_.Release(); |
| ChangePipelineState(GST_STATE_PAUSED); |
| mutex_.Acquire(); |
| } |
| return; |
| } |
| |
| // Ask for data. |
| if (state_ == State::kInitialPreroll) { |
| MediaType need_data = static_cast<MediaType>( |
| static_cast<int>(MediaType::kBoth) & ~has_enough_data_ ); |
| if (need_data != MediaType::kNone) |
| DecoderNeedsData(lock, need_data); |
| } |
| |
| #if GST_CHECK_VERSION(1, 18, 0) |
| // It is safe to replace the segment before app writes any data. |
| bool can_change_segment = !audio_caps_ && !video_caps_ && !isRialtoEnabled(); |
| if (can_change_segment) { |
| GST_INFO_OBJECT(pipeline_, "Changing initial segment."); |
| GstClockTime position = seek_position_ * kSbTimeNanosecondsPerMicrosecond; |
| GstSegment segment; |
| gst_segment_init (&segment, GST_FORMAT_TIME); |
| if ( !gst_segment_do_seek(&segment, !rate_ ? 1.0 : rate_, |
| GST_FORMAT_TIME, GST_SEEK_FLAG_ACCURATE, |
| GST_SEEK_TYPE_SET, position, |
| GST_SEEK_TYPE_NONE, 0, nullptr) ) { |
| GST_WARNING_OBJECT(pipeline_, "Segment seek failed."); |
| } |
| else if ( !gst_base_src_new_segment(GST_BASE_SRC(audio_appsrc_), &segment) ) { |
| GST_WARNING_OBJECT(pipeline_, "Could not change audio source segment."); |
| } |
| else if ( !gst_base_src_new_segment(GST_BASE_SRC(video_appsrc_), &segment) ) { |
| GST_WARNING_OBJECT(pipeline_, "Could not change video source segment."); |
| } |
| else { |
| DispatchOnWorkerThread(new PlayerStatusTask(player_status_func_, player_, ticket_, |
| context_, kSbPlayerStatePrerolling)); |
| AddBufferingProbe(position, ticket_); |
| GST_INFO_OBJECT(pipeline_, "Successfully changed initial segment, position: %" GST_TIME_FORMAT ", ticket: %d", GST_TIME_ARGS(position), ticket_); |
| return; |
| } |
| } |
| #endif |
| |
| // Else send seek after pre-roll. |
| GST_INFO_OBJECT(pipeline_, "Delaying seek."); |
| is_seek_pending_ = true; |
| } |
| |
| void PlayerImpl::Seek(SbTime seek_to_timestamp, int ticket) { |
| ::starboard::ScopedLock lock(seek_mutex_); |
| gint64 current_pos_ns = GetPosition(); |
| GST_INFO_OBJECT(pipeline_, "===> time %" PRId64 " (target=%" GST_TIME_FORMAT ", curr=%" GST_TIME_FORMAT ") TID: %d state %d, ticket = %d", |
| seek_to_timestamp, |
| GST_TIME_ARGS(seek_to_timestamp * kSbTimeNanosecondsPerMicrosecond), |
| GST_TIME_ARGS(current_pos_ns), |
| SbThreadGetId(), |
| static_cast<int>(state_), |
| ticket); |
| double rate = 1.; |
| { |
| ::starboard::ScopedLock lock(mutex_); |
| if (ticket_ > ticket) { |
| GST_INFO_OBJECT(pipeline_, "Ignore seek with ticket: %d (stored ticket: %d)", ticket, ticket_); |
| return; |
| } |
| |
| if (ticket_ != ticket) { |
| pending_samples_.clear(); |
| max_sample_timestamps_[kVideoIndex] = 0; |
| max_sample_timestamps_[kAudioIndex] = 0; |
| min_sample_timestamp_ = kSbTimeMax; |
| samples_serial_[kVideoIndex] = 0; |
| samples_serial_[kAudioIndex] = 0; |
| buf_target_min_ts_ = kSbTimeMax; |
| dropped_video_frames_ = 0; |
| total_video_frames_ = 0; |
| } |
| |
| ticket_ = ticket; |
| seek_position_ = seek_to_timestamp; |
| decoder_state_data_ = 0; |
| eos_data_ = 0; |
| is_seek_pending_ = false; |
| |
| rate = rate_; |
| |
| if (state_ == State::kInitial || GST_STATE(pipeline_) < GST_STATE_PAUSED) { |
| HandleInititialSeek(lock); |
| return; |
| } |
| } |
| |
| GST_DEBUG_OBJECT(pipeline_, "Calling seek"); |
| DispatchOnWorkerThread(new PlayerStatusTask(player_status_func_, player_, |
| ticket_, context_, |
| kSbPlayerStatePrerolling)); |
| if (!gst_element_seek(pipeline_, !rate ? 1.0 : rate, GST_FORMAT_TIME, |
| static_cast<GstSeekFlags>(GST_SEEK_FLAG_FLUSH | |
| GST_SEEK_FLAG_ACCURATE), |
| GST_SEEK_TYPE_SET, |
| seek_to_timestamp * kSbTimeNanosecondsPerMicrosecond, |
| GST_SEEK_TYPE_NONE, 0)) { |
| GST_ERROR_OBJECT(pipeline_, "Seek failed"); |
| DispatchOnWorkerThread(new PlayerStatusTask(player_status_func_, player_, |
| ticket_, context_, |
| kSbPlayerStatePresenting)); |
| DispatchOnWorkerThread(new FunctionTask([this]() { |
| state_ = State::kPresenting; |
| }, "Presenting after seek failure")); |
| } else { |
| GST_DEBUG_OBJECT(pipeline_, "Seek called with success"); |
| DispatchOnWorkerThread(new FunctionTask([this]() { |
| state_ = State::kPrerollAfterSeek; |
| }, "Preroll after seek")); |
| } |
| |
| AddBufferingProbe(seek_to_timestamp * kSbTimeNanosecondsPerMicrosecond, ticket); |
| } |
| |
| bool PlayerImpl::SetRate(double rate) { |
| GST_DEBUG_OBJECT(pipeline_, "===> rate %lf (rate_ %lf), TID: %d", rate, rate_, |
| SbThreadGetId()); |
| |
| GstState state; |
| double old_rate; |
| bool success = true; |
| |
| mutex_.Acquire(); |
| old_rate = rate_; |
| rate_ = rate; |
| pending_rate_ = .0; |
| |
| if (rate == .0) { |
| mutex_.Release(); |
| ChangePipelineState(GST_STATE_PAUSED); |
| return true; |
| } |
| |
| gst_element_get_state(pipeline_, &state, nullptr, 0); |
| |
| if (state < GST_STATE_PLAYING) |
| SchedulePlayingStateUpdate(); |
| |
| if (rate != 1. || need_instant_rate_change_) { |
| if (is_seek_pending_) { |
| mutex_.Release(); |
| GST_DEBUG_OBJECT(pipeline_, "Rate will be set when doing seek"); |
| return true; |
| } |
| if (state < GST_STATE_PLAYING || need_first_segment_ack_) { |
| rate_ = old_rate; |
| pending_rate_ = rate; |
| mutex_.Release(); |
| GST_DEBUG_OBJECT(pipeline_, "===> Set rate postponed"); |
| return true; |
| } |
| need_instant_rate_change_ = ( rate != 1. ); |
| mutex_.Release(); |
| |
| GstStructure* s = gst_structure_new( |
| kCustomInstantRateChangeEventName, "rate", G_TYPE_DOUBLE, rate, NULL); |
| success = gst_element_send_event( |
| pipeline_, gst_event_new_custom(GST_EVENT_CUSTOM_DOWNSTREAM_OOB, s)); |
| |
| mutex_.Acquire(); |
| } |
| |
| if (!success) { |
| rate_ = old_rate; |
| GST_ERROR_OBJECT(pipeline_, "Set rate failed"); |
| } |
| mutex_.Release(); |
| |
| return success; |
| } |
| |
| #if SB_API_VERSION >= 15 |
| void PlayerImpl::GetInfo(SbPlayerInfo* out_player_info) { |
| #else // SB_API_VERSION >= 15 |
| void PlayerImpl::GetInfo(SbPlayerInfo2* out_player_info) { |
| #endif // SB_API_VERSION >= 15 |
| |
| gint64 position = GetPosition(); |
| |
| CheckBuffering(position); |
| |
| GST_LOG_OBJECT( |
| pipeline_,"Current position: %" GST_TIME_FORMAT " (Seek position: %" GST_TIME_FORMAT ")", |
| GST_TIME_ARGS(position), |
| GST_TIME_ARGS(seek_position_ != kSbTimeMax ? seek_position_ * kSbTimeNanosecondsPerMicrosecond : GST_CLOCK_TIME_NONE)); |
| |
| out_player_info->duration = SB_PLAYER_NO_DURATION; |
| out_player_info->current_media_timestamp = |
| GST_CLOCK_TIME_IS_VALID(position) |
| ? position / kSbTimeNanosecondsPerMicrosecond |
| : 0; |
| |
| out_player_info->frame_width = frame_width_; |
| out_player_info->frame_height = frame_height_; |
| out_player_info->is_paused = GST_STATE(pipeline_) != GST_STATE_PLAYING; |
| out_player_info->volume = gst_stream_volume_get_volume( |
| GST_STREAM_VOLUME(pipeline_), GST_STREAM_VOLUME_FORMAT_LINEAR); |
| out_player_info->total_video_frames = total_video_frames_; |
| out_player_info->corrupted_video_frames = 0; |
| |
| { |
| ::starboard::ScopedLock lock(mutex_); |
| out_player_info->dropped_video_frames = dropped_video_frames_; |
| } |
| |
| GST_LOG_OBJECT( |
| pipeline_, |
| "Frames dropped: %d, Frames corrupted: %d", |
| out_player_info->dropped_video_frames, |
| out_player_info->corrupted_video_frames); |
| |
| out_player_info->playback_rate = rate_; |
| } |
| |
| void PlayerImpl::SetBounds(int zindex, int x, int y, int w, int h) { |
| GST_TRACE_OBJECT(pipeline_, "Set Bounds: %d %d %d %d %d", zindex, x, y, w, h); |
| GstElement* vid_sink = nullptr; |
| g_object_get(pipeline_, "video-sink", &vid_sink, nullptr); |
| if (vid_sink && g_object_class_find_property(G_OBJECT_GET_CLASS(vid_sink), |
| "rectangle")) { |
| gchar* rect = g_strdup_printf("%d,%d,%d,%d", x, y, w, h); |
| g_object_set(vid_sink, "rectangle", rect, nullptr); |
| free(rect); |
| } else { |
| pending_bounds_ = PendingBounds{x, y, w, h}; |
| } |
| if (vid_sink) |
| gst_object_unref(GST_OBJECT(vid_sink)); |
| } |
| |
| bool PlayerImpl::ChangePipelineState(GstState state) const { |
| if (force_stop_ && state > GST_STATE_READY) { |
| GST_INFO_OBJECT(pipeline_, "Ignore state change due to forced stop"); |
| return false; |
| } |
| |
| GstState current, pending; |
| current = pending = GST_STATE_VOID_PENDING; |
| gst_element_get_state(pipeline_, ¤t, &pending, 0); |
| if (current == state || pending == state) { |
| GST_DEBUG_OBJECT( |
| pipeline_, "Rejected state change to %s from %s with %s pending", |
| gst_element_state_get_name(state), |
| gst_element_state_get_name(current), |
| gst_element_state_get_name(pending)); |
| return true; |
| } |
| |
| if (state == GST_STATE_PLAYING) { |
| GstClockTime seek_pos_ns = GST_CLOCK_TIME_NONE; |
| SbTime min_ts = kSbTimeMax; |
| { |
| ::starboard::ScopedLock lock(mutex_); |
| if (seek_position_ != kSbTimeMax) { |
| seek_pos_ns = seek_position_ * kSbTimeNanosecondsPerMicrosecond; |
| min_ts = MinTimestamp(nullptr); |
| } |
| } |
| |
| if (GST_CLOCK_TIME_IS_VALID(seek_pos_ns)) { |
| if (kSbTimeMax == min_ts) { |
| GST_INFO_OBJECT(pipeline_, "Ignore state change to playing: invalid min sample ts"); |
| return false; |
| } |
| else if (seek_pos_ns > min_ts) { |
| GST_INFO_OBJECT( |
| pipeline_, |
| "Ignore state change to playing: no samples for seek time yet" |
| "(seek time: %" GST_TIME_FORMAT ", min sample time: %" GST_TIME_FORMAT ")", |
| GST_TIME_ARGS(seek_pos_ns), GST_TIME_ARGS(min_ts)); |
| return false; |
| } |
| } |
| } |
| else { |
| ::starboard::ScopedLock lock(mutex_); |
| guint src_id = std::exchange(playing_state_update_source_id_, 0u); |
| if (src_id != 0u) { |
| GSource* src = g_main_context_find_source_by_id(main_loop_context_, src_id); |
| g_source_destroy(src); |
| } |
| } |
| GST_INFO_OBJECT(pipeline_, "Changing state to %s", |
| gst_element_state_get_name(state)); |
| bool result = gst_element_set_state(pipeline_, state) != GST_STATE_CHANGE_FAILURE; |
| if (!result) { |
| GST_ERROR_OBJECT( |
| pipeline_, "Failed to change pipeline state to %s from %s with %s pending", |
| gst_element_state_get_name(state), |
| gst_element_state_get_name(current), |
| gst_element_state_get_name(pending)); |
| } |
| return result; |
| } |
| |
| void PlayerImpl::CheckBuffering(gint64 position) { |
| if (!GST_CLOCK_TIME_IS_VALID(position)) |
| return; |
| |
| constexpr SbTime kMarginNs = |
| 50 * kSbTimeMillisecond * kSbTimeNanosecondsPerMicrosecond; |
| |
| MediaType origin = MediaType::kNone; |
| SbTime min_ts = MinTimestamp(&origin); |
| |
| if (min_ts == kSbTimeMax) |
| return; |
| |
| if (min_ts + kMarginNs <= position && |
| GST_STATE(pipeline_) == GST_STATE_PLAYING && |
| GST_STATE_PENDING(pipeline_) != GST_STATE_PAUSED && |
| eos_data_ == 0) { |
| { |
| ::starboard::ScopedLock lock(mutex_); |
| DecoderNeedsData(lock, origin); |
| buf_target_min_ts_ = min_ts + kMarginNs; |
| } |
| PrintPositionPerSink(pipeline_, GST_LEVEL_INFO); |
| GST_INFO_OBJECT(pipeline_, "Pause for buffering. Pos: %" GST_TIME_FORMAT |
| ", min ts:%" GST_TIME_FORMAT, GST_TIME_ARGS(position), GST_TIME_ARGS(min_ts)); |
| ChangePipelineState(GST_STATE_PAUSED); |
| } else if (buf_target_min_ts_ != kSbTimeMax && min_ts > buf_target_min_ts_) { |
| double rate; |
| SbTime buf_target_min_ts = buf_target_min_ts_; |
| { |
| ::starboard::ScopedLock lock(mutex_); |
| buf_target_min_ts_ = kSbTimeMax; |
| rate = rate_; |
| } |
| GstState state, pending; |
| gst_element_get_state(pipeline_, &state, &pending, 0); |
| if (rate > .0 && state != GST_STATE_PLAYING && pending != GST_STATE_PLAYING) { |
| GST_INFO_OBJECT( |
| pipeline_, "Resuming playback. min_ts: %" GST_TIME_FORMAT ", buff traget: %" GST_TIME_FORMAT, |
| GST_TIME_ARGS(min_ts), GST_TIME_ARGS(buf_target_min_ts)); |
| ChangePipelineState(GST_STATE_PLAYING); |
| } |
| } |
| } |
| |
| gint64 PlayerImpl::GetPosition() const { |
| gint64 position = GST_CLOCK_TIME_NONE; |
| |
| GstQuery* query = gst_query_new_position(GST_FORMAT_TIME); |
| if (gst_element_query(pipeline_, query)) |
| gst_query_parse_position(query, 0, &position); |
| gst_query_unref(query); |
| |
| { |
| ::starboard::ScopedLock lock(mutex_); |
| gint64 seek_pos_ns = seek_position_ * kSbTimeNanosecondsPerMicrosecond; |
| double rate = rate_; |
| |
| if (!GST_CLOCK_TIME_IS_VALID(position)) { |
| if (seek_position_ != kSbTimeMax) |
| return seek_pos_ns; |
| if (GST_CLOCK_TIME_IS_VALID(cached_position_ns_)) |
| return cached_position_ns_; |
| return 0; |
| } |
| |
| if (seek_position_ != kSbTimeMax) { |
| if (GST_STATE(pipeline_) != GST_STATE_PLAYING) |
| return seek_pos_ns; |
| |
| if ((rate >= 0. && position <= seek_pos_ns) || |
| (rate < 0. && position >= seek_pos_ns)) { |
| return seek_pos_ns; |
| } |
| |
| cached_position_ns_ = seek_pos_ns; |
| seek_position_ = kSbTimeMax; |
| } |
| } |
| |
| if (GST_CLOCK_TIME_IS_VALID(cached_position_ns_) && |
| std::abs(position - cached_position_ns_) > GST_SECOND) { |
| PrintPositionPerSink(pipeline_, GST_LEVEL_WARNING); |
| GST_WARNING_OBJECT(pipeline_, "Unexpected position! More than 1 second jump detected: " |
| "%" GST_TIME_FORMAT " --> %" GST_TIME_FORMAT "", |
| GST_TIME_ARGS(cached_position_ns_), |
| GST_TIME_ARGS(position)); |
| } |
| |
| PrintPositionPerSink(pipeline_, GST_LEVEL_LOG); |
| cached_position_ns_ = position; |
| return position; |
| } |
| |
| void PlayerImpl::WritePendingSamples() { |
| PendingSamples local_samples; |
| bool keep_samples = false; |
| int ticket = -1; |
| { |
| ::starboard::ScopedLock lock(mutex_); |
| keep_samples = is_seek_pending_; |
| ticket = ticket_; |
| local_samples.swap(pending_samples_); |
| } |
| |
| if (!local_samples.empty()) { |
| std::sort( |
| local_samples.begin(), local_samples.end(), |
| [](const PendingSample& lhs, const PendingSample& rhs) -> bool { |
| return lhs.SerialID() < rhs.SerialID(); |
| }); |
| GstClockTime prev_timestamps[kMediaNumber] = {GST_CLOCK_TIME_NONE, GST_CLOCK_TIME_NONE}; |
| for (auto& sample : local_samples) { |
| auto &prev_ts = prev_timestamps[sample.Type() == kSbMediaTypeVideo ? kVideoIndex : kAudioIndex]; |
| |
| if (prev_ts == sample.Timestamp()) { |
| GST_WARNING_OBJECT(pipeline_, "Skipping %" GST_TIME_FORMAT ". Already written.", |
| GST_TIME_ARGS(prev_ts)); |
| continue; |
| } |
| |
| GstBuffer* buffer = keep_samples ? sample.CopyBuffer() : sample.TakeBuffer(); |
| GST_INFO_OBJECT(pipeline_, "Writing pending: SampleType:%d id:%" PRIu64 " b:%" GST_PTR_FORMAT, sample.Type(), sample.SerialID(), buffer); |
| prev_ts = GST_BUFFER_TIMESTAMP(buffer); |
| if (WriteSample(sample.Type(), buffer, sample.SerialID())) { |
| GST_INFO_OBJECT(pipeline_, "Pending sample was written."); |
| } else { |
| gst_buffer_unref(buffer); |
| } |
| } |
| |
| if (keep_samples) { |
| { |
| ::starboard::ScopedLock lock(mutex_); |
| if (ticket_ == ticket) { |
| std::move(local_samples.begin(), local_samples.end(), |
| std::back_inserter(pending_samples_)); |
| } else { |
| keep_samples = false; |
| } |
| } |
| if (keep_samples) { |
| GST_INFO_OBJECT(pipeline_, "Stored samples again."); |
| } else { |
| GST_INFO_OBJECT(pipeline_, "Seek ticket changed (%d -> %d), dropped local samples.", ticket, ticket_); |
| } |
| } |
| } |
| } |
| |
| MediaType PlayerImpl::GetBothMediaTypeTakingCodecsIntoAccount() const { |
| SB_DCHECK(audio_codec_ != kSbMediaAudioCodecNone || |
| video_codec_ != kSbMediaVideoCodecNone); |
| MediaType both_need_data = MediaType::kBoth; |
| |
| if (audio_codec_ == kSbMediaAudioCodecNone) |
| both_need_data = MediaType::kVideo; |
| |
| if (video_codec_ == kSbMediaVideoCodecNone) |
| both_need_data = MediaType::kAudio; |
| |
| return both_need_data; |
| } |
| |
| void PlayerImpl::RecordTimestamp(SbMediaType type, SbTime timestamp) { |
| if (type == kSbMediaTypeVideo) { |
| max_sample_timestamps_[kVideoIndex] = |
| std::max(max_sample_timestamps_[kVideoIndex], timestamp); |
| } else if (type == kSbMediaTypeAudio) { |
| max_sample_timestamps_[kAudioIndex] = |
| std::max(max_sample_timestamps_[kAudioIndex], timestamp); |
| } |
| |
| if (audio_codec_ == kSbMediaAudioCodecNone) { |
| min_sample_timestamp_origin_ = MediaType::kVideo; |
| min_sample_timestamp_ = max_sample_timestamps_[kVideoIndex]; |
| } else if (video_codec_ == kSbMediaVideoCodecNone) { |
| min_sample_timestamp_origin_ = MediaType::kAudio; |
| min_sample_timestamp_ = max_sample_timestamps_[kAudioIndex]; |
| } else { |
| min_sample_timestamp_ = std::min(max_sample_timestamps_[kVideoIndex], |
| max_sample_timestamps_[kAudioIndex]); |
| if (min_sample_timestamp_ == max_sample_timestamps_[kVideoIndex]) |
| min_sample_timestamp_origin_ = MediaType::kVideo; |
| else |
| min_sample_timestamp_origin_ = MediaType::kAudio; |
| } |
| } |
| |
| SbTime PlayerImpl::MinTimestamp(MediaType* origin) const { |
| if (origin) |
| *origin = min_sample_timestamp_origin_; |
| return min_sample_timestamp_; |
| } |
| |
| void PlayerImpl::HandleApplicationMessage(GstBus* bus, GstMessage* message) { |
| const GstStructure* structure = gst_message_get_structure(message); |
| if (gst_structure_has_name(structure, "force-stop") && !force_stop_) { |
| GST_INFO_OBJECT(pipeline_, "Received force STOP, pipeline = %p!!!", pipeline_); |
| force_stop_ = true; |
| ChangePipelineState(GST_STATE_READY); |
| g_signal_handlers_disconnect_by_func(pipeline_, reinterpret_cast<gpointer>(&PlayerImpl::SetupSource), this); |
| g_signal_handlers_disconnect_by_func(pipeline_, reinterpret_cast<gpointer>(&PlayerImpl::SetupElement), this); |
| ::starboard::ScopedLock lock(source_setup_mutex_); |
| if (source_setup_id_ > -1) { |
| GSource* src = g_main_context_find_source_by_id(main_loop_context_, source_setup_id_); |
| g_source_destroy(src); |
| source_setup_id_ = -1; |
| } |
| } |
| else if (gst_structure_has_name(structure, kDidReceiveFirstSegmentMsgName)) { |
| if (GST_MESSAGE_SRC(message) == GST_OBJECT(audio_appsrc_) || GST_MESSAGE_SRC(message) == GST_OBJECT(video_appsrc_)) { |
| GST_INFO_OBJECT(pipeline_, "Received '%s' message from %" GST_PTR_FORMAT, kDidReceiveFirstSegmentMsgName, GST_MESSAGE_SRC(message)); |
| bool should_set_rate = false; |
| double rate = 0.; |
| auto type = GST_MESSAGE_SRC(message) == GST_OBJECT(audio_appsrc_) ? MediaType::kAudio : MediaType::kVideo; |
| |
| mutex_.Acquire(); |
| need_first_segment_ack_ &= ~ static_cast<int>(type); |
| should_set_rate = (need_first_segment_ack_ == 0 && pending_rate_ != .0 && !is_seek_pending_); |
| rate = pending_rate_; |
| mutex_.Release(); |
| |
| if (should_set_rate) { |
| GST_INFO_OBJECT(pipeline_, "Sending pending SetRate(rate=%lf)", rate); |
| SetRate(rate); |
| } |
| } |
| } |
| else if (gst_structure_has_name(structure, kDidReachBufferingTargetMsgName)) { |
| if (GST_MESSAGE_SRC(message) == GST_OBJECT(audio_appsrc_) || GST_MESSAGE_SRC(message) == GST_OBJECT(video_appsrc_)) { |
| int ticket; |
| if (gst_structure_get_int(structure, "ticket", &ticket)) { |
| GST_INFO_OBJECT(pipeline_, "Received '%s' message from %" GST_PTR_FORMAT, kDidReachBufferingTargetMsgName, GST_MESSAGE_SRC(message)); |
| auto type = GST_MESSAGE_SRC(message) == GST_OBJECT(audio_appsrc_) ? MediaType::kAudio : MediaType::kVideo; |
| bool should_update_playing_state = false; |
| ::starboard::ScopedLock lock(mutex_); |
| if (ticket == ticket_ && buffering_state_ != 0) { |
| buffering_state_ &= ~(static_cast<int>(type)); |
| should_update_playing_state = (buffering_state_ == 0); |
| } |
| if (should_update_playing_state) |
| SchedulePlayingStateUpdate(); |
| } |
| } |
| } |
| } |
| |
| void PlayerImpl::ConfigureLimitedVideo() { |
| if (!isRialtoEnabled()) { |
| GstElementFactory* factory = gst_element_factory_find("westerossink"); |
| if (factory) { |
| GstElement* video_sink = gst_element_factory_create(factory, nullptr); |
| if (video_sink) { |
| if (g_object_class_find_property(G_OBJECT_GET_CLASS(video_sink), "res-usage")) { |
| g_object_set(video_sink, "res-usage", 0x0u, nullptr); |
| } |
| else { |
| GST_WARNING_OBJECT(pipeline_, "'westerossink' has no 'res-usage' property, secondary video may steal decoder"); |
| } |
| g_object_set(pipeline_, "video-sink", video_sink, nullptr); |
| } |
| else { |
| GST_DEBUG_OBJECT(pipeline_, "Failed to create 'westerossink'"); |
| } |
| gst_object_unref(GST_OBJECT(factory)); |
| } |
| |
| GstContext* context = gst_context_new("erm", FALSE); |
| GstStructure* context_structure = gst_context_writable_structure(context); |
| gst_structure_set(context_structure, "res-usage", G_TYPE_UINT, 0x0u, nullptr); |
| gst_element_set_context(GST_ELEMENT(pipeline_), context); |
| gst_context_unref(context); |
| |
| } |
| // enforce no audio |
| audio_codec_ = kSbMediaAudioCodecNone; |
| } |
| |
| void PlayerImpl::AddBufferingProbe(GstClockTime target, int ticket) { |
| struct BufferingProbeData { |
| GstClockTime target_time; |
| int ticket; |
| }; |
| |
| auto add_probe = [](GstElement* element, GstClockTime target, int ticket, GstPadProbeCallback callback) -> gulong { |
| BufferingProbeData* data = reinterpret_cast<BufferingProbeData*>(g_malloc0(sizeof (BufferingProbeData))); |
| data->target_time = target; |
| data->ticket = ticket; |
| |
| GstPad* pad = gst_element_get_static_pad(element, "src"); |
| GstPadProbeType probe_type = static_cast<GstPadProbeType>(GST_PAD_PROBE_TYPE_BUFFER | GST_PAD_PROBE_TYPE_EVENT_FLUSH); |
| gulong ret = gst_pad_add_probe (pad, probe_type, callback, data, g_free); |
| GST_DEBUG_OBJECT(element, |
| "Buffering probe added to %" GST_PTR_FORMAT ", target time: %" GST_TIME_FORMAT ", ticket: %d", |
| pad, GST_TIME_ARGS(target), ticket); |
| gst_object_unref(pad); |
| |
| return ret; |
| }; |
| |
| auto buffering_probe_callback = [](GstPad * pad, GstPadProbeInfo * info, gpointer user_data) -> GstPadProbeReturn { |
| if (GST_PAD_PROBE_INFO_TYPE(info) & GST_PAD_PROBE_TYPE_EVENT_FLUSH) |
| return GST_PAD_PROBE_REMOVE; |
| |
| SB_CHECK(GST_PAD_PROBE_INFO_TYPE(info) & GST_PAD_PROBE_TYPE_BUFFER); |
| |
| GstBuffer* buffer = gst_pad_probe_info_get_buffer(info); |
| BufferingProbeData* data = reinterpret_cast<BufferingProbeData*>(user_data); |
| |
| GST_TRACE_OBJECT(pad, "Testing buffer: %" GST_PTR_FORMAT " for target time: %" GST_TIME_FORMAT " with ticket: %d", buffer, GST_TIME_ARGS(data->target_time), data->ticket); |
| |
| if ( GST_BUFFER_TIMESTAMP(buffer) > data->target_time ) { |
| |
| GstObject* parent = gst_pad_get_parent(pad); |
| if (!parent) { |
| GST_WARNING_OBJECT(pad, "Pad(%" GST_PTR_FORMAT ") has no parent", pad); |
| SB_DCHECK(parent); |
| return GST_PAD_PROBE_REMOVE; |
| } |
| |
| GST_DEBUG_OBJECT(parent, "Did reach target buffering time:%" GST_TIME_FORMAT ", ticket: %d, on pad: %" GST_PTR_FORMAT, GST_TIME_ARGS(data->target_time), data->ticket, pad); |
| GstStructure* structure = gst_structure_new(kDidReachBufferingTargetMsgName, "ticket", G_TYPE_INT, data->ticket, nullptr); |
| gst_element_post_message(GST_ELEMENT(parent), gst_message_new_application(parent, structure)); |
| gst_object_unref(parent); |
| |
| return GST_PAD_PROBE_REMOVE; |
| } |
| |
| return GST_PAD_PROBE_OK; |
| }; |
| |
| buffering_state_ = 0; |
| |
| if (audio_appsrc_ && audio_codec_ != kSbMediaAudioCodecNone) { |
| if (add_probe(audio_appsrc_, target, ticket, buffering_probe_callback) != 0u) |
| buffering_state_ |= static_cast<int>(MediaType::kAudio); |
| } |
| |
| if (video_appsrc_ && video_codec_ != kSbMediaVideoCodecNone) { |
| target += 10 * 16 * GST_MSECOND; |
| if (add_probe(video_appsrc_, target, ticket, buffering_probe_callback) != 0u) |
| buffering_state_ |= static_cast<int>(MediaType::kVideo); |
| } |
| } |
| |
| void PlayerImpl::SchedulePlayingStateUpdate() { |
| mutex_.DCheckAcquired(); |
| |
| if (playing_state_update_source_id_ != 0u) // already scheduled |
| return; |
| |
| const auto update_callback = [](gpointer data) -> gboolean { |
| PlayerImpl* self = static_cast<PlayerImpl*>(data); |
| bool should_be_playing = false, can_play = false; |
| |
| { |
| GstState state, pending; |
| GstStateChangeReturn ret; |
| bool need_preroll; |
| guint src_id; |
| |
| ret = gst_element_get_state(self->pipeline_, &state, &pending, 0); |
| need_preroll = (state == GST_STATE_PAUSED && pending == GST_STATE_PAUSED && ret == GST_STATE_CHANGE_ASYNC); |
| |
| ::starboard::ScopedLock lock(self->mutex_); |
| src_id = std::exchange(self->playing_state_update_source_id_, 0u); |
| should_be_playing = (self->rate_ || self->pending_rate_); |
| can_play = (self->buffering_state_ == 0 && !need_preroll && src_id != 0u && self->buf_target_min_ts_ == kSbTimeMax); // the update was canceled when src_id == 0 |
| } |
| |
| GST_DEBUG_OBJECT(self->pipeline_, "Update pipeline state, should be playing: %d, can_play: %d", should_be_playing, can_play); |
| |
| if (should_be_playing && can_play) |
| self->ChangePipelineState(GST_STATE_PLAYING); |
| |
| return G_SOURCE_REMOVE; |
| }; |
| |
| GSource* src = g_idle_source_new(); |
| g_source_set_priority (src, G_PRIORITY_DEFAULT); |
| g_source_set_callback(src, update_callback, this, nullptr); |
| playing_state_update_source_id_ = g_source_attach(src, main_loop_context_); |
| g_source_unref(src); |
| } |
| |
| void PlayerImpl::DidEnd() { |
| if (state_ < State::kPresenting) { |
| DispatchOnWorkerThread( |
| new PlayerStatusTask( |
| player_status_func_, player_, ticket_, |
| context_, kSbPlayerStatePresenting)); |
| state_ = State::kPresenting; |
| } |
| |
| if (state_ == State::kPresenting) { |
| DispatchOnWorkerThread( |
| new PlayerStatusTask( |
| player_status_func_, player_, ticket_, |
| context_, kSbPlayerStateEndOfStream)); |
| state_ = State::kEnded; |
| } |
| } |
| |
| void PlayerImpl::AudioConfigurationChanged() { |
| GST_WARNING_OBJECT(pipeline_, "Emitting an error on audio configuration change."); |
| DispatchOnWorkerThread( |
| new PlayerErrorTask( |
| player_error_func_, player_, context_, |
| kSbPlayerErrorCapabilityChanged, "Audio device capability changed")); |
| |
| } |
| |
| } // namespace |
| |
| void ForceStop() { |
| using third_party::starboard::rdk::shared::player::GetPlayerRegistry; |
| GetPlayerRegistry()->ForceStop(); |
| } |
| |
| void AudioConfigurationChanged() { |
| using third_party::starboard::rdk::shared::player::GetPlayerRegistry; |
| GetPlayerRegistry()->AudioConfigurationChanged(); |
| } |
| |
| } // namespace player |
| } // namespace shared |
| } // namespace rdk |
| } // namespace starboard |
| } // namespace third_party |
| |
| using third_party::starboard::rdk::shared::player::PlayerImpl; |
| |
| SbPlayerPrivate::SbPlayerPrivate( |
| SbWindow window, |
| SbMediaVideoCodec video_codec, |
| SbMediaAudioCodec audio_codec, |
| SbDrmSystem drm_system, |
| #if SB_API_VERSION >= 15 |
| const SbMediaAudioStreamInfo& audio_info, |
| #else // SB_API_VERSION >= 15 |
| const SbMediaAudioSampleInfo& audio_info, |
| #endif |
| const char* max_video_capabilities, |
| SbPlayerDeallocateSampleFunc sample_deallocate_func, |
| SbPlayerDecoderStatusFunc decoder_status_func, |
| SbPlayerStatusFunc player_status_func, |
| SbPlayerErrorFunc player_error_func, |
| void* context, |
| SbPlayerOutputMode output_mode, |
| SbDecodeTargetGraphicsContextProvider* provider) { |
| if ( third_party::starboard::rdk::shared::player::GetPlayerRegistry()->CanCreate(max_video_capabilities) ) { |
| player_.reset( |
| new PlayerImpl(this, |
| window, |
| video_codec, |
| audio_codec, |
| drm_system, |
| audio_info, |
| max_video_capabilities, |
| sample_deallocate_func, |
| decoder_status_func, |
| player_status_func, |
| player_error_func, |
| context, |
| output_mode, |
| provider)); |
| if ( !static_cast<PlayerImpl&>(*player_).IsValid() ) |
| player_.reset(nullptr); |
| } |
| } |