| /* |
| * Copyright (C) 2020 Igalia S.L |
| * |
| * This library is free software; you can redistribute it and/or |
| * modify it under the terms of the GNU Library General Public |
| * License as published by the Free Software Foundation; either |
| * version 2 of the License, or (at your option) any later version. |
| * |
| * This library is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| * Library General Public License for more details. |
| * |
| * You should have received a copy of the GNU Library General Public License |
| * aint with this library; see the file COPYING.LIB. If not, write to |
| * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, |
| * Boston, MA 02110-1301, USA. |
| */ |
| |
| #include "config.h" |
| #include "ImageDecoderGStreamer.h" |
| |
| #if USE(GSTREAMER) && ENABLE(VIDEO) |
| |
| #include "FloatSize.h" |
| #include "GStreamerCommon.h" |
| #include "GStreamerRegistryScanner.h" |
| #include "ImageGStreamer.h" |
| #include "MediaSampleGStreamer.h" |
| #include "NotImplemented.h" |
| #include "RuntimeApplicationChecks.h" |
| #include <gst/app/gstappsink.h> |
| #include <wtf/Lock.h> |
| #include <wtf/MainThread.h> |
| #include <wtf/Scope.h> |
| #include <wtf/Threading.h> |
| |
| namespace WebCore { |
| |
| GST_DEBUG_CATEGORY(webkit_image_decoder_debug); |
| #define GST_CAT_DEFAULT webkit_image_decoder_debug |
| |
| class ImageDecoderGStreamerSample final : public MediaSampleGStreamer { |
| public: |
| static Ref<ImageDecoderGStreamerSample> create(GRefPtr<GstSample>&& sample, const FloatSize& presentationSize) |
| { |
| return adoptRef(*new ImageDecoderGStreamerSample(WTFMove(sample), presentationSize)); |
| } |
| |
| PlatformImagePtr image() const |
| { |
| if (!m_image) |
| return nullptr; |
| return m_image->image().nativeImage()->platformImage(); |
| } |
| void dropImage() { m_image = nullptr; } |
| |
| SampleFlags flags() const override |
| { |
| return (SampleFlags)(MediaSampleGStreamer::flags() | (m_image && m_image->hasAlpha() ? HasAlpha : 0)); |
| } |
| |
| private: |
| ImageDecoderGStreamerSample(GRefPtr<GstSample>&& sample, const FloatSize& presentationSize) |
| : MediaSampleGStreamer(WTFMove(sample), presentationSize, { }) |
| { |
| m_image = ImageGStreamer::createImage(platformSample().sample.gstSample); |
| } |
| |
| RefPtr<ImageGStreamer> m_image; |
| }; |
| |
| static ImageDecoderGStreamerSample* toSample(const PresentationOrderSampleMap::value_type& pair) |
| { |
| return (ImageDecoderGStreamerSample*)pair.second.get(); |
| } |
| |
| template <typename Iterator> |
| ImageDecoderGStreamerSample* toSample(Iterator iter) |
| { |
| return (ImageDecoderGStreamerSample*)iter->second.get(); |
| } |
| |
| RefPtr<ImageDecoderGStreamer> ImageDecoderGStreamer::create(FragmentedSharedBuffer& data, const String& mimeType, AlphaOption alphaOption, GammaAndColorProfileOption gammaAndColorProfileOption) |
| { |
| return adoptRef(*new ImageDecoderGStreamer(data, mimeType, alphaOption, gammaAndColorProfileOption)); |
| } |
| |
| ImageDecoderGStreamer::ImageDecoderGStreamer(FragmentedSharedBuffer& data, const String& mimeType, AlphaOption, GammaAndColorProfileOption) |
| : m_mimeType(mimeType) |
| { |
| static std::once_flag onceFlag; |
| std::call_once(onceFlag, [] { |
| GST_DEBUG_CATEGORY_INIT(webkit_image_decoder_debug, "webkitimagedecoder", 0, "WebKit image decoder"); |
| }); |
| |
| pushEncodedData(data); |
| } |
| |
| bool ImageDecoderGStreamer::supportsContainerType(const String& type) |
| { |
| // Ideally this decoder should operate only from the WebProcess (or from the GPUProcess) which |
| // should be the only process where GStreamer has been runtime initialized. |
| if (!isInWebProcess()) |
| return false; |
| |
| if (!type.startsWith("video/"_s)) |
| return false; |
| |
| return GStreamerRegistryScanner::singleton().isContainerTypeSupported(GStreamerRegistryScanner::Configuration::Decoding, type); |
| } |
| |
| bool ImageDecoderGStreamer::canDecodeType(const String& mimeType) |
| { |
| if (mimeType.isEmpty()) |
| return false; |
| |
| if (!mimeType.startsWith("video/"_s)) |
| return false; |
| |
| // Ideally this decoder should operate only from the WebProcess (or from the GPUProcess) which |
| // should be the only process where GStreamer has been runtime initialized. |
| if (!isInWebProcess()) |
| return false; |
| |
| return GStreamerRegistryScanner::singleton().isContainerTypeSupported(GStreamerRegistryScanner::Configuration::Decoding, mimeType); |
| } |
| |
| EncodedDataStatus ImageDecoderGStreamer::encodedDataStatus() const |
| { |
| if (m_eos) |
| return EncodedDataStatus::Complete; |
| if (m_size) |
| return EncodedDataStatus::SizeAvailable; |
| if (m_innerDecoder) |
| return m_innerDecoder->encodedDataStatus(); |
| return EncodedDataStatus::Unknown; |
| } |
| |
| IntSize ImageDecoderGStreamer::size() const |
| { |
| if (m_size) |
| return m_size.value(); |
| return { }; |
| } |
| |
| RepetitionCount ImageDecoderGStreamer::repetitionCount() const |
| { |
| // In the absence of instructions to the contrary, assume all media formats repeat infinitely. |
| return frameCount() > 1 ? RepetitionCountInfinite : RepetitionCountNone; |
| } |
| |
| String ImageDecoderGStreamer::uti() const |
| { |
| notImplemented(); |
| return { }; |
| } |
| |
| ImageDecoder::FrameMetadata ImageDecoderGStreamer::frameMetadataAtIndex(size_t) const |
| { |
| notImplemented(); |
| return { }; |
| } |
| |
| Seconds ImageDecoderGStreamer::frameDurationAtIndex(size_t index) const |
| { |
| auto* sampleData = sampleAtIndex(index); |
| if (!sampleData) |
| return { }; |
| |
| return Seconds(sampleData->duration().toDouble()); |
| } |
| |
| bool ImageDecoderGStreamer::frameHasAlphaAtIndex(size_t index) const |
| { |
| auto* sampleData = sampleAtIndex(index); |
| return sampleData ? sampleData->hasAlpha() : false; |
| } |
| |
| unsigned ImageDecoderGStreamer::frameBytesAtIndex(size_t index, SubsamplingLevel subsamplingLevel) const |
| { |
| if (!frameIsCompleteAtIndex(index)) |
| return 0; |
| |
| return frameSizeAtIndex(index, subsamplingLevel).area() * 4; |
| } |
| |
| PlatformImagePtr ImageDecoderGStreamer::createFrameImageAtIndex(size_t index, SubsamplingLevel, const DecodingOptions&) |
| { |
| Locker locker { m_sampleGeneratorLock }; |
| |
| auto* sampleData = sampleAtIndex(index); |
| if (!sampleData) |
| return nullptr; |
| |
| if (auto image = sampleData->image()) |
| return image; |
| |
| return nullptr; |
| } |
| |
| void ImageDecoderGStreamer::setData(const FragmentedSharedBuffer& data, bool) |
| { |
| pushEncodedData(data); |
| } |
| |
| void ImageDecoderGStreamer::clearFrameBufferCache(size_t index) |
| { |
| size_t i = 0; |
| for (auto& samplePair : m_sampleData.presentationOrder()) { |
| toSample(samplePair)->dropImage(); |
| if (++i > index) |
| break; |
| } |
| } |
| |
| const ImageDecoderGStreamerSample* ImageDecoderGStreamer::sampleAtIndex(size_t index) const |
| { |
| if (index >= m_sampleData.presentationOrder().size()) |
| return nullptr; |
| |
| // FIXME: std::map is not random-accessible; this can get expensive if callers repeatedly call |
| // with monotonically increasing indexes. Investigate adding an O(1) side structure to make this |
| // style of access faster. |
| auto iter = m_sampleData.presentationOrder().begin(); |
| for (size_t i = 0; i != index; ++i) |
| ++iter; |
| |
| return toSample(iter); |
| } |
| |
| void ImageDecoderGStreamer::InnerDecoder::decodebinPadAddedCallback(ImageDecoderGStreamer::InnerDecoder* decoder, GstPad* pad) |
| { |
| decoder->connectDecoderPad(pad); |
| } |
| |
| void ImageDecoderGStreamer::InnerDecoder::connectDecoderPad(GstPad* pad) |
| { |
| auto padCaps = adoptGRef(gst_pad_query_caps(pad, nullptr)); |
| GST_DEBUG_OBJECT(m_pipeline.get(), "New decodebin pad %" GST_PTR_FORMAT " caps: %" GST_PTR_FORMAT, pad, padCaps.get()); |
| |
| // Decodebin3 in GStreamer <= 1.16 does not respect user-supplied select-stream events. So we |
| // need to relax the release assert for these versions. This bug was fixed in: |
| // https://gitlab.freedesktop.org/gstreamer/gst-plugins-base/-/commit/b41b87522f59355bb21c001e9e2df96dc6956928 |
| bool isVideo = doCapsHaveType(padCaps.get(), "video"); |
| if (webkitGstCheckVersion(1, 18, 0)) |
| RELEASE_ASSERT(isVideo); |
| else if (!isVideo) |
| return; |
| |
| GstElement* sink = makeGStreamerElement("appsink", nullptr); |
| static GstAppSinkCallbacks callbacks = { |
| nullptr, |
| [](GstAppSink* sink, gpointer userData) -> GstFlowReturn { |
| auto sample = adoptGRef(gst_app_sink_try_pull_preroll(sink, 0)); |
| static_cast<ImageDecoderGStreamer*>(userData)->notifySample(WTFMove(sample)); |
| return GST_FLOW_OK; |
| }, |
| [](GstAppSink* sink, gpointer userData) -> GstFlowReturn { |
| auto sample = adoptGRef(gst_app_sink_try_pull_sample(sink, 0)); |
| static_cast<ImageDecoderGStreamer*>(userData)->notifySample(WTFMove(sample)); |
| return GST_FLOW_OK; |
| }, |
| #if GST_CHECK_VERSION(1, 20, 0) |
| // new_event |
| nullptr, |
| #endif |
| { nullptr } |
| }; |
| gst_app_sink_set_callbacks(GST_APP_SINK(sink), &callbacks, &m_decoder, nullptr); |
| |
| GRefPtr<GstCaps> caps = adoptGRef(gst_caps_from_string("video/x-raw, format=(string)RGBA")); |
| g_object_set(sink, "sync", false, "caps", caps.get(), nullptr); |
| |
| GstElement* videoconvert = makeGStreamerElement("videoconvert", nullptr); |
| |
| gst_bin_add_many(GST_BIN_CAST(m_pipeline.get()), videoconvert, sink, nullptr); |
| gst_element_link(videoconvert, sink); |
| auto sinkPad = adoptGRef(gst_element_get_static_pad(videoconvert, "sink")); |
| gst_pad_link(pad, sinkPad.get()); |
| gst_element_sync_state_with_parent(videoconvert); |
| gst_element_sync_state_with_parent(sink); |
| } |
| |
| void ImageDecoderGStreamer::setHasEOS() |
| { |
| GST_DEBUG("EOS on decoder %p", this); |
| { |
| Locker locker { m_sampleLock }; |
| m_eos = true; |
| m_sampleCondition.notifyOne(); |
| } |
| { |
| Locker locker { m_handlerLock }; |
| m_handlerCondition.wait(m_handlerLock); |
| } |
| } |
| |
| void ImageDecoderGStreamer::notifySample(GRefPtr<GstSample>&& sample) |
| { |
| { |
| Locker locker { m_sampleLock }; |
| m_sample = WTFMove(sample); |
| m_sampleCondition.notifyOne(); |
| } |
| { |
| Locker locker { m_handlerLock }; |
| m_handlerCondition.wait(m_handlerLock); |
| } |
| } |
| |
| void ImageDecoderGStreamer::InnerDecoder::handleMessage(GstMessage* message) |
| { |
| ASSERT(&m_runLoop == &RunLoop::current()); |
| |
| auto scopeExit = makeScopeExit([protectedThis = WeakPtr { *this }] { |
| if (!protectedThis) |
| return; |
| Locker locker { protectedThis->m_messageLock }; |
| protectedThis->m_messageDispatched = true; |
| protectedThis->m_messageCondition.notifyOne(); |
| }); |
| |
| GUniqueOutPtr<GError> error; |
| GUniqueOutPtr<gchar> debug; |
| |
| switch (GST_MESSAGE_TYPE(message)) { |
| case GST_MESSAGE_EOS: |
| m_decoder.setHasEOS(); |
| break; |
| case GST_MESSAGE_WARNING: |
| gst_message_parse_warning(message, &error.outPtr(), &debug.outPtr()); |
| g_warning("Warning: %d, %s. Debug output: %s", error->code, error->message, debug.get()); |
| break; |
| case GST_MESSAGE_ERROR: |
| gst_message_parse_error(message, &error.outPtr(), &debug.outPtr()); |
| g_warning("Error: %d, %s. Debug output: %s", error->code, error->message, debug.get()); |
| m_decoder.setHasEOS(); |
| break; |
| case GST_MESSAGE_STREAM_COLLECTION: { |
| GRefPtr<GstStreamCollection> collection; |
| gst_message_parse_stream_collection(message, &collection.outPtr()); |
| if (collection && GST_MESSAGE_SRC(message) == GST_OBJECT_CAST(m_decodebin.get())) { |
| unsigned size = gst_stream_collection_get_size(collection.get()); |
| GList* streams = nullptr; |
| for (unsigned i = 0 ; i < size; i++) { |
| auto* stream = gst_stream_collection_get_stream(collection.get(), i); |
| auto streamType = gst_stream_get_stream_type(stream); |
| if (streamType == GST_STREAM_TYPE_VIDEO) { |
| streams = g_list_append(streams, const_cast<char*>(gst_stream_get_stream_id(stream))); |
| break; |
| } |
| } |
| if (streams) { |
| gst_element_send_event(m_decodebin.get(), gst_event_new_select_streams(streams)); |
| g_list_free(streams); |
| } |
| } |
| break; |
| } |
| default: |
| break; |
| } |
| } |
| |
| void ImageDecoderGStreamer::InnerDecoder::preparePipeline() |
| { |
| static Atomic<uint32_t> pipelineId; |
| m_pipeline = gst_pipeline_new(makeString("image-decoder-", pipelineId.exchangeAdd(1)).utf8().data()); |
| |
| GRefPtr<GstBus> bus = adoptGRef(gst_pipeline_get_bus(GST_PIPELINE(m_pipeline.get()))); |
| ASSERT(bus); |
| |
| gst_bus_set_sync_handler(bus.get(), [](GstBus*, GstMessage* message, gpointer userData) { |
| auto& decoder = *static_cast<ImageDecoderGStreamer::InnerDecoder*>(userData); |
| |
| { |
| Locker locker { decoder.m_messageLock }; |
| decoder.m_messageDispatched = false; |
| } |
| if (&decoder.m_runLoop == &RunLoop::current()) |
| decoder.handleMessage(message); |
| else { |
| GRefPtr<GstMessage> protectedMessage(message); |
| WeakPtr weakThis { decoder }; |
| decoder.m_runLoop.dispatch([weakThis, protectedMessage] { |
| if (weakThis) |
| weakThis->handleMessage(protectedMessage.get()); |
| }); |
| { |
| Locker locker { decoder.m_messageLock }; |
| if (!decoder.m_messageDispatched) |
| decoder.m_messageCondition.wait(decoder.m_messageLock); |
| } |
| } |
| gst_message_unref(message); |
| return GST_BUS_DROP; |
| }, this, nullptr); |
| |
| GstElement* source = makeGStreamerElement("giostreamsrc", nullptr); |
| g_object_set(source, "stream", m_memoryStream.get(), nullptr); |
| |
| m_decodebin = makeGStreamerElement("decodebin3", nullptr); |
| g_signal_connect_swapped(m_decodebin.get(), "pad-added", G_CALLBACK(decodebinPadAddedCallback), this); |
| |
| gst_bin_add_many(GST_BIN_CAST(m_pipeline.get()), source, m_decodebin.get(), nullptr); |
| gst_element_link(source, m_decodebin.get()); |
| gst_element_set_state(m_pipeline.get(), GST_STATE_PLAYING); |
| } |
| |
| void ImageDecoderGStreamer::InnerDecoder::run() |
| { |
| m_runLoop.dispatch([this]() { |
| preparePipeline(); |
| }); |
| |
| m_runLoop.run(); |
| } |
| |
| EncodedDataStatus ImageDecoderGStreamer::InnerDecoder::encodedDataStatus() const |
| { |
| GstState state; |
| gst_element_get_state(m_pipeline.get(), &state, nullptr, 0); |
| if (state >= GST_STATE_READY) |
| return EncodedDataStatus::TypeAvailable; |
| return EncodedDataStatus::Unknown; |
| } |
| |
| void ImageDecoderGStreamer::pushEncodedData(const FragmentedSharedBuffer& buffer) |
| { |
| auto contiguousBuffer = buffer.makeContiguous(); |
| m_eos = false; |
| auto thread = Thread::create("ImageDecoderGStreamer", [this, data = contiguousBuffer->data(), size = buffer.size()] { |
| m_innerDecoder = ImageDecoderGStreamer::InnerDecoder::create(*this, data, size); |
| m_innerDecoder->run(); |
| }, ThreadType::Graphics); |
| thread->detach(); |
| bool isEOS = false; |
| { |
| Locker locker { m_sampleLock }; |
| isEOS = m_eos; |
| } |
| while (!isEOS) { |
| { |
| Locker locker { m_sampleLock }; |
| m_sampleCondition.wait(m_sampleLock); |
| isEOS = m_eos; |
| if (m_sample) { |
| auto* caps = gst_sample_get_caps(m_sample.get()); |
| GST_DEBUG("Handling sample with caps %" GST_PTR_FORMAT " on decoder %p", caps, this); |
| auto presentationSize = getVideoResolutionFromCaps(caps); |
| if (presentationSize && !presentationSize->isEmpty() && (!m_size || m_size != roundedIntSize(*presentationSize))) |
| m_size = roundedIntSize(*presentationSize); |
| m_sampleData.addSample(ImageDecoderGStreamerSample::create(WTFMove(m_sample), *m_size)); |
| } |
| } |
| { |
| Locker locker { m_handlerLock }; |
| m_handlerCondition.notifyAll(); |
| } |
| } |
| m_innerDecoder = nullptr; |
| callOnMainThreadAndWait([&] { |
| if (m_encodedDataStatusChangedCallback) |
| m_encodedDataStatusChangedCallback(encodedDataStatus()); |
| }); |
| } |
| |
| } |
| #endif |